/** @module @ember/object */ import { assert } from '@ember/debug'; import { BINDING_SUPPORT } from '@ember/deprecated-features'; import { assign } from '@ember/polyfills'; import { DEBUG } from '@glimmer/env'; import { ENV } from 'ember-environment'; import { descriptorFor, meta as metaFor, peekMeta } from 'ember-meta'; import { getListeners, getObservers, guidFor, makeArray, NAME_KEY, ROOT, setObservers, wrap, } from 'ember-utils'; import { ComputedProperty } from './computed'; import { addListener, removeListener } from './events'; import expandProperties from './expand_properties'; import { classToString, setUnprocessedMixins } from './namespace_search'; import { addObserver, removeObserver } from './observer'; import { defineProperty, Descriptor } from './properties'; const a_concat = Array.prototype.concat; const { isArray } = Array; 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) { if (mixin instanceof Mixin) { if (mixinsMeta.hasMixin(mixin)) { return CONTINUE; } mixinsMeta.addMixin(mixin); return mixin.properties; } else { return mixin; // apply anonymous mixin properties } } function concatenatedMixinProperties(concatProp, props, values, base) { // reset before adding each new mixin to pickup concats from previous let concats = values[concatProp] || base[concatProp]; if (props[concatProp]) { concats = concats ? a_concat.call(concats, 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) { superProperty = descriptorFor(base, key, meta); } 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) { // Methods overwrite computed properties, and do not call super to them. if (descs[key] !== undefined) { return method; } // Find the original method in a parent mixin let superMethod = values[key]; // If we didn't find the original value in a parent mixin, find it in // the original object if (superMethod === undefined && descriptorFor(obj, key) === undefined) { superMethod = obj[key]; } // Only wrap the new method if the original method was a function if (typeof superMethod === 'function') { return wrap(method, superMethod); } return method; } function applyConcatenatedProperties(obj, key, value, values) { let baseValue = values[key] || obj[key]; let ret = makeArray(baseValue).concat(makeArray(value)); if (DEBUG) { // 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]; assert(`You passed in \`${JSON.stringify(value)}\` as the value for \`${key}\` but \`${key}\` cannot be an Array`, !isArray(value)); 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) { // 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) > -1) { 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, meta, 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(meta, currentMixin); if (props === CONTINUE) { continue; } if (props) { // remove willMergeMixin after 3.4 as it was used for _actions 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], meta, 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, meta, descs, values, base, keys); if (currentMixin._without) { currentMixin._without.forEach(removeKeys); } } } } function followAlias(obj, desc, descs, values) { let altKey = desc.methodName; let value; let possibleDesc; if (descs[altKey] || values[altKey]) { value = values[altKey]; desc = descs[altKey]; } else if ((possibleDesc = descriptorFor(obj, altKey)) !== undefined) { desc = possibleDesc; value = undefined; } else { desc = undefined; value = obj[altKey]; } return { desc, value }; } function updateObserversAndListeners(obj, key, paths, updateMethod) { if (paths) { for (let i = 0; i < paths.length; i++) { updateMethod(obj, paths[i], null, key); } } } function replaceObserversAndListeners(obj, key, prev, next) { if (typeof prev === 'function') { updateObserversAndListeners(obj, key, getObservers(prev), removeObserver); updateObserversAndListeners(obj, key, getListeners(prev), removeListener); } if (typeof next === 'function') { updateObserversAndListeners(obj, key, getObservers(next), addObserver); updateObserversAndListeners(obj, key, getListeners(next), addListener); } } export function applyMixin(obj, mixins, partial) { let descs = {}; let values = {}; let meta = 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, meta, 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]; while (desc && desc instanceof Alias) { let followed = followAlias(obj, desc, descs, values); desc = followed.desc; value = followed.value; } if (desc === undefined && value === undefined) { continue; } if (descriptorFor(obj, key) !== undefined) { replaceObserversAndListeners(obj, key, null, value); } else { replaceObserversAndListeners(obj, key, obj[key], value); } if (BINDING_SUPPORT && ENV._ENABLE_BINDING_SUPPORT && typeof Mixin.detectBinding === 'function' && Mixin.detectBinding(key)) { meta.writeBindings(key, value); } defineProperty(obj, key, desc, value, meta); } if (BINDING_SUPPORT && ENV._ENABLE_BINDING_SUPPORT && !partial && typeof Mixin.finishPartial === 'function') { Mixin.finishPartial(obj, meta); } return obj; } /** @method mixin @param obj @param mixins* @return obj @private */ export function mixin(obj, ...args) { applyMixin(obj, args, false); return obj; } /** The `Mixin` class allows you to create mixins, whose properties can be added to other classes. For instance, ```javascript import Mixin from '@ember/object/mixin'; const EditableMixin = Mixin.create({ edit() { console.log('starting to edit'); this.set('isEditing', true); }, isEditing: false }); ``` ```javascript import EmberObject from '@ember/object'; import EditableMixin from '../mixins/editable'; // Mix mixins into classes by passing them as the first arguments to // `.extend.` const Comment = EmberObject.extend(EditableMixin, { post: null }); let comment = Comment.create({ post: somePost }); comment.edit(); // outputs 'starting to edit' ``` Note that Mixins are created with `Mixin.create`, not `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 import Mixin from '@ember/object/mixin'; import { A } from '@ember/array'; const FilterableMixin = Mixin.create({ filters: A() }); ``` ```javascript import Mixin from '@ember/object/mixin'; import { A } from '@ember/array'; import { computed } from '@ember/object'; // filters will be a separate array for every object implementing the mixin const FilterableMixin = Mixin.create({ filters: computed(function() { return A(); }) }); ``` ```javascript import Mixin from '@ember/object/mixin'; import { A } from '@ember/array'; // filters will be created as a separate array during the object's initialization const Filterable = Mixin.create({ filters: null, init() { this._super(...arguments); this.set("filters", A()); } }); ``` @class Mixin @public */ export default class Mixin { constructor(mixins, properties) { this.properties = properties; this.mixins = buildMixinsArray(mixins); this.ownerConstructor = undefined; this._without = undefined; if (DEBUG) { this[NAME_KEY] = undefined; /* In debug builds, we seal mixins to help avoid performance pitfalls. In IE11 there is a quirk that prevents sealed objects from being added to a WeakMap. Unfortunately, the mixin system currently relies on weak maps in `guidFor`, so we need to prime the guid cache weak map. */ guidFor(this); Object.seal(this); } } /** @method create @for @ember/object/mixin @static @param arguments* @public */ static create(...args) { // ES6TODO: this relies on a global state? setUnprocessedMixins(); let M = this; return new M(args, undefined); } // returns the mixins currently applied to the specified object // TODO: Make `mixin` static mixins(obj) { let meta = peekMeta(obj); let ret = []; if (meta === undefined) { return ret; } meta.forEachMixins((currentMixin) => { // skip primitive mixins since these are always anonymous if (!currentMixin.properties) { ret.push(currentMixin); } }); return ret; } /** @method reopen @param arguments* @private */ reopen(...args) { if (args.length === 0) { return; } if (this.properties) { let currentMixin = new Mixin(undefined, this.properties); this.properties = undefined; this.mixins = [currentMixin]; } else if (!this.mixins) { this.mixins = []; } this.mixins = this.mixins.concat(buildMixinsArray(args)); return this; } /** @method apply @param obj @return applied object @private */ apply(obj) { return applyMixin(obj, [this], false); } applyPartial(obj) { return applyMixin(obj, [this], true); } /** @method detect @param obj @return {Boolean} @private */ detect(obj) { if (typeof obj !== 'object' || obj === null) { return false; } if (obj instanceof Mixin) { return _detect(obj, this); } let meta = peekMeta(obj); if (meta === undefined) { return false; } return meta.hasMixin(this); } without(...args) { let ret = new Mixin([this]); ret._without = args; return ret; } keys() { return _keys(this); } toString() { return '(unknown mixin)'; } } function buildMixinsArray(mixins) { let length = (mixins && mixins.length) || 0; let m = undefined; if (length > 0) { m = new Array(length); for (let i = 0; i < length; i++) { let x = mixins[i]; assert(`Expected hash or Mixin instance, got ${Object.prototype.toString.call(x)}`, typeof x === 'object' && x !== null && Object.prototype.toString.call(x) !== '[object Array]'); if (x instanceof Mixin) { m[i] = x; } else { m[i] = new Mixin(undefined, x); } } } return m; } if (BINDING_SUPPORT && ENV._ENABLE_BINDING_SUPPORT) { // slotting this so that the legacy addon can add the function here // without triggering an error due to the Object.seal done below Mixin.finishPartial = null; Mixin.detectBinding = null; } Mixin.prototype.toString = classToString; if (DEBUG) { Mixin.prototype[NAME_KEY] = undefined; Object.seal(Mixin.prototype); } function _detect(curMixin, targetMixin, seen = new Set()) { if (seen.has(curMixin)) { return false; } seen.add(curMixin); if (curMixin === targetMixin) { return true; } let mixins = curMixin.mixins; if (mixins) { return mixins.some(mixin => _detect(mixin, targetMixin, seen)); } return false; } function _keys(mixin, ret = new Set(), seen = new Set()) { if (seen.has(mixin)) { return; } seen.add(mixin); if (mixin.properties) { let props = Object.keys(mixin.properties); for (let i = 0; i < props.length; i++) { ret.add(props[i]); } } else if (mixin.mixins) { mixin.mixins.forEach((x) => _keys(x, ret, seen)); } return ret; } class Alias extends Descriptor { constructor(methodName) { super(); this.methodName = methodName; } teardown(_obj, _keyName, _meta) { throw new Error('Method not implemented.'); } get(_obj, _keyName) { throw new Error('Method not implemented.'); } set(_obj, _keyName, _value) { throw new Error('Method not implemented.'); } } /** Makes a method available via an additional name. ```app/utils/person.js import EmberObject, { aliasMethod } from '@ember/object'; export default EmberObject.extend({ name() { return 'Tomhuda Katzdale'; }, moniker: aliasMethod('name') }); ``` ```javascript let goodGuy = Person.create(); goodGuy.name(); // 'Tomhuda Katzdale' goodGuy.moniker(); // 'Tomhuda Katzdale' ``` @method aliasMethod @static @for @ember/object @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 import EmberObject from '@ember/object'; import { observer } from '@ember/object'; export default EmberObject.extend({ valueObserver: observer('value', function() { // Executes whenever the "value" property changes }) }); ``` Also available as `Function.prototype.observes` if prototype extensions are enabled. @method observer @for @ember/object @param {String} propertyNames* @param {Function} func @return func @public @static */ export function observer(...args) { let func = args.pop(); let _paths = args; assert('observer called without a function', typeof func === 'function'); assert('observer called without valid path', _paths.length > 0 && _paths.every(p => typeof p === 'string' && !!p.length)); let paths = []; let addWatchedProperty = (path) => paths.push(path); for (let i = 0; i < _paths.length; ++i) { expandProperties(_paths[i], addWatchedProperty); } setObservers(func, paths); return func; } export { Mixin };