elwayman02/ember-interactivity

View on GitHub
addon/services/interactivity.js

Summary

Maintainability
A
0 mins
Test Coverage
import { bind } from '@ember/runloop';
import Service from '@ember/service';
import {
  getLatencySubscriptionId,
  getLatencyReportingName
} from 'ember-interactivity/utils/interactivity';
import {
  RouteInteractivitySubscriber,
  ComponentInteractivitySubscriber
} from 'ember-interactivity/utils/interactivity-subscriber';

/**
 * This service keeps track of all rendered components that have reported that they
 * are ready for user interaction. This service also allows a Route to monitor for a
 * custom interactivity condition to be met.
 *
 * Use with these mixins: component-interactivity & route-interactivity
 */
export default Service.extend({
  /**
   * The current route being tracked for interactivity
   */
  _currentRouteSubscriber: null,

  /**
   * Components that rely on their children to report interactivity
   */
  _componentSubscribers: null,

  /**
   * Setup private variables
   *
   * @method init
   */
  init() {
    this._super(...arguments);

    this._componentSubscribers = {};
    this._currentRouteSubscriber = new RouteInteractivitySubscriber();
  },

  /**
   * Track a component's latency. When components become interactive or non-interactive, check the component's
   * `isInteractive()` to determine if the component is deemed "ready for user interaction".
   *
   * @method subscribeComponent
   *
   * @param {object} options - Single configuration parameter that expects the following attributes:
   *    {string} id - Unique component id
   *    {function} isInteractive - Method for checking interactivity conditions as reports come in
   * @return {RSVP.Promise} Resolves when interactivity conditions are met
   */
  subscribeComponent({ id, isInteractive }) {
    let subscriber = new ComponentInteractivitySubscriber({
      id,
      isInteractive
    });

    this._componentSubscribers[id] = subscriber;
    return subscriber.promise;
  },

  /**
   * Unsubscribe the component from latency tracking. This is used for teardown.
   *
   * @method unsubscribeComponent
   */
  unsubscribeComponent(subscriberId) {
    this._componentSubscribers[subscriberId] = null;
  },

  /**
   * Track a route's latency. When components become interactive or non-interactive, check the route's
   * `isInteractive()` to determine if the route is deemed "ready for user interaction". Only one route should be tracked at a time.
   *
   * @method subscribeRoute
   *
   * @param {object} options - Single configuration parameter that expects the following attributes:
   *    {string} name - The name of the subscriber (used in testing) // TODO: Still needed?
   *    {function} isInteractive - Method for checking interactivity conditions as reports come in
   * @return {RSVP.Promise} Resolves when interactivity conditions are met
   */
  subscribeRoute(options) {
    this.unsubscribeRoute();
    this._currentRouteSubscriber.subscribe(options);
    this._currentRouteSubscriber.checkInteractivity();
    return this._currentRouteSubscriber.promise.then(bind(this, this.unsubscribeRoute));
  },

  /**
   * Unsubscribe the current route from latency tracking. This is used for teardown.
   *
   * @method unsubscribeRoute
   */
  unsubscribeRoute() {
    this._currentRouteSubscriber.unsubscribe();
  },

  /**
   * Find the correct parent subscriber for the given component
   *
   * @method subscriberFor
   *
   * @param {Ember.Component} reporter - The component reporting interactivity
   * @return {ComponentInteractivitySubscriber|RouteInteractivitySubscriber} The parent subscriber
   */
  subscriberFor(reporter) {
    let componentSubscriber = this._findParentSubscriber(reporter);

    if (componentSubscriber) {
      return componentSubscriber;
    }

    return this._currentRouteSubscriber;
  },

  /**
   * Notify the service that a reporter became interactive.
   * Checks the appropriate subscriber for interactivity conditions.
   *
   * @method didReporterBecomeInteractive
   *
   * @param {Ember.Component} reporter - The component that is now interactive
   */
  didReporterBecomeInteractive(reporter) {
    let subscriber = this.subscriberFor(reporter);
    subscriber.childBecameInteractive(reporter);
    subscriber.checkInteractivity();
  },

  /**
   * Notify the service that a reporter became non-interactive.
   * Checks the appropriate subscriber for interactivity conditions.
   *
   * @method didReporterBecomeNonInteractive
   *
   * @param {Ember.Component} reporter - The component that is no longer interactive
   */
  didReporterBecomeNonInteractive(reporter) {
    let subscriber = this.subscriberFor(reporter);
    subscriber.childBecameNonInteractive(reporter);
    subscriber.checkInteractivity();
  },

  /**
   * Finds whether a parent of this component is subscribed to the interactivity service
   *
   * @method _findParentSubscriber
   * @private
   *
   * @param {Ember.Component} child - The child component
   * @return {ComponentInteractivitySubscriber|undefined} The parent subscriber, if it exists
   */
  _findParentSubscriber(child) {
    let parentId, parentName;

    while (parentName !== 'application-wrapper' && child.parentView) {
      parentId = getLatencySubscriptionId(child.parentView);
      if (this._componentSubscribers[parentId]) {
        return this._componentSubscribers[parentId];
      }
      parentName = getLatencyReportingName(child.parentView);
      child = child.parentView;
    }
  }
});