ember-source
Version:
A JavaScript framework for creating ambitious web applications
1,446 lines (1,297 loc) • 67.8 kB
JavaScript
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