API Docs for: v2.11.1
Show:

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
};