#

API Docs for: v2.11.1
Show: Inherited Protected Private Deprecated

# File: packages/ember-metal/lib/mixin.js

    'no use strict';
    // Remove "use strict"; from transpiled module until
    // https://bugs.webkit.org/show_bug.cgi?id=138038 is fixed

    /**
    @module ember
    @submodule ember-metal
    */
    import {
      assign,
      guidFor,
      GUID_KEY,
      NAME_KEY,
      wrap,
      makeArray
    } from 'ember-utils';
    import EmberError from './error';
    import {
      debugSeal,
      assert,
      deprecate,
      runInDebug
    } from './debug';
    import { meta as metaFor, peekMeta } from './meta';
    import expandProperties from './expand_properties';
    import {
      Descriptor,
      defineProperty
    } from './properties';
    import { ComputedProperty } from './computed';
    import { Binding } from './binding';
    import {
      addObserver,
      removeObserver,
      _addBeforeObserver,
      _removeBeforeObserver
    } from './observer';
    import {
      addListener,
      removeListener
    } from './events';

    function ROOT() {}
    ROOT.__hasSuper = false;

    const a_slice = [].slice;

    function isMethod(obj) {
      return 'function' === typeof obj &&
             obj.isMethod !== false &&
             obj !== Boolean &&
             obj !== Object &&
             obj !== Number &&
             obj !== Array &&
             obj !== Date &&
             obj !== String;
    }

    const CONTINUE = {};

    function mixinProperties(mixinsMeta, mixin) {
      let guid;

      if (mixin instanceof Mixin) {
        guid = guidFor(mixin);
        if (mixinsMeta.peekMixins(guid)) { return CONTINUE; }
        mixinsMeta.writeMixins(guid, mixin);
        return mixin.properties;
      } else {
        return mixin; // apply anonymous mixin properties
      }
    }

    function concatenatedMixinProperties(concatProp, props, values, base) {
      let concats;

      // reset before adding each new mixin to pickup concats from previous
      concats = values[concatProp] || base[concatProp];
      if (props[concatProp]) {
        concats = concats ? concats.concat(props[concatProp]) : props[concatProp];
      }

      return concats;
    }

    function giveDescriptorSuper(meta, key, property, values, descs, base) {
      let superProperty;

      // Computed properties override methods, and do not call super to them
      if (values[key] === undefined) {
        // Find the original descriptor in a parent mixin
        superProperty = descs[key];
      }

      // If we didn't find the original descriptor in a parent mixin, find
      // it on the original object.
      if (!superProperty) {
        let possibleDesc = base[key];
        let superDesc = (possibleDesc !== null && typeof possibleDesc === 'object' && possibleDesc.isDescriptor) ? possibleDesc : undefined;

        superProperty = superDesc;
      }

      if (superProperty === undefined || !(superProperty instanceof ComputedProperty)) {
        return property;
      }

      // Since multiple mixins may inherit from the same parent, we need
      // to clone the computed property so that other mixins do not receive
      // the wrapped version.
      property = Object.create(property);
      property._getter = wrap(property._getter, superProperty._getter);
      if (superProperty._setter) {
        if (property._setter) {
          property._setter = wrap(property._setter, superProperty._setter);
        } else {
          property._setter = superProperty._setter;
        }
      }

      return property;
    }

    function giveMethodSuper(obj, key, method, values, descs) {
      let superMethod;

      // Methods overwrite computed properties, and do not call super to them.
      if (descs[key] === undefined) {
        // Find the original method in a parent mixin
        superMethod = values[key];
      }

      // If we didn't find the original value in a parent mixin, find it in
      // the original object
      superMethod = superMethod || obj[key];

      // Only wrap the new method if the original method was a function
      if (superMethod === undefined || 'function' !== typeof superMethod) {
        return method;
      }

      return wrap(method, superMethod);
    }

    function applyConcatenatedProperties(obj, key, value, values) {
      let baseValue = values[key] || obj[key];
      let ret;

      if (baseValue) {
        if ('function' === typeof baseValue.concat) {
          if (value === null || value === undefined) {
            ret = baseValue;
          } else {
            ret = baseValue.concat(value);
          }
        } else {
          ret = makeArray(baseValue).concat(value);
        }
      } else {
        ret = makeArray(value);
      }

      runInDebug(() => {
        // it is possible to use concatenatedProperties with strings (which cannot be frozen)
        // only freeze objects...
        if (typeof ret === 'object' && ret !== null) {
          // prevent mutating `concatenatedProperties` array after it is applied
          Object.freeze(ret);
        }
      });

      return ret;
    }

    function applyMergedProperties(obj, key, value, values) {
      let baseValue = values[key] || obj[key];

      runInDebug(function() {
        if (Array.isArray(value)) { // use conditional to avoid stringifying every time
          assert(`You passed in \`${JSON.stringify(value)}\` as the value for \`${key}\` but \`${key}\` cannot be an Array`, false);
        }
      });

      if (!baseValue) { return value; }

      let newBase = assign({}, baseValue);
      let hasFunction = false;

      for (let prop in value) {
        if (!value.hasOwnProperty(prop)) { continue; }

        let propValue = value[prop];
        if (isMethod(propValue)) {
          // TODO: support for Computed Properties, etc?
          hasFunction = true;
          newBase[prop] = giveMethodSuper(obj, prop, propValue, baseValue, {});
        } else {
          newBase[prop] = propValue;
        }
      }

      if (hasFunction) {
        newBase._super = ROOT;
      }

      return newBase;
    }

    function addNormalizedProperty(base, key, value, meta, descs, values, concats, mergings) {
      if (value instanceof Descriptor) {
        if (value === REQUIRED && descs[key]) { return CONTINUE; }

        // Wrap descriptor function to implement
        // _super() if needed
        if (value._getter) {
          value = giveDescriptorSuper(meta, key, value, values, descs, base);
        }

        descs[key]  = value;
        values[key] = undefined;
      } else {
        if ((concats && concats.indexOf(key) >= 0) ||
                    key === 'concatenatedProperties' ||
                    key === 'mergedProperties') {
          value = applyConcatenatedProperties(base, key, value, values);
        } else if ((mergings && mergings.indexOf(key) >= 0)) {
          value = applyMergedProperties(base, key, value, values);
        } else if (isMethod(value)) {
          value = giveMethodSuper(base, key, value, values, descs);
        }

        descs[key] = undefined;
        values[key] = value;
      }
    }

    function mergeMixins(mixins, m, descs, values, base, keys) {
      let currentMixin, props, key, concats, mergings;

      function removeKeys(keyName) {
        delete descs[keyName];
        delete values[keyName];
      }

      for (let i = 0; i < mixins.length; i++) {
        currentMixin = mixins[i];
        assert(
          `Expected hash or Mixin instance, got ${Object.prototype.toString.call(currentMixin)}`,
          typeof currentMixin === 'object' && currentMixin !== null && Object.prototype.toString.call(currentMixin) !== '[object Array]'
        );

        props = mixinProperties(m, currentMixin);
        if (props === CONTINUE) { continue; }

        if (props) {
          if (base.willMergeMixin) { base.willMergeMixin(props); }
          concats = concatenatedMixinProperties('concatenatedProperties', props, values, base);
          mergings = concatenatedMixinProperties('mergedProperties', props, values, base);

          for (key in props) {
            if (!props.hasOwnProperty(key)) { continue; }
            keys.push(key);
            addNormalizedProperty(base, key, props[key], m, descs, values, concats, mergings);
          }

          // manually copy toString() because some JS engines do not enumerate it
          if (props.hasOwnProperty('toString')) { base.toString = props.toString; }
        } else if (currentMixin.mixins) {
          mergeMixins(currentMixin.mixins, m, descs, values, base, keys);
          if (currentMixin._without) { currentMixin._without.forEach(removeKeys); }
        }
      }
    }

    export function detectBinding(key) {
      var length = key.length;

      return length > 7 && key.charCodeAt(length - 7) === 66 && key.indexOf('inding', length - 6) !== -1;
    }
    // warm both paths of above function
    detectBinding('notbound');
    detectBinding('fooBinding');

    function connectBindings(obj, m) {
      // TODO Mixin.apply(instance) should disconnect binding if exists
      m.forEachBindings((key, binding) => {
        if (binding) {
          let to = key.slice(0, -7); // strip Binding off end
          if (binding instanceof Binding) {
            binding = binding.copy(); // copy prototypes' instance
            binding.to(to);
          } else { // binding is string path
            binding = new Binding(to, binding);
          }
          binding.connect(obj);
          obj[key] = binding;
        }
      });
      // mark as applied
      m.clearBindings();
    }

    function finishPartial(obj, m) {
      connectBindings(obj, m || metaFor(obj));
      return obj;
    }

    function followAlias(obj, desc, m, descs, values) {
      let altKey = desc.methodName;
      let value;
      let possibleDesc;
      if (descs[altKey] || values[altKey]) {
        value = values[altKey];
        desc  = descs[altKey];
      } else if ((possibleDesc = obj[altKey]) && possibleDesc !== null && typeof possibleDesc === 'object' && possibleDesc.isDescriptor) {
        desc  = possibleDesc;
        value = undefined;
      } else {
        desc = undefined;
        value = obj[altKey];
      }

      return { desc: desc, value: value };
    }

    function updateObserversAndListeners(obj, key, observerOrListener, pathsKey, updateMethod) {
      let paths = observerOrListener[pathsKey];

      if (paths) {
        for (let i = 0; i < paths.length; i++) {
          updateMethod(obj, paths[i], null, key);
        }
      }
    }

    function replaceObserversAndListeners(obj, key, observerOrListener) {
      let prev = obj[key];

      if ('function' === typeof prev) {
        updateObserversAndListeners(obj, key, prev, '__ember_observesBefore__', _removeBeforeObserver);
        updateObserversAndListeners(obj, key, prev, '__ember_observes__', removeObserver);
        updateObserversAndListeners(obj, key, prev, '__ember_listens__', removeListener);
      }

      if ('function' === typeof observerOrListener) {
        updateObserversAndListeners(obj, key, observerOrListener, '__ember_observesBefore__', _addBeforeObserver);
        updateObserversAndListeners(obj, key, observerOrListener, '__ember_observes__', addObserver);
        updateObserversAndListeners(obj, key, observerOrListener, '__ember_listens__', addListener);
      }
    }

    function applyMixin(obj, mixins, partial) {
      let descs = {};
      let values = {};
      let m = metaFor(obj);
      let keys = [];
      let key, value, desc;

      obj._super = ROOT;

      // Go through all mixins and hashes passed in, and:
      //
      // * Handle concatenated properties
      // * Handle merged properties
      // * Set up _super wrapping if necessary
      // * Set up computed property descriptors
      // * Copying `toString` in broken browsers
      mergeMixins(mixins, m, descs, values, obj, keys);

      for (let i = 0; i < keys.length; i++) {
        key = keys[i];
        if (key === 'constructor' || !values.hasOwnProperty(key)) { continue; }

        desc = descs[key];
        value = values[key];

        if (desc === REQUIRED) { continue; }

        while (desc && desc instanceof Alias) {
          let followed = followAlias(obj, desc, m, descs, values);
          desc = followed.desc;
          value = followed.value;
        }

        if (desc === undefined && value === undefined) { continue; }

        replaceObserversAndListeners(obj, key, value);

        if (detectBinding(key)) {
          m.writeBindings(key, value);
        }

        defineProperty(obj, key, desc, value, m);
      }

      if (!partial) { // don't apply to prototype
        finishPartial(obj, m);
      }

      return obj;
    }

    /**
      @method mixin
      @for Ember
      @param obj
      @param mixins*
      @return obj
      @private
    */
    export function mixin(obj, ...args) {
      applyMixin(obj, args, false);
      return obj;
    }

    /**
      The `Ember.Mixin` class allows you to create mixins, whose properties can be
      added to other classes. For instance,

      ```javascript
      const EditableMixin = Ember.Mixin.create({
        edit() {
          console.log('starting to edit');
          this.set('isEditing', true);
        },
        isEditing: false
      });

      // Mix mixins into classes by passing them as the first arguments to
      // `.extend.`
      const Comment = Ember.Object.extend(EditableMixin, {
        post: null
      });

      let comment = Comment.create(post: somePost);
      comment.edit(); // outputs 'starting to edit'
      ```

      Note that Mixins are created with `Ember.Mixin.create`, not
      `Ember.Mixin.extend`.

      Note that mixins extend a constructor's prototype so arrays and object literals
      defined as properties will be shared amongst objects that implement the mixin.
      If you want to define a property in a mixin that is not shared, you can define
      it either as a computed property or have it be created on initialization of the object.

      ```javascript
      // filters array will be shared amongst any object implementing mixin
      const FilterableMixin = Ember.Mixin.create({
        filters: Ember.A()
      });

      // filters will be a separate array for every object implementing the mixin
      const FilterableMixin = Ember.Mixin.create({
        filters: Ember.computed(function() {
          return Ember.A();
        })
      });

      // filters will be created as a separate array during the object's initialization
      const Filterable = Ember.Mixin.create({
        init() {
          this._super(...arguments);
          this.set("filters", Ember.A());
        }
      });
      ```

      @class Mixin
      @namespace Ember
      @public
    */
    export default function Mixin(args, properties) {
      this.properties = properties;

      let length = args && args.length;

      if (length > 0) {
        let m = new Array(length);

        for (let i = 0; i < length; i++) {
          let x = args[i];
          if (x instanceof Mixin) {
            m[i] = x;
          } else {
            m[i] = new Mixin(undefined, x);
          }
        }

        this.mixins = m;
      } else {
        this.mixins = undefined;
      }
      this.ownerConstructor = undefined;
      this._without = undefined;
      this[GUID_KEY] = null;
      this[NAME_KEY] = null;
      debugSeal(this);
    }

    Mixin._apply = applyMixin;

    Mixin.applyPartial = function(obj) {
      let args = a_slice.call(arguments, 1);
      return applyMixin(obj, args, true);
    };

    Mixin.finishPartial = finishPartial;

    let unprocessedFlag = false;

    export function hasUnprocessedMixins() {
      return unprocessedFlag;
    }

    export function clearUnprocessedMixins() {
      unprocessedFlag = false;
    }

    /**
      @method create
      @static
      @param arguments*
      @public
    */
    Mixin.create = function(...args) {
      // ES6TODO: this relies on a global state?
      unprocessedFlag = true;
      let M = this;
      return new M(args, undefined);
    };

    let MixinPrototype = Mixin.prototype;

    /**
      @method reopen
      @param arguments*
      @private
    */
    MixinPrototype.reopen = function() {
      let currentMixin;

      if (this.properties) {
        currentMixin = new Mixin(undefined, this.properties);
        this.properties = undefined;
        this.mixins = [currentMixin];
      } else if (!this.mixins) {
        this.mixins = [];
      }

      let mixins = this.mixins;
      let idx;

      for (idx = 0; idx < arguments.length; idx++) {
        currentMixin = arguments[idx];
        assert(
          `Expected hash or Mixin instance, got ${Object.prototype.toString.call(currentMixin)}`,
          typeof currentMixin === 'object' && currentMixin !== null &&
            Object.prototype.toString.call(currentMixin) !== '[object Array]'
        );

        if (currentMixin instanceof Mixin) {
          mixins.push(currentMixin);
        } else {
          mixins.push(new Mixin(undefined, currentMixin));
        }
      }

      return this;
    };

    /**
      @method apply
      @param obj
      @return applied object
      @private
    */
    MixinPrototype.apply = function(obj) {
      return applyMixin(obj, [this], false);
    };

    MixinPrototype.applyPartial = function(obj) {
      return applyMixin(obj, [this], true);
    };

    MixinPrototype.toString = Object.toString;

    function _detect(curMixin, targetMixin, seen) {
      let guid = guidFor(curMixin);

      if (seen[guid]) { return false; }
      seen[guid] = true;

      if (curMixin === targetMixin) { return true; }
      let mixins = curMixin.mixins;
      let loc = mixins ? mixins.length : 0;
      while (--loc >= 0) {
        if (_detect(mixins[loc], targetMixin, seen)) { return true; }
      }
      return false;
    }

    /**
      @method detect
      @param obj
      @return {Boolean}
      @private
    */
    MixinPrototype.detect = function(obj) {
      if (typeof obj !== 'object' || obj === null) { return false; }
      if (obj instanceof Mixin) { return _detect(obj, this, {}); }
      let m = peekMeta(obj);
      if (!m) { return false; }
      return !!m.peekMixins(guidFor(this));
    };

    MixinPrototype.without = function(...args) {
      let ret = new Mixin([this]);
      ret._without = args;
      return ret;
    };

    function _keys(ret, mixin, seen) {
      if (seen[guidFor(mixin)]) { return; }
      seen[guidFor(mixin)] = true;

      if (mixin.properties) {
        let props = Object.keys(mixin.properties);
        for (let i = 0; i < props.length; i++) {
          let key = props[i];
          ret[key] = true;
        }
      } else if (mixin.mixins) {
        mixin.mixins.forEach((x) => _keys(ret, x, seen));
      }
    }

    MixinPrototype.keys = function() {
      let keys = {};
      let seen = {};

      _keys(keys, this, seen);
      let ret = Object.keys(keys);
      return ret;
    };

    debugSeal(MixinPrototype);

    // returns the mixins currently applied to the specified object
    // TODO: Make Ember.mixin
    Mixin.mixins = function(obj) {
      let m = peekMeta(obj);
      let ret = [];
      if (!m) { return ret; }

      m.forEachMixins((key, currentMixin) => {
        // skip primitive mixins since these are always anonymous
        if (!currentMixin.properties) { ret.push(currentMixin); }
      });

      return ret;
    };

    const REQUIRED = new Descriptor();
    REQUIRED.toString = function() { return '(Required Property)'; };

    /**
      Denotes a required property for a mixin

      @method required
      @for Ember
      @private
    */
    export function required() {
      deprecate(
        'Ember.required is deprecated as its behavior is inconsistent and unreliable.',
        false,
        { id: 'ember-metal.required', until: '3.0.0' }
      );
      return REQUIRED;
    }

    function Alias(methodName) {
      this.isDescriptor = true;
      this.methodName = methodName;
    }

    Alias.prototype = new Descriptor();

    /**
      Makes a method available via an additional name.

      ```javascript
      App.Person = Ember.Object.extend({
        name: function() {
          return 'Tomhuda Katzdale';
        },
        moniker: Ember.aliasMethod('name')
      });

      let goodGuy = App.Person.create();

      goodGuy.name();    // 'Tomhuda Katzdale'
      goodGuy.moniker(); // 'Tomhuda Katzdale'
      ```

      @method aliasMethod
      @for Ember
      @param {String} methodName name of the method to alias
      @public
    */
    export function aliasMethod(methodName) {
      return new Alias(methodName);
    }

    // ..........................................................
    // OBSERVER HELPER
    //

    /**
      Specify a method that observes property changes.

      ```javascript
      Ember.Object.extend({
        valueObserver: Ember.observer('value', function() {
          // Executes whenever the "value" property changes
        })
      });
      ```

      Also available as `Function.prototype.observes` if prototype extensions are
      enabled.

      @method observer
      @for Ember
      @param {String} propertyNames*
      @param {Function} func
      @return func
      @public
    */
    export function observer(...args) {
      let func  = args.slice(-1)[0];
      let paths;

      let addWatchedProperty = function(path) {
        paths.push(path);
      };
      let _paths = args.slice(0, -1);

      if (typeof func !== 'function') {
        // revert to old, soft-deprecated argument ordering
        deprecate('Passing the dependentKeys after the callback function in Ember.observer is deprecated. Ensure the callback function is the last argument.', false, { id: 'ember-metal.observer-argument-order', until: '3.0.0' });

        func  = args[0];
        _paths = args.slice(1);
      }

      paths = [];

      for (let i = 0; i < _paths.length; ++i) {
        expandProperties(_paths[i], addWatchedProperty);
      }

      if (typeof func !== 'function') {
        throw new EmberError('Ember.observer called without a function');
      }

      func.__ember_observes__ = paths;
      return func;
    }

    /**
      Specify a method that observes property changes.

      ```javascript
      Ember.Object.extend({
        valueObserver: Ember.immediateObserver('value', function() {
          // Executes whenever the "value" property changes
        })
      });
      ```

      In the future, `Ember.observer` may become asynchronous. In this event,
      `Ember.immediateObserver` will maintain the synchronous behavior.

      Also available as `Function.prototype.observesImmediately` if prototype extensions are
      enabled.

      @method _immediateObserver
      @for Ember
      @param {String} propertyNames*
      @param {Function} func
      @deprecated Use `Ember.observer` instead.
      @return func
      @private
    */
    export function _immediateObserver() {
      deprecate('Usage of `Ember.immediateObserver` is deprecated, use `Ember.observer` instead.', false, { id: 'ember-metal.immediate-observer', until: '3.0.0' });

      for (let i = 0; i < arguments.length; i++) {
        let arg = arguments[i];
        assert(
          'Immediate observers must observe internal properties only, not properties on other objects.',
          typeof arg !== 'string' || arg.indexOf('.') === -1
        );
      }

      return observer.apply(this, arguments);
    }

    /**
      When observers fire, they are called with the arguments `obj`, `keyName`.

      Note, `@each.property` observer is called per each add or replace of an element
      and it's not called with a specific enumeration item.

      A `_beforeObserver` fires before a property changes.

      @method beforeObserver
      @for Ember
      @param {String} propertyNames*
      @param {Function} func
      @return func
      @deprecated
      @private
    */
    export function _beforeObserver(...args) {
      let func  = args.slice(-1)[0];
      let paths;

      let addWatchedProperty = function(path) { paths.push(path); };

      let _paths = args.slice(0, -1);

      if (typeof func !== 'function') {
        // revert to old, soft-deprecated argument ordering

        func  = args[0];
        _paths = args.slice(1);
      }

      paths = [];

      for (let i = 0; i < _paths.length; ++i) {
        expandProperties(_paths[i], addWatchedProperty);
      }

      if (typeof func !== 'function') {
        throw new EmberError('_beforeObserver called without a function');
      }

      func.__ember_observesBefore__ = paths;
      return func;
    }

    export {
      Mixin,
      required,
      REQUIRED
    };