elwayman02/ember-interactivity

View on GitHub
addon/mixins/component-interactivity.js

Summary

Maintainability
A
0 mins
Test Coverage
import Ember from 'ember';
import { assert } from '@ember/debug';
import { computed } from '@ember/object';
import { on } from '@ember/object/evented';
import Mixin from '@ember/object/mixin';
import { assign } from '@ember/polyfills';
import { bind } from '@ember/runloop';
import { inject as injectService } from '@ember/service';
import IsFastbootMixin from 'ember-is-fastboot/mixins/is-fastboot';
import getConfig from 'ember-interactivity/utils/config';
import { getTimeAsFloat } from 'ember-interactivity/utils/date';
import {
  getLatencySubscriptionId,
  getLatencyReportingName
} from 'ember-interactivity/utils/interactivity';
import { INITIALIZING_LABEL, INTERACTIVE_LABEL, markTimeline } from 'ember-interactivity/utils/timeline-marking';

/**
 * For components that should inform the interactivity service that they are now ready for user interaction.
 *
 * In your component, you MUST call `reportInteractive` or define `isInteractive`.
 */
export default Mixin.create(IsFastbootMixin, {
  interactivity: injectService(),
  interactivityTracking: injectService(),

  /**
   * A component may implement the method isInteractive, which returns true if all conditions for interactivity have been met
   *
   * If isInteractive is defined, it is used to see if conditions are met and then fires the interactive event.
   * If isInteractive is not defined, the developer must call `reportInteractive` manually.
   *
   * @method isInteractive
   * @param {function} didReportInteractive - Method that takes a reporter name and returns whether it is interactive
   * @return {boolean} True if all interactivity conditions have been met
   */
  isInteractive: null,

  /**
   * Subscribe component for interactivity tracking
   */
  willInsertElement() {
    this._super(...arguments);

    this._isInitializing();
    if (this._isSubscriber()) { // Component has implemented the `isInteractive` method
      this.get('interactivity').subscribeComponent({
        id: this.get('_latencySubscriptionId'),
        name: this.get('_latencyReportingName'),
        isInteractive: bind(this, this.isInteractive)
      }).then(bind(this, this._becameInteractive));
    }
  },

  /**
   * Unsubscribe component from interactivity tracking
   */
  willDestroyElement() {
    this._super(...arguments);

    if (this._isSubscriber()) {
      this.get('interactivity').unsubscribeComponent(this.get('_latencySubscriptionId'));
    }
  },

  /**
   * This method will notify the `interactivity` service that the component has
   * finished rendering and is now interactive for the user.
   *
   * Example:
   * interactiveAfterRendered: on('didInsertElement', function () {
   *   scheduleOnce('afterRender', this, this.reportInteractive);
   * })
   *
   * @method reportInteractive
   */
  reportInteractive() {
    assert(`Do not invoke reportInteractive if isInteractive is defined: {{${this.get('_latencyReportingName')}}}`, !this._isSubscriber());
    this._becameInteractive();
  },

  /**
   * Call this method if the component is no longer interactive (e.g. reloading data)
   * Also executes by default during component teardown
   *
   * @method reportNonInteractive
   */
  reportNonInteractive: on('willDestroyElement', function () {
    this.get('interactivity').didReporterBecomeNonInteractive(this);
  }),

  /**
   * Human-readable component name
   * @private
   */
  _latencyReportingName: computed(function () {
    return getLatencyReportingName(this);
  }),

  /**
   * Unique component ID, useful for distinguishing multiple instances of the same component
   * @private
   */
  _latencySubscriptionId: computed(function () {
    return getLatencySubscriptionId(this);
  }),

  /**
   * Marks that the component has become interactive and sends a tracking event.
   * If enabled, adds the event to the performance timeline.
   *
   * @method _becameInteractive
   * @private
   */
  _becameInteractive() {
    let timestamp = getTimeAsFloat();
    this.get('interactivity').unsubscribeComponent(this.get('_latencySubscriptionId'));
    this._markTimeline(INTERACTIVE_LABEL);

    this._sendEvent('componentInteractive', {
      clientTime: timestamp,
      timeElapsed: timestamp - this._componentInitializingTimestamp
    });

    this.get('interactivity').didReporterBecomeInteractive(this);
  },

  /**
   * Marks that the component has begun rendering.
   * If enabled, adds the event to the performance timeline.
   *
   * @method _isInitializing
   * @private
   */
  _isInitializing() {
    this._componentInitializingTimestamp = getTimeAsFloat();
    this._markTimeline(INITIALIZING_LABEL);
    this._sendEvent('componentInitializing', { clientTime: this._componentInitializingTimestamp });
  },

  /**
   * Determines whether this component is a subscriber (relies on instrumented child components)
   *
   * @method _isSubscriber
   * @private
   *
   * @return {boolean} Subscriber status
   */
  _isSubscriber() {
    return !!this.isInteractive;
  },

  /**
   * Creates a unique label for use in the performance timeline
   *
   * @method _getTimelineLabel
   * @private
   *
   * @param {string} type - The type of label being created
   * @return {string} The timeline label
   */
  _getTimelineLabel(type) { // BUG: Components that have "component" in their name will not have a unique label, due to the parsing logic below
    let latencyId = this.get('_latencySubscriptionId').split('component:')[1].slice(0, -1); // Make the component name more readable but still unique
    return `Component ${type}: ${latencyId}`;
  },

  /**
   * Marks the performance timeline with component latency events
   *
   * @method _markTimeline
   * @private
   *
   * @param {string} type - The event type
   */
  _markTimeline(type) {
    if(Ember.testing || this.get('_isFastBoot') || this._isFeaturedDisabled('timelineMarking')) {
      return;
    }

    markTimeline(type, bind(this, this._getTimelineLabel));
  },

  /**
   * Sends tracking information for the component's interactivity
   *
   * @method _sendEvent
   * @private
   *
   * @param {string} name - Name of the event
   * @param {object} data - Data attributes for the event
   */
  _sendEvent(name, data = {}) {
    if (this.get('_isFastBoot') || this._isFeaturedDisabled('tracking')) {
      return;
    }

    this.get('interactivityTracking').trackComponent(assign({
      event: name,
      component: this.get('_latencyReportingName'),
      componentId: this.get('_latencySubscriptionId')
    }, data));
  },

  /**
   * Check to see if a feature has been disabled by the app config
   *
   * @method _isFeatureDisabled
   * @private
   *
   * @param {string} type - The name of the feature being checked
   * @return {boolean} - True if the feature is disabled
   */
  _isFeaturedDisabled(type) {
    let option = getConfig(this)[type];
    return option && (option.disableComponents || (option.disableLeafComponents && !this._isSubscriber()));
  }
});