UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

1,446 lines (1,297 loc) 67.8 kB
import { peekMeta, meta } from '../@ember/-internals/meta/lib/meta.js'; import { f as setupMandatorySetter, e as isObject, d as setListeners, h as setWithMandatorySetter } from './mandatory-setter-BiXq-dpN.js'; import { isDevelopingApp } from '@embroider/macros'; import { warn, debug } from '../@ember/debug/index.js'; import { isDestroyed, registerDestructor } from '../@glimmer/destroyable/index.js'; import { tagFor, CONSTANT_TAG, dirtyTagFor, updateTag as UPDATE_TAG, tagMetaFor, combine, validateTag, createUpdatableTag, valueForTag, CURRENT_TAG, untrack, ALLOW_CYCLES, consumeTag, track, isTracking, trackedData } from '../@glimmer/validator/index.js'; import { getCustomTagFor } from '../@glimmer/manager/index.js'; import { E as ENV } from './env-mInZ1DuF.js'; import { assert } from '../@ember/debug/lib/assert.js'; import { t as toString, s as symbol } from './to-string-B1BmwUkt.js'; import { s as setProxy } from './is_proxy-DjvCKvd5.js'; import { isEmberArray } from '../@ember/array/-internals.js'; import { C as Cache } from './cache-qDyqAcpg.js'; import Version from '../ember/version.js'; import { getOwner } from '../@ember/-internals/owner/index.js'; import inspect from '../@ember/debug/lib/inspect.js'; function objectAt(array, index) { if (Array.isArray(array)) { return array[index]; } else { return array.objectAt(index); } } // This is exported for `@tracked`, but should otherwise be avoided. Use `tagForObject`. const SELF_TAG = symbol('SELF_TAG'); function tagForProperty(obj, propertyKey, addMandatorySetter = false, meta) { let customTagFor = getCustomTagFor(obj); if (customTagFor !== undefined) { return customTagFor(obj, propertyKey, addMandatorySetter); } let tag = tagFor(obj, propertyKey, meta); if (isDevelopingApp() && addMandatorySetter) { setupMandatorySetter(tag, obj, propertyKey); } return tag; } function tagForObject(obj) { if (isObject(obj)) { if (isDevelopingApp()) { (isDevelopingApp() && !(!isDestroyed(obj)) && assert(isDestroyed(obj) ? `Cannot create a new tag for \`${toString(obj)}\` after it has been destroyed.` : '', !isDestroyed(obj))); } return tagFor(obj, SELF_TAG); } return CONSTANT_TAG; } function markObjectAsDirty(obj, propertyKey) { dirtyTagFor(obj, propertyKey); dirtyTagFor(obj, SELF_TAG); } const CHAIN_PASS_THROUGH = new WeakSet(); function finishLazyChains(meta, key, value) { let lazyTags = meta.readableLazyChainsFor(key); if (lazyTags === undefined) { return; } if (isObject(value)) { for (let [tag, deps] of lazyTags) { UPDATE_TAG(tag, getChainTagsForKey(value, deps, tagMetaFor(value), peekMeta(value))); } } lazyTags.length = 0; } function getChainTagsForKeys(obj, keys, tagMeta, meta) { let tags = []; for (let key of keys) { getChainTags(tags, obj, key, tagMeta, meta); } return combine(tags); } function getChainTagsForKey(obj, key, tagMeta, meta) { return combine(getChainTags([], obj, key, tagMeta, meta)); } function getChainTags(chainTags, obj, path, tagMeta, meta$1) { let current = obj; let currentTagMeta = tagMeta; let currentMeta = meta$1; let pathLength = path.length; let segmentEnd = -1; // prevent closures let segment, descriptor; // eslint-disable-next-line no-constant-condition while (true) { let lastSegmentEnd = segmentEnd + 1; segmentEnd = path.indexOf('.', lastSegmentEnd); if (segmentEnd === -1) { segmentEnd = pathLength; } segment = path.slice(lastSegmentEnd, segmentEnd); // If the segment is an @each, we can process it and then break if (segment === '@each' && segmentEnd !== pathLength) { lastSegmentEnd = segmentEnd + 1; segmentEnd = path.indexOf('.', lastSegmentEnd); let arrLength = current.length; if (typeof arrLength !== 'number' || // TODO: should the second test be `isEmberArray` instead? !(Array.isArray(current) || 'objectAt' in current)) { // If the current object isn't an array, there's nothing else to do, // we don't watch individual properties. Break out of the loop. break; } else if (arrLength === 0) { // Fast path for empty arrays chainTags.push(tagForProperty(current, '[]')); break; } if (segmentEnd === -1) { segment = path.slice(lastSegmentEnd); } else { // Deprecated, remove once we turn the deprecation into an assertion segment = path.slice(lastSegmentEnd, segmentEnd); } // Push the tags for each item's property for (let i = 0; i < arrLength; i++) { let item = objectAt(current, i); if (item) { (isDevelopingApp() && !(typeof item === 'object') && assert(`When using @each to observe the array \`${current.toString()}\`, the items in the array must be objects`, typeof item === 'object')); chainTags.push(tagForProperty(item, segment, true)); currentMeta = peekMeta(item); descriptor = currentMeta !== null ? currentMeta.peekDescriptors(segment) : undefined; // If the key is an alias, we need to bootstrap it if (descriptor !== undefined && typeof descriptor.altKey === 'string') { item[segment]; } } } // Push the tag for the array length itself chainTags.push(tagForProperty(current, '[]', true, currentTagMeta)); break; } let propertyTag = tagForProperty(current, segment, true, currentTagMeta); descriptor = currentMeta !== null ? currentMeta.peekDescriptors(segment) : undefined; chainTags.push(propertyTag); // If we're at the end of the path, processing the last segment, and it's // not an alias, we should _not_ get the last value, since we already have // its tag. There's no reason to access it and do more work. if (segmentEnd === pathLength) { // If the key was an alias, we should always get the next value in order to // bootstrap the alias. This is because aliases, unlike other CPs, should // always be in sync with the aliased value. if (CHAIN_PASS_THROUGH.has(descriptor)) { current[segment]; } break; } if (descriptor === undefined) { // If the descriptor is undefined, then its a normal property, so we should // lookup the value to chain off of like normal. if (!(segment in current) && typeof current.unknownProperty === 'function') { current = current.unknownProperty(segment); } else { current = current[segment]; } } else if (CHAIN_PASS_THROUGH.has(descriptor)) { current = current[segment]; } else { // If the descriptor is defined, then its a normal CP (not an alias, which // would have been handled earlier). We get the last revision to check if // the CP is still valid, and if so we use the cached value. If not, then // we create a lazy chain lookup, and the next time the CP is calculated, // it will update that lazy chain. let instanceMeta = currentMeta.source === current ? currentMeta : meta(current); let lastRevision = instanceMeta.revisionFor(segment); if (lastRevision !== undefined && validateTag(propertyTag, lastRevision)) { current = instanceMeta.valueFor(segment); } else { // use metaFor here to ensure we have the meta for the instance let lazyChains = instanceMeta.writableLazyChainsFor(segment); let rest = path.substring(segmentEnd + 1); let placeholderTag = createUpdatableTag(); lazyChains.push([placeholderTag, rest]); chainTags.push(placeholderTag); break; } } if (!isObject(current)) { // we've hit the end of the chain for now, break out break; } currentTagMeta = tagMetaFor(current); currentMeta = peekMeta(current); } return chainTags; } function isElementDescriptor(args) { let [maybeTarget, maybeKey, maybeDesc] = args; return ( // Ensure we have the right number of args args.length === 3 && ( // Make sure the target is a class or object (prototype) typeof maybeTarget === 'function' || typeof maybeTarget === 'object' && maybeTarget !== null) && // Make sure the key is a string typeof maybeKey === 'string' && ( // Make sure the descriptor is the right shape typeof maybeDesc === 'object' && maybeDesc !== null || maybeDesc === undefined) ); } function nativeDescDecorator(propertyDesc) { let decorator = function () { return propertyDesc; }; setClassicDecorator(decorator); return decorator; } /** Objects of this type can implement an interface to respond to requests to get and set. The default implementation handles simple properties. @class Descriptor @private */ class ComputedDescriptor { enumerable = true; configurable = true; _dependentKeys = undefined; _meta = undefined; setup(_obj, keyName, _propertyDesc, meta) { meta.writeDescriptors(keyName, this); } teardown(_obj, keyName, meta) { meta.removeDescriptors(keyName); } } let COMPUTED_GETTERS; if (isDevelopingApp()) { COMPUTED_GETTERS = new WeakSet(); } function DESCRIPTOR_GETTER_FUNCTION(name, descriptor) { function getter() { return descriptor.get(this, name); } if (isDevelopingApp()) { COMPUTED_GETTERS.add(getter); } return getter; } function DESCRIPTOR_SETTER_FUNCTION(name, descriptor) { let set = function CPSETTER_FUNCTION(value) { return descriptor.set(this, name, value); }; COMPUTED_SETTERS.add(set); return set; } const COMPUTED_SETTERS = new WeakSet(); function makeComputedDecorator(desc, DecoratorClass) { let decorator = function COMPUTED_DECORATOR(target, key, propertyDesc, maybeMeta, isClassicDecorator) { (isDevelopingApp() && !(isClassicDecorator || !propertyDesc || !propertyDesc.get || !COMPUTED_GETTERS.has(propertyDesc.get)) && assert(`Only one computed property decorator can be applied to a class field or accessor, but '${key}' was decorated twice. You may have added the decorator to both a getter and setter, which is unnecessary.`, isClassicDecorator || !propertyDesc || !propertyDesc.get || !COMPUTED_GETTERS.has(propertyDesc.get))); let meta$1 = arguments.length === 3 ? meta(target) : maybeMeta; desc.setup(target, key, propertyDesc, meta$1); let computedDesc = { enumerable: desc.enumerable, configurable: desc.configurable, get: DESCRIPTOR_GETTER_FUNCTION(key, desc), set: DESCRIPTOR_SETTER_FUNCTION(key, desc) }; return computedDesc; }; setClassicDecorator(decorator, desc); Object.setPrototypeOf(decorator, DecoratorClass.prototype); return decorator; } ///////////// const DECORATOR_DESCRIPTOR_MAP = new WeakMap(); /** Returns the CP descriptor associated with `obj` and `keyName`, if any. @method descriptorForProperty @param {Object} obj the object to check @param {String} keyName the key to check @return {Descriptor} @private */ function descriptorForProperty(obj, keyName, _meta) { (isDevelopingApp() && !(obj !== null) && assert('Cannot call `descriptorForProperty` on null', obj !== null)); (isDevelopingApp() && !(obj !== undefined) && assert('Cannot call `descriptorForProperty` on undefined', obj !== undefined)); (isDevelopingApp() && !(typeof obj === 'object' || typeof obj === 'function') && assert(`Cannot call \`descriptorForProperty\` on ${typeof obj}`, typeof obj === 'object' || typeof obj === 'function')); let meta = _meta === undefined ? peekMeta(obj) : _meta; if (meta !== null) { return meta.peekDescriptors(keyName); } } function descriptorForDecorator(dec) { return DECORATOR_DESCRIPTOR_MAP.get(dec); } /** Check whether a value is a decorator @method isClassicDecorator @param {any} possibleDesc the value to check @return {boolean} @private */ function isClassicDecorator(dec) { return typeof dec === 'function' && DECORATOR_DESCRIPTOR_MAP.has(dec); } /** Set a value as a decorator @method setClassicDecorator @param {function} decorator the value to mark as a decorator @private */ function setClassicDecorator(dec, value = true) { DECORATOR_DESCRIPTOR_MAP.set(dec, value); } const END_WITH_EACH_REGEX = /\.@each$/; /** Expands `pattern`, invoking `callback` for each expansion. The only pattern supported is brace-expansion, anything else will be passed once to `callback` directly. Example ```js import { expandProperties } from '@ember/object/computed'; function echo(arg){ console.log(arg); } expandProperties('foo.bar', echo); //=> 'foo.bar' expandProperties('{foo,bar}', echo); //=> 'foo', 'bar' expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz' expandProperties('{foo,bar}.baz', echo); //=> 'foo.baz', 'bar.baz' expandProperties('foo.{bar,baz}.[]', echo) //=> 'foo.bar.[]', 'foo.baz.[]' expandProperties('{foo,bar}.{spam,eggs}', echo) //=> 'foo.spam', 'foo.eggs', 'bar.spam', 'bar.eggs' expandProperties('{foo}.bar.{baz}') //=> 'foo.bar.baz' ``` @method expandProperties @static @for @ember/object/computed @public @param {String} pattern The property pattern to expand. @param {Function} callback The callback to invoke. It is invoked once per expansion, and is passed the expansion. */ function expandProperties(pattern, callback) { (isDevelopingApp() && !(typeof pattern === 'string') && assert(`A computed property key must be a string, you passed ${typeof pattern} ${pattern}`, typeof pattern === 'string')); (isDevelopingApp() && !(pattern.indexOf(' ') === -1) && assert('Brace expanded properties cannot contain spaces, e.g. "user.{firstName, lastName}" should be "user.{firstName,lastName}"', pattern.indexOf(' ') === -1)); // regex to look for double open, double close, or unclosed braces (isDevelopingApp() && !(pattern.match(/\{[^}{]*\{|\}[^}{]*\}|\{[^}]*$/g) === null) && assert(`Brace expanded properties have to be balanced and cannot be nested, pattern: ${pattern}`, pattern.match(/\{[^}{]*\{|\}[^}{]*\}|\{[^}]*$/g) === null)); let start = pattern.indexOf('{'); if (start < 0) { callback(pattern.replace(END_WITH_EACH_REGEX, '.[]')); } else { dive('', pattern, start, callback); } } function dive(prefix, pattern, start, callback) { let end = pattern.indexOf('}'), i = 0, newStart, arrayLength; let tempArr = pattern.substring(start + 1, end).split(','); let after = pattern.substring(end + 1); prefix = prefix + pattern.substring(0, start); arrayLength = tempArr.length; while (i < arrayLength) { newStart = after.indexOf('{'); if (newStart < 0) { callback((prefix + tempArr[i++] + after).replace(END_WITH_EACH_REGEX, '.[]')); } else { dive(prefix + tempArr[i++], after, newStart, callback); } } } const AFTER_OBSERVERS = ':change'; function changeEvent(keyName) { return keyName + AFTER_OBSERVERS; } /** @module @ember/object */ function addListener(obj, eventName, target, method, once, sync = true) { (isDevelopingApp() && !(Boolean(obj) && Boolean(eventName)) && assert('You must pass at least an object and event name to addListener', Boolean(obj) && Boolean(eventName))); if (!method && 'function' === typeof target) { method = target; target = null; } meta(obj).addToListeners(eventName, target, method, once === true, sync); } /** Remove an event listener Arguments should match those passed to `addListener`. @method removeListener @static @for @ember/object/events @param obj @param {String} eventName @param {Object|Function} target A target object or a function @param {Function|String} method A function or the name of a function to be called on `target` @public */ function removeListener(obj, eventName, targetOrFunction, functionOrName) { (isDevelopingApp() && !(Boolean(obj) && Boolean(eventName) && (typeof targetOrFunction === 'function' || typeof targetOrFunction === 'object' && Boolean(functionOrName))) && assert('You must pass at least an object, event name, and method or target and method/method name to removeListener', Boolean(obj) && Boolean(eventName) && (typeof targetOrFunction === 'function' || typeof targetOrFunction === 'object' && Boolean(functionOrName)))); let target, method; if (typeof targetOrFunction === 'object') { target = targetOrFunction; method = functionOrName; } else { target = null; method = targetOrFunction; } let m = meta(obj); m.removeFromListeners(eventName, target, method); } /** Send an event. The execution of suspended listeners is skipped, and once listeners are removed. A listener without a target is executed on the passed object. If an array of actions is not passed, the actions stored on the passed object are invoked. @method sendEvent @static @for @ember/object/events @param obj @param {String} eventName @param {Array} params Optional parameters for each listener. @return {Boolean} if the event was delivered to one or more actions @public */ function sendEvent(obj, eventName, params, actions, _meta) { if (actions === undefined) { let meta = _meta === undefined ? peekMeta(obj) : _meta; actions = meta !== null ? meta.matchingListeners(eventName) : undefined; } if (actions === undefined || actions.length === 0) { return false; } for (let i = actions.length - 3; i >= 0; i -= 3) { // looping in reverse for once listeners let target = actions[i]; let method = actions[i + 1]; let once = actions[i + 2]; if (!method) { continue; } if (once) { removeListener(obj, eventName, target, method); } if (!target) { target = obj; } let type = typeof method; if (type === 'string' || type === 'symbol') { method = target[method]; } method.apply(target, params); } return true; } /** @public @method hasListeners @static @for @ember/object/events @param obj @param {String} eventName @return {Boolean} if `obj` has listeners for event `eventName` */ function hasListeners(obj, eventName) { let meta = peekMeta(obj); if (meta === null) { return false; } let matched = meta.matchingListeners(eventName); return matched !== undefined && matched.length > 0; } /** Define a property as a function that should be executed when a specified event or events are triggered. ``` javascript import EmberObject from '@ember/object'; import { on } from '@ember/object/evented'; import { sendEvent } from '@ember/object/events'; let Job = EmberObject.extend({ logCompleted: on('completed', function() { console.log('Job completed!'); }) }); let job = Job.create(); sendEvent(job, 'completed'); // Logs 'Job completed!' ``` @method on @static @for @ember/object/evented @param {String} eventNames* @param {Function} func @return {Function} the listener function, passed as last argument to on(...) @public */ function on(...args) { let func = args.pop(); let events = args; (isDevelopingApp() && !(typeof func === 'function') && assert('on expects function as last argument', typeof func === 'function')); (isDevelopingApp() && !(events.length > 0 && events.every(p => typeof p === 'string' && p.length > 0)) && assert('on called without valid event names', events.length > 0 && events.every(p => typeof p === 'string' && p.length > 0))); setListeners(func, events); return func; } const SYNC_DEFAULT = !ENV._DEFAULT_ASYNC_OBSERVERS; const SYNC_OBSERVERS = new Map(); const ASYNC_OBSERVERS = new Map(); /** @module @ember/object */ /** @method addObserver @static @for @ember/object/observers @param obj @param {String} path @param {Object|Function} target @param {Function|String} [method] @public */ function addObserver(obj, path, target, method, sync = SYNC_DEFAULT) { let eventName = changeEvent(path); addListener(obj, eventName, target, method, false, sync); let meta = peekMeta(obj); if (meta === null || !(meta.isPrototypeMeta(obj) || meta.isInitializing())) { activateObserver(obj, eventName, sync); } } /** @method removeObserver @static @for @ember/object/observers @param obj @param {String} path @param {Object|Function} target @param {Function|String} [method] @public */ function removeObserver(obj, path, target, method, sync = SYNC_DEFAULT) { let eventName = changeEvent(path); let meta = peekMeta(obj); if (meta === null || !(meta.isPrototypeMeta(obj) || meta.isInitializing())) { deactivateObserver(obj, eventName, sync); } removeListener(obj, eventName, target, method); } function getOrCreateActiveObserversFor(target, sync) { let observerMap = sync === true ? SYNC_OBSERVERS : ASYNC_OBSERVERS; if (!observerMap.has(target)) { observerMap.set(target, new Map()); registerDestructor(target, () => destroyObservers(target), true); } return observerMap.get(target); } function activateObserver(target, eventName, sync = false) { let activeObservers = getOrCreateActiveObserversFor(target, sync); if (activeObservers.has(eventName)) { activeObservers.get(eventName).count++; } else { let path = eventName.substring(0, eventName.lastIndexOf(':')); let tag = getChainTagsForKey(target, path, tagMetaFor(target), peekMeta(target)); activeObservers.set(eventName, { count: 1, path, tag, lastRevision: valueForTag(tag), suspended: false }); } } let DEACTIVATE_SUSPENDED = false; let SCHEDULED_DEACTIVATE = []; function deactivateObserver(target, eventName, sync = false) { if (DEACTIVATE_SUSPENDED === true) { SCHEDULED_DEACTIVATE.push([target, eventName, sync]); return; } let observerMap = sync === true ? SYNC_OBSERVERS : ASYNC_OBSERVERS; let activeObservers = observerMap.get(target); if (activeObservers !== undefined) { let observer = activeObservers.get(eventName); observer.count--; if (observer.count === 0) { activeObservers.delete(eventName); if (activeObservers.size === 0) { observerMap.delete(target); } } } } function suspendedObserverDeactivation() { DEACTIVATE_SUSPENDED = true; } function resumeObserverDeactivation() { DEACTIVATE_SUSPENDED = false; for (let [target, eventName, sync] of SCHEDULED_DEACTIVATE) { deactivateObserver(target, eventName, sync); } SCHEDULED_DEACTIVATE = []; } /** * Primarily used for cases where we are redefining a class, e.g. mixins/reopen * being applied later. Revalidates all the observers, resetting their tags. * * @private * @param target */ function revalidateObservers(target) { if (ASYNC_OBSERVERS.has(target)) { ASYNC_OBSERVERS.get(target).forEach(observer => { observer.tag = getChainTagsForKey(target, observer.path, tagMetaFor(target), peekMeta(target)); observer.lastRevision = valueForTag(observer.tag); }); } if (SYNC_OBSERVERS.has(target)) { SYNC_OBSERVERS.get(target).forEach(observer => { observer.tag = getChainTagsForKey(target, observer.path, tagMetaFor(target), peekMeta(target)); observer.lastRevision = valueForTag(observer.tag); }); } } let lastKnownRevision = 0; function flushAsyncObservers(_schedule) { let currentRevision = valueForTag(CURRENT_TAG); if (lastKnownRevision === currentRevision) { return; } lastKnownRevision = currentRevision; ASYNC_OBSERVERS.forEach((activeObservers, target) => { let meta = peekMeta(target); activeObservers.forEach((observer, eventName) => { if (!validateTag(observer.tag, observer.lastRevision)) { let sendObserver = () => { try { sendEvent(target, eventName, [target, observer.path], undefined, meta); } finally { observer.tag = getChainTagsForKey(target, observer.path, tagMetaFor(target), peekMeta(target)); observer.lastRevision = valueForTag(observer.tag); } }; if (_schedule) { _schedule('actions', sendObserver); } else { sendObserver(); } } }); }); } function flushSyncObservers() { // When flushing synchronous observers, we know that something has changed (we // only do this during a notifyPropertyChange), so there's no reason to check // a global revision. SYNC_OBSERVERS.forEach((activeObservers, target) => { let meta = peekMeta(target); activeObservers.forEach((observer, eventName) => { if (!observer.suspended && !validateTag(observer.tag, observer.lastRevision)) { try { observer.suspended = true; sendEvent(target, eventName, [target, observer.path], undefined, meta); } finally { observer.tag = getChainTagsForKey(target, observer.path, tagMetaFor(target), peekMeta(target)); observer.lastRevision = valueForTag(observer.tag); observer.suspended = false; } } }); }); } function setObserverSuspended(target, property, suspended) { let activeObservers = SYNC_OBSERVERS.get(target); if (!activeObservers) { return; } let observer = activeObservers.get(changeEvent(property)); if (observer) { observer.suspended = suspended; } } function destroyObservers(target) { if (SYNC_OBSERVERS.size > 0) SYNC_OBSERVERS.delete(target); if (ASYNC_OBSERVERS.size > 0) ASYNC_OBSERVERS.delete(target); } const PROPERTY_DID_CHANGE = Symbol('PROPERTY_DID_CHANGE'); function hasPropertyDidChange(obj) { return obj != null && typeof obj === 'object' && typeof obj[PROPERTY_DID_CHANGE] === 'function'; } let deferred = 0; /** This function is called just after an object property has changed. It will notify any observers and clear caches among other things. Normally you will not need to call this method directly but if for some reason you can't directly watch a property you can invoke this method manually. @method notifyPropertyChange @for @ember/object @param {Object} obj The object with the property that will change @param {String} keyName The property key (or path) that will change. @param {Meta} [_meta] The objects meta. @param {unknown} [value] The new value to set for the property @return {void} @since 3.1.0 @public */ function notifyPropertyChange(obj, keyName, _meta, value) { let meta = _meta === undefined ? peekMeta(obj) : _meta; if (meta !== null && (meta.isInitializing() || meta.isPrototypeMeta(obj))) { return; } markObjectAsDirty(obj, keyName); if (deferred <= 0) { flushSyncObservers(); } if (PROPERTY_DID_CHANGE in obj) { // It's redundant to do this here, but we don't want to check above so we can avoid an extra function call in prod. (isDevelopingApp() && !(hasPropertyDidChange(obj)) && assert('property did change hook is invalid', hasPropertyDidChange(obj))); // we need to check the arguments length here; there's a check in Component's `PROPERTY_DID_CHANGE` // that checks its arguments length, so we have to explicitly not call this with `value` // if it is not passed to `notifyPropertyChange` if (arguments.length === 4) { obj[PROPERTY_DID_CHANGE](keyName, value); } else { obj[PROPERTY_DID_CHANGE](keyName); } } } /** @method beginPropertyChanges @chainable @private */ function beginPropertyChanges() { deferred++; suspendedObserverDeactivation(); } /** @method endPropertyChanges @private */ function endPropertyChanges() { deferred--; if (deferred <= 0) { flushSyncObservers(); resumeObserverDeactivation(); } } /** Make a series of property changes together in an exception-safe way. ```javascript Ember.changeProperties(function() { obj1.set('foo', mayBlowUpWhenSet); obj2.set('bar', baz); }); ``` @method changeProperties @param {Function} callback @private */ function changeProperties(callback) { beginPropertyChanges(); try { callback(); } finally { endPropertyChanges(); } } /** @module @ember/object */ const DEEP_EACH_REGEX = /\.@each\.[^.]+\./; function noop() {} /** `@computed` is a decorator that turns a JavaScript getter and setter into a computed property, which is a _cached, trackable value_. By default the getter will only be called once and the result will be cached. You can specify various properties that your computed property depends on. This will force the cached result to be cleared if the dependencies are modified, and lazily recomputed the next time something asks for it. In the following example we decorate a getter - `fullName` - by calling `computed` with the property dependencies (`firstName` and `lastName`) as arguments. The `fullName` getter will be called once (regardless of how many times it is accessed) as long as its dependencies do not change. Once `firstName` or `lastName` are updated any future calls to `fullName` will incorporate the new values, and any watchers of the value such as templates will be updated: ```javascript import { computed, set } from '@ember/object'; class Person { constructor(firstName, lastName) { set(this, 'firstName', firstName); set(this, 'lastName', lastName); } @computed('firstName', 'lastName') get fullName() { return `${this.firstName} ${this.lastName}`; } }); let tom = new Person('Tom', 'Dale'); tom.fullName; // 'Tom Dale' ``` You can also provide a setter, which will be used when updating the computed property. Ember's `set` function must be used to update the property since it will also notify observers of the property: ```javascript import { computed, set } from '@ember/object'; class Person { constructor(firstName, lastName) { set(this, 'firstName', firstName); set(this, 'lastName', lastName); } @computed('firstName', 'lastName') get fullName() { return `${this.firstName} ${this.lastName}`; } set fullName(value) { let [firstName, lastName] = value.split(' '); set(this, 'firstName', firstName); set(this, 'lastName', lastName); } }); let person = new Person(); set(person, 'fullName', 'Peter Wagenet'); person.firstName; // 'Peter' person.lastName; // 'Wagenet' ``` You can also pass a getter function or object with `get` and `set` functions as the last argument to the computed decorator. This allows you to define computed property _macros_: ```js import { computed } from '@ember/object'; function join(...keys) { return computed(...keys, function() { return keys.map(key => this[key]).join(' '); }); } class Person { @join('firstName', 'lastName') fullName; } ``` Note that when defined this way, getters and setters receive the _key_ of the property they are decorating as the first argument. Setters receive the value they are setting to as the second argument instead. Additionally, setters must _return_ the value that should be cached: ```javascript import { computed, set } from '@ember/object'; function fullNameMacro(firstNameKey, lastNameKey) { return computed(firstNameKey, lastNameKey, { get() { return `${this[firstNameKey]} ${this[lastNameKey]}`; } set(key, value) { let [firstName, lastName] = value.split(' '); set(this, firstNameKey, firstName); set(this, lastNameKey, lastName); return value; } }); } class Person { constructor(firstName, lastName) { set(this, 'firstName', firstName); set(this, 'lastName', lastName); } @fullNameMacro('firstName', 'lastName') fullName; }); let person = new Person(); set(person, 'fullName', 'Peter Wagenet'); person.firstName; // 'Peter' person.lastName; // 'Wagenet' ``` Computed properties can also be used in classic classes. To do this, we provide the getter and setter as the last argument like we would for a macro, and we assign it to a property on the class definition. This is an _anonymous_ computed macro: ```javascript import EmberObject, { computed, set } from '@ember/object'; let Person = EmberObject.extend({ // these will be supplied by `create` firstName: null, lastName: null, fullName: computed('firstName', 'lastName', { get() { return `${this.firstName} ${this.lastName}`; } set(key, value) { let [firstName, lastName] = value.split(' '); set(this, 'firstName', firstName); set(this, 'lastName', lastName); return value; } }) }); let tom = Person.create({ firstName: 'Tom', lastName: 'Dale' }); tom.get('fullName') // 'Tom Dale' ``` You can overwrite computed property without setters with a normal property (no longer computed) that won't change if dependencies change. You can also mark computed property as `.readOnly()` and block all attempts to set it. ```javascript import { computed, set } from '@ember/object'; class Person { constructor(firstName, lastName) { set(this, 'firstName', firstName); set(this, 'lastName', lastName); } @computed('firstName', 'lastName').readOnly() get fullName() { return `${this.firstName} ${this.lastName}`; } }); let person = new Person(); person.set('fullName', 'Peter Wagenet'); // Uncaught Error: Cannot set read-only property "fullName" on object: <(...):emberXXX> ``` Additional resources: - [Decorators RFC](https://github.com/emberjs/rfcs/blob/master/text/0408-decorators.md) - [New CP syntax RFC](https://github.com/emberjs/rfcs/blob/master/text/0011-improved-cp-syntax.md) - [New computed syntax explained in "Ember 1.12 released" ](https://emberjs.com/blog/2015/05/13/ember-1-12-released.html#toc_new-computed-syntax) @class ComputedProperty @public */ class ComputedProperty extends ComputedDescriptor { _readOnly = false; _hasConfig = false; _getter = undefined; _setter = undefined; constructor(args) { super(); let maybeConfig = args[args.length - 1]; if (typeof maybeConfig === 'function' || maybeConfig !== null && typeof maybeConfig === 'object') { this._hasConfig = true; let config = args.pop(); if (typeof config === 'function') { (isDevelopingApp() && !(!isClassicDecorator(config)) && assert(`You attempted to pass a computed property instance to computed(). Computed property instances are decorator functions, and cannot be passed to computed() because they cannot be turned into decorators twice`, !isClassicDecorator(config))); this._getter = config; } else { const objectConfig = config; (isDevelopingApp() && !(typeof objectConfig === 'object' && !Array.isArray(objectConfig)) && assert('computed expects a function or an object as last argument.', typeof objectConfig === 'object' && !Array.isArray(objectConfig))); (isDevelopingApp() && !(Object.keys(objectConfig).every(key => key === 'get' || key === 'set')) && assert('Config object passed to computed can only contain `get` and `set` keys.', Object.keys(objectConfig).every(key => key === 'get' || key === 'set'))); (isDevelopingApp() && !(Boolean(objectConfig.get) || Boolean(objectConfig.set)) && assert('Computed properties must receive a getter or a setter, you passed none.', Boolean(objectConfig.get) || Boolean(objectConfig.set))); this._getter = objectConfig.get || noop; this._setter = objectConfig.set; } } if (args.length > 0) { this._property(...args); } } setup(obj, keyName, propertyDesc, meta) { super.setup(obj, keyName, propertyDesc, meta); (isDevelopingApp() && !(!(propertyDesc && typeof propertyDesc.value === 'function')) && assert(`@computed can only be used on accessors or fields, attempted to use it with ${keyName} but that was a method. Try converting it to a getter (e.g. \`get ${keyName}() {}\`)`, !(propertyDesc && typeof propertyDesc.value === 'function'))); (isDevelopingApp() && !(!propertyDesc || !propertyDesc.initializer) && assert(`@computed can only be used on empty fields. ${keyName} has an initial value (e.g. \`${keyName} = someValue\`)`, !propertyDesc || !propertyDesc.initializer)); (isDevelopingApp() && !(!(this._hasConfig && propertyDesc && (typeof propertyDesc.get === 'function' || typeof propertyDesc.set === 'function'))) && assert(`Attempted to apply a computed property that already has a getter/setter to a ${keyName}, but it is a method or an accessor. If you passed @computed a function or getter/setter (e.g. \`@computed({ get() { ... } })\`), then it must be applied to a field`, !(this._hasConfig && propertyDesc && (typeof propertyDesc.get === 'function' || typeof propertyDesc.set === 'function')))); if (this._hasConfig === false) { (isDevelopingApp() && !(propertyDesc && (typeof propertyDesc.get === 'function' || typeof propertyDesc.set === 'function')) && assert(`Attempted to use @computed on ${keyName}, but it did not have a getter or a setter. You must either pass a get a function or getter/setter to @computed directly (e.g. \`@computed({ get() { ... } })\`) or apply @computed directly to a getter/setter`, propertyDesc && (typeof propertyDesc.get === 'function' || typeof propertyDesc.set === 'function'))); let { get, set } = propertyDesc; if (get !== undefined) { this._getter = get; } if (set !== undefined) { this._setter = function setterWrapper(_key, value) { let ret = set.call(this, value); if (get !== undefined) { return typeof ret === 'undefined' ? get.call(this) : ret; } return ret; }; } } } _property(...passedArgs) { let args = []; function addArg(property) { (isDevelopingApp() && !(DEEP_EACH_REGEX.test(property) === false) && assert(`Dependent keys containing @each only work one level deep. ` + `You used the key "${property}" which is invalid. ` + `Please create an intermediary computed property or ` + `switch to using tracked properties.`, DEEP_EACH_REGEX.test(property) === false)); args.push(property); } for (let arg of passedArgs) { expandProperties(arg, addArg); } this._dependentKeys = args; } get(obj, keyName) { let meta$1 = meta(obj); let tagMeta = tagMetaFor(obj); let propertyTag = tagFor(obj, keyName, tagMeta); let ret; let revision = meta$1.revisionFor(keyName); if (revision !== undefined && validateTag(propertyTag, revision)) { ret = meta$1.valueFor(keyName); } else { // For backwards compatibility, we only throw if the CP has any dependencies. CPs without dependencies // should be allowed, even after the object has been destroyed, which is why we check _dependentKeys. (isDevelopingApp() && !(this._dependentKeys === undefined || !isDestroyed(obj)) && assert(`Attempted to access the computed ${obj}.${keyName} on a destroyed object, which is not allowed`, this._dependentKeys === undefined || !isDestroyed(obj))); let { _getter, _dependentKeys } = this; // Create a tracker that absorbs any trackable actions inside the CP untrack(() => { ret = _getter.call(obj, keyName); }); if (_dependentKeys !== undefined) { UPDATE_TAG(propertyTag, getChainTagsForKeys(obj, _dependentKeys, tagMeta, meta$1)); if (isDevelopingApp()) { ALLOW_CYCLES.set(propertyTag, true); } } meta$1.setValueFor(keyName, ret); meta$1.setRevisionFor(keyName, valueForTag(propertyTag)); finishLazyChains(meta$1, keyName, ret); } consumeTag(propertyTag); // Add the tag of the returned value if it is an array, since arrays // should always cause updates if they are consumed and then changed if (Array.isArray(ret)) { consumeTag(tagFor(ret, '[]')); } return ret; } set(obj, keyName, value) { if (this._readOnly) { this._throwReadOnlyError(obj, keyName); } (isDevelopingApp() && !(this._setter !== undefined) && assert(`Cannot override the computed property \`${keyName}\` on ${toString(obj)}.`, this._setter !== undefined)); let meta$1 = meta(obj); // ensure two way binding works when the component has defined a computed // property with both a setter and dependent keys, in that scenario without // the sync observer added below the caller's value will never be updated // // See GH#18147 / GH#19028 for details. if ( // ensure that we only run this once, while the component is being instantiated meta$1.isInitializing() && this._dependentKeys !== undefined && this._dependentKeys.length > 0 && typeof obj[PROPERTY_DID_CHANGE] === 'function' && obj.isComponent) { // It's redundant to do this here, but we don't want to check above so we can avoid an extra function call in prod. (isDevelopingApp() && !(hasPropertyDidChange(obj)) && assert('property did change hook is invalid', hasPropertyDidChange(obj))); addObserver(obj, keyName, () => { obj[PROPERTY_DID_CHANGE](keyName); }, undefined, true); } let ret; try { beginPropertyChanges(); ret = this._set(obj, keyName, value, meta$1); finishLazyChains(meta$1, keyName, ret); let tagMeta = tagMetaFor(obj); let propertyTag = tagFor(obj, keyName, tagMeta); let { _dependentKeys } = this; if (_dependentKeys !== undefined) { UPDATE_TAG(propertyTag, getChainTagsForKeys(obj, _dependentKeys, tagMeta, meta$1)); if (isDevelopingApp()) { ALLOW_CYCLES.set(propertyTag, true); } } meta$1.setRevisionFor(keyName, valueForTag(propertyTag)); } finally { endPropertyChanges(); } return ret; } _throwReadOnlyError(obj, keyName) { throw new Error(`Cannot set read-only property "${keyName}" on object: ${inspect(obj)}`); } _set(obj, keyName, value, meta) { let hadCachedValue = meta.revisionFor(keyName) !== undefined; let cachedValue = meta.valueFor(keyName); let ret; let { _setter } = this; setObserverSuspended(obj, keyName, true); try { ret = _setter.call(obj, keyName, value, cachedValue); } finally { setObserverSuspended(obj, keyName, false); } // allows setter to return the same value that is cached already if (hadCachedValue && cachedValue === ret) { return ret; } meta.setValueFor(keyName, ret); notifyPropertyChange(obj, keyName, meta, value); return ret; } /* called before property is overridden */ teardown(obj, keyName, meta) { if (meta.revisionFor(keyName) !== undefined) { meta.setRevisionFor(keyName, undefined); meta.setValueFor(keyName, undefined); } super.teardown(obj, keyName, meta); } } class AutoComputedProperty extends ComputedProperty { get(obj, keyName) { let meta$1 = meta(obj); let tagMeta = tagMetaFor(obj); let propertyTag = tagFor(obj, keyName, tagMeta); let ret; let revision = meta$1.revisionFor(keyName); if (revision !== undefined && validateTag(propertyTag, revision)) { ret = meta$1.valueFor(keyName); } else { (isDevelopingApp() && !(!isDestroyed(obj)) && assert(`Attempted to access the computed ${obj}.${keyName} on a destroyed object, which is not allowed`, !isDestroyed(obj))); let { _getter } = this; // Create a tracker that absorbs any trackable actions inside the CP let tag = track(() => { ret = _getter.call(obj, keyName); }); UPDATE_TAG(propertyTag, tag); meta$1.setValueFor(keyName, ret); meta$1.setRevisionFor(keyName, valueForTag(propertyTag)); finishLazyChains(meta$1, keyName, ret); } consumeTag(propertyTag); // Add the tag of the returned value if it is an array, since arrays // should always cause updates if they are consumed and then changed if (Array.isArray(ret)) { consumeTag(tagFor(ret, '[]', tagMeta)); } return ret; } } // TODO: This class can be svelted once `meta` has been deprecated class ComputedDecoratorImpl extends Function { /** Call on a computed property to set it into read-only mode. When in this mode the computed property will throw an error when set. Example: ```javascript import { computed, set } from '@ember/object'; class Person { @computed().readOnly() get guid() { return 'guid-guid-guid'; } } let person = new Person(); set(person, 'guid', 'new-guid'); // will throw an exception ``` Classic Class Example: ```javascript import EmberObject, { computed } from '@ember/object'; let Person = EmberObject.extend({ guid: computed(function() { return 'guid-guid-guid'; }).readOnly() }); let person = Person.create(); person.set('guid', 'new-guid'); // will throw an exception ``` @method readOnly @return {ComputedProperty} this @chainable @public */ readOnly() { let desc = descriptorForDecorator(this); (isDevelopingApp() && !(!(desc._setter && desc._setter !== desc._getter)) && assert('Computed properties that define a setter using the new syntax cannot be read-only', !(desc._setter && desc._setter !== desc._getter))); desc._readOnly = true; return this; } /** In some cases, you may want to annotate computed properties with additional metadata about how they function or what values they operate on. For example, computed property functions may close over variables that are then no longer available for introspection. You can pass a hash of these values to a computed property. Example: ```javascript import { computed } from '@ember/object'; import Person from 'my-app/utils/person'; class Store { @computed().meta({ type: Person }) get person() { let personId = this.personId; return Person.create({ id: personId }); } } ``` Classic Class Example: ```javascript import { computed } from '@ember/object'; import Person from 'my-app/utils/person'; const Store = EmberObject.extend({ person: computed(function() { let personId = this.get('personId'); return Person.create({ id: personId }); }).meta({ type: Person }) }); ``` The hash that you pass to the `meta()` function will be saved on the computed property descriptor under the `_meta` key. Ember runtime exposes a public API for retrieving these values from classes, via the `metaForProperty()` function. @method meta @param {Object} meta @chainable @public */ meta(meta) { let prop = descriptorForDecorator(this); if (arguments.length === 0) { return prop._meta || {}; } else { prop._meta = meta; return this; } } // TODO: Remove this when we can provide alternatives in the ecosystem to // addons such as ember-macro-helpers that use it. /** @internal */ get _getter() { return descriptorForDecorator(this)._getter; } // TODO: Refactor this, this is an internal API only /** @internal */ set enumerable(value) { descriptorForDecorator(this).enumerable = value; } } /** This helper returns a new property descriptor that wraps the passed computed property function. You can use this helper to define properties with native decorator syntax, mixins, or via `defineProperty()`. Example: ```js import { computed, set } from '@ember/object'; class Person { constructor() { this.firstName = 'Betty'; this.lastName = 'Jones'; }, @computed('firstName', 'lastName') get fullName() { return `${this.firstName} ${this.lastName}`; } } let client = new Person(); client.fullName; // 'Betty Jones' set(client, 'lastName', 'Fuller'); client.fullName; // 'Betty Fuller' ``` Classic Class Example: ```js import EmberObject, { computed } from '@ember/object'; let Person = EmberObject.extend({ init() { this._super(...arguments); this.firstName = 'Betty'; this.lastName = 'Jones'; }, fullName: computed('firstName', 'lastName', function() { return `${this.get('firstName')} ${this.get('lastName')}`; }) }); let client = Person.create(); client.get('fullName'); // 'Betty Jones' client.set('lastName', 'Fuller'); client.get('fullName'); // 'Betty Fuller' ``` You can also provide a setter, either directly on the class using native class syntax, or by passing a hash with `get` and `set` functions. Example: ```js import { computed, set } from '@ember/object'; class Person { constructor() { this.firstName = 'Betty'; this.lastName = 'Jones'; }, @computed('firstName', 'lastName') get fullName() { return `${this.firstName} ${this.lastName}`; } set fullName(value) { let [firstName, lastName] = value.split(/\s+/); set(this, 'firstName', firstName); set(this, 'lastName', lastName); return value; } } let client = new Person(); client.fullName; // 'Betty Jones' set(client, 'lastName', 'Fuller'); client.fullName; // 'Betty Fuller' ``` Classic Class Example: ```js import EmberObject, { computed } from '@ember/object'; let Person = EmberObject.extend({ init() { this._super(...arguments); this.firstName = 'Betty'; this.lastName = 'Jones'; }, fullName: computed('firstName', 'lastName', { get(key) { return `${this.get('firstName')} ${this.get('lastName')}`; }, set(key, value) { let [firstName, lastName] = value.split(/\s+/); this.setProperties({ firstName, lastName }); return value; } }) }); let client = Pe