APIs

Show:
/**
  @module ember-flexberry
*/

import $ from 'jquery';
import Mixin from '@ember/object/mixin';
import { get, computed, observer } from '@ember/object';
import { A, isArray } from '@ember/array';
import { assert } from '@ember/debug';
import { typeOf, isNone } from '@ember/utils';
import { deprecate } from '@ember/application/deprecations';

/**
  Mixin containing logic making available passing all desirable properties to components
  in a single object, which keys are related to component's desirable properties.

  @class DynamicPropertiesMixin
  @uses <a href="https://www.emberjs.com/api/ember/release/classes/Mixin">Mixin</a>
*/
export default Mixin.create({
  /**
    Flag: indicates whether component is tagless or not
    (has empty [tagName](https://emberjs.com/api/ember/release/classes/Component#property_tagName) or not).

    @property isTagless
    @type Boolean
    @readOnly
   */
  isTagless: computed('tagName', function () {
    let tagName = this.get('tagName');
    if (typeOf(tagName) === 'string') {
      tagName = $.trim(tagName);
    }

    return tagName === '';
  }),

  /**
    Object containing dynamic properties that must be assigned to
    component using {{#crossLink "DynamicPropertiesMixin"}}dynamic-properties mixin{{/crossLink}}.

    @property dynamicProperties
    @type Object
    @default null
  */
  dynamicProperties: null,

  /**
    Flag: indicates whether component's wrapper DOM-element is available for select now.

    @property _componentWrapperIsAvailable
    @type Boolean
    @private
  */
  _componentWrapperIsAvailable: false,

  /**
    Array with objects containing names of already assigned
    {{#crossLink "DynamicPropertiesMixin:dynamicProperties:property"}}dynamic properties{{/crossLink}}
    and observer handlers related to them.
    Each object in array has following structure: { propertyName: '...', propertyObserverHandler: function() { ... } }.

    @property _dynamicPropertiesMetadata
    @type Object[]
    @default null
    @private
   */
  _dynamicPropertiesMetadata: null,

  /**
    Adds component's new dynamic property.
    Gets related value from {{#crossLink "DynamicPropertiesMixin:dynamicProperties:property"}}dynamic properties{{/crossLink}},
    and assignes it to related component's property, then attaches observer.

    @method _addDynamicProperty
    @param {String} propertyName Name of dynamic property that must be added.
    @private
  */
  _addDynamicProperty(propertyName) {
    let dynamicProperties = this.get('dynamicProperties');
    if (isNone(dynamicProperties)) {
      return;
    }

    let previousCustomClassNames = [];
    let setDynamicClassProperty = (propertyValue) => {
      assert(
          `Wrong type of \`class\` property: ` +
          `actual type is \`${typeOf(propertyValue)}\`, but \`string\` is expected.`,
          typeOf(propertyValue) === 'string');

      let customClassNames = A(propertyValue.split(' ')).map((customClassName) => {
        return $.trim(customClassName);
      });

      let classNames = this.get('classNames');
      let $component = this.get('_componentWrapperIsAvailable') ? this.$() : null;

      if (!isArray(classNames)) {
        classNames = [];
      }

      if ($component) {
        // Remove previously added custom class names.
        A(previousCustomClassNames).forEach((previousCustomClassName) => {
          let index = classNames.indexOf(previousCustomClassName);

          if (index >= 0) {
            // For some reason changes to classNames will not cause automatic rerender,
            // so there is no other way to remove class names manually through jQuery methods.
            $component.removeClass(previousCustomClassName);
          }
        });

        // Add new custom class names.
        A(customClassNames).forEach((customClassName) => {
          // For some reason changes to classNames will not cause automatic rerender,
          // so there is no other way to add class names manually through jQuery methods.
          $component.addClass(customClassName);
        });
      }

      classNames = classNames.concat(customClassNames.filter(c => classNames.indexOf(c) < 0));

      this.set('classNames', classNames);

      // Remember added custom class names in the context of property observer handler.
      previousCustomClassNames = customClassNames;
    };

    let setDynamicProperty = () => {
      let propertyValue = this.get(`dynamicProperties.${propertyName}`);
      if (!this.get('isTagless') && propertyName === 'class') {
        setDynamicClassProperty(propertyValue);
      } else {
        this.set(propertyName, propertyValue);
      }
    };

    if (propertyName === 'computedProperties') {
      deprecate(`Just don't use it.`, true, { id: 'dynamic-properties-mixin.computed-properties' });
      let propertyValue = this.get(`dynamicProperties.computedProperties`);
      let thisController = propertyValue.thisController;
      thisController.set('computedProperties', this);
      return;
    }

    setDynamicProperty();
    this.addObserver(`dynamicProperties.${propertyName}`, setDynamicProperty);

    let dynamicPropertiesMetadata = this.get('_dynamicPropertiesMetadata');
    dynamicPropertiesMetadata.pushObject({
      propertyName: propertyName,
      propertyObserverHandler: setDynamicProperty
    });
  },

  /**
    Removes component's previously added dynamic property.
    Removes related component's property & observer.

    @method _removeDynamicProperty
    @param {String} propertyName Name of dynamic property that must be removed.
    @private
  */
  _removeDynamicProperty(propertyName) {
    let dynamicPropertiesMetadata = this.get('_dynamicPropertiesMetadata');
    let dynamicPropertyMetadata = dynamicPropertiesMetadata.filter((metadata) => {
      return metadata.propertyName === propertyName;
    })[0];

    if (isNone(dynamicPropertyMetadata)) {
      return;
    }

    // Set undefined in ember-way to notify component's related observers & computed properties.
    this.set(propertyName, undefined);

    // Delete property.
    delete this[propertyName];

    // Remove observer.
    this.removeObserver(`dynamicProperties.${propertyName}`, get(dynamicPropertyMetadata, 'propertyObserverHandler'));

    // Remove metadata.
    dynamicPropertiesMetadata.removeObject(dynamicPropertyMetadata);
  },

  /**
    Removes component's all previously added dynamic properties.
    Removes related component's properties & observers.

    @method _removeDynamicProperties
    @private
  */
  _removeDynamicProperties() {
    let dynamicPropertiesMetadata = this.get('_dynamicPropertiesMetadata');
    var len = get(dynamicPropertiesMetadata, 'length');
    while (--len >= 0) {
      let dynamicPropertyMetadata = dynamicPropertiesMetadata[len];
      this._removeDynamicProperty(get(dynamicPropertyMetadata, 'propertyName'));
    }
  },

  /**
    Observes & handles any changes in
    {{#crossLink "DynamicPropertiesMixin/dynamicProperties:property"}}'dynamicProperties'{{/crossLink}},
    assigns actual values to a component's related properties (including initialization moment).

    @method _dynamicPropertiesDidChange
    @private
  */
  _dynamicPropertiesDidChange: observer('dynamicProperties', function () {
    let dynamicProperties = this.get('dynamicProperties');
    assert(
      `Wrong type of \`dynamicProperties\` property: ` +
      `actual type is \`${typeOf(dynamicProperties)}\`, but \`object\` or \`instance\` is expected.`,
      isNone(dynamicProperties) ||
      typeOf(dynamicProperties) === 'object' ||
      typeOf(dynamicProperties) === 'instance');

    let dynamicPropertiesMetadata = this.get('_dynamicPropertiesMetadata');
    if (isNone(dynamicPropertiesMetadata)) {
      dynamicPropertiesMetadata = A();
      this.set('_dynamicPropertiesMetadata', dynamicPropertiesMetadata);
    }

    // Clean up results of previous assignments.
    this._removeDynamicProperties();

    // Break after clean up, if new dynamic properties are none.
    if (isNone(dynamicProperties)) {
      return;
    }

    // Perform new assignments if new dynamic properties are defined.
    let dynamicPropertiesNames = Object.keys(dynamicProperties);
    for (let i = 0, len = dynamicPropertiesNames.length; i < len; i++) {
      this._addDynamicProperty(dynamicPropertiesNames[i]);
    }
  }),

  init() {
    this._super(...arguments);
    this._dynamicPropertiesDidChange();
  },

  /**
    Executes component's DOM-related logic.
  */
  didInsertElement() {
    this._super(...arguments);

    // We must know this to handle changes in component's 'class' property.
    this.set('_componentWrapperIsAvailable', true);
  },

  /**
    Executes component's DOM-related clean up logic.
  */
  willDestroyElement() {
    this._super(...arguments);

    // We must know this to handle changes in component's 'class' property.
    this.set('_componentWrapperIsAvailable', false);
  },

  /**
    Handles component's destroy.
    Removes component's all previously added dynamic properties.
  */
  willDestroy() {
    this._super(...arguments);

    // This call is needed to remove dynamically added observers.
    this._removeDynamicProperties();
  }
});