From 0dc1a6c73258d891fe54014754c33772310d7b48 Mon Sep 17 00:00:00 2001 From: Stefan Penner Date: Fri, 6 Mar 2015 14:57:46 -0800 Subject: [PATCH 1/2] [BUGFIX beta] [fixes #11181, #10343, #9739, #9462, #4919, #4231, #3706, #5596, #9485, #9492, #5319, #5268, #4831, #5558] Move away from AC/RC instead use the simpler naive enumerable methods, and rely on glimmers stable rendering for efficiency. For more complex scenarios, custom solutions should be used. @wagenet & @stefanpenner --- packages/ember-metal/lib/mixin.js | 3 +- .../lib/computed/array_computed.js | 192 -- .../lib/computed/reduce_computed.js | 883 -------- .../lib/computed/reduce_computed_macros.js | 521 ++--- packages/ember-runtime/lib/main.js | 14 - .../computed/reduce_computed_macros_test.js | 1949 ++++++++--------- .../tests/computed/reduce_computed_test.js | 1078 --------- .../controllers/item_controller_class_test.js | 4 +- 8 files changed, 1026 insertions(+), 3618 deletions(-) delete mode 100644 packages/ember-runtime/lib/computed/array_computed.js delete mode 100644 packages/ember-runtime/lib/computed/reduce_computed.js delete mode 100644 packages/ember-runtime/tests/computed/reduce_computed_test.js diff --git a/packages/ember-metal/lib/mixin.js b/packages/ember-metal/lib/mixin.js index 655d44c70f8..7b402726128 100644 --- a/packages/ember-metal/lib/mixin.js +++ b/packages/ember-metal/lib/mixin.js @@ -542,8 +542,7 @@ export function mixin(obj, ...args) { @namespace Ember @public */ -export default Mixin; -function Mixin(args, properties) { +export default function Mixin(args, properties) { this.properties = properties; var length = args && args.length; diff --git a/packages/ember-runtime/lib/computed/array_computed.js b/packages/ember-runtime/lib/computed/array_computed.js deleted file mode 100644 index 0c4dcdc9950..00000000000 --- a/packages/ember-runtime/lib/computed/array_computed.js +++ /dev/null @@ -1,192 +0,0 @@ -import Ember from 'ember-metal/core'; -import { - ReduceComputedProperty -} from 'ember-runtime/computed/reduce_computed'; -import { addObserver } from 'ember-metal/observer'; -import EmberError from 'ember-metal/error'; - -var a_slice = [].slice; - -function ArrayComputedProperty() { - var cp = this; - - this._isArrayComputed = true; - ReduceComputedProperty.apply(this, arguments); - - this._getter = (function(reduceFunc) { - return function (propertyName) { - if (!cp._hasInstanceMeta(this, propertyName)) { - // When we recompute an array computed property, we need already - // retrieved arrays to be updated; we can't simply empty the cache and - // hope the array is re-retrieved. - cp._dependentKeys.forEach(function(dependentKey) { - addObserver(this, dependentKey, function() { - cp.recomputeOnce.call(this, propertyName); - }); - }, this); - } - - return reduceFunc.apply(this, arguments); - }; - })(this._getter); - - return this; -} - -ArrayComputedProperty.prototype = Object.create(ReduceComputedProperty.prototype); - -ArrayComputedProperty.prototype.initialValue = function () { - return Ember.A(); -}; - -ArrayComputedProperty.prototype.resetValue = function (array) { - array.clear(); - return array; -}; - -// This is a stopgap to keep the reference counts correct with lazy CPs. -ArrayComputedProperty.prototype.didChange = function (obj, keyName) { - return; -}; - -/** - Creates a computed property which operates on dependent arrays and - is updated with "one at a time" semantics. When items are added or - removed from the dependent array(s) an array computed only operates - on the change instead of re-evaluating the entire array. This should - return an array, if you'd like to use "one at a time" semantics and - compute some value other then an array look at - `Ember.reduceComputed`. - - If there are more than one arguments the first arguments are - considered to be dependent property keys. The last argument is - required to be an options object. The options object can have the - following three properties. - - `initialize` - An optional initialize function. Typically this will be used - to set up state on the instanceMeta object. - - `removedItem` - A function that is called each time an element is - removed from the array. - - `addedItem` - A function that is called each time an element is - added to the array. - - - The `initialize` function has the following signature: - - ```javascript - function(array, changeMeta, instanceMeta) - ``` - - `array` - The initial value of the arrayComputed, an empty array. - - `changeMeta` - An object which contains meta information about the - computed. It contains the following properties: - - - `property` the computed property - - `propertyName` the name of the property on the object - - `instanceMeta` - An object that can be used to store meta - information needed for calculating your computed. For example a - unique computed might use this to store the number of times a given - element is found in the dependent array. - - - The `removedItem` and `addedItem` functions both have the following signature: - - ```javascript - function(accumulatedValue, item, changeMeta, instanceMeta) - ``` - - `accumulatedValue` - The value returned from the last time - `removedItem` or `addedItem` was called or an empty array. - - `item` - the element added or removed from the array - - `changeMeta` - An object which contains meta information about the - change. It contains the following properties: - - - `property` the computed property - - `propertyName` the name of the property on the object - - `index` the index of the added or removed item - - `item` the added or removed item: this is exactly the same as - the second arg - - `arrayChanged` the array that triggered the change. Can be - useful when depending on multiple arrays. - - For property changes triggered on an item property change (when - depKey is something like `someArray.@each.someProperty`), - `changeMeta` will also contain the following property: - - - `previousValues` an object whose keys are the properties that changed on - the item, and whose values are the item's previous values. - - `previousValues` is important Ember coalesces item property changes via - Ember.run.once. This means that by the time removedItem gets called, item has - the new values, but you may need the previous value (eg for sorting & - filtering). - - `instanceMeta` - An object that can be used to store meta - information needed for calculating your computed. For example a - unique computed might use this to store the number of times a given - element is found in the dependent array. - - The `removedItem` and `addedItem` functions should return the accumulated - value. It is acceptable to not return anything (ie return undefined) - to invalidate the computation. This is generally not a good idea for - arrayComputed but it's used in eg max and min. - - Example - - ```javascript - Ember.computed.map = function(dependentKey, callback) { - var options = { - addedItem: function(array, item, changeMeta, instanceMeta) { - var mapped = callback(item); - array.insertAt(changeMeta.index, mapped); - return array; - }, - removedItem: function(array, item, changeMeta, instanceMeta) { - array.removeAt(changeMeta.index, 1); - return array; - } - }; - - return Ember.arrayComputed(dependentKey, options); - }; - ``` - - @method arrayComputed - @for Ember - @param {String} [dependentKeys*] - @param {Object} options - @return {Ember.ComputedProperty} - @deprecated - @private -*/ -function arrayComputed(options) { - var args; - - if (arguments.length > 1) { - args = a_slice.call(arguments, 0, -1); - options = a_slice.call(arguments, -1)[0]; - } - - if (typeof options !== 'object') { - throw new EmberError('Array Computed Property declared without an options hash'); - } - - var cp = new ArrayComputedProperty(options); - - if (args) { - cp.property.apply(cp, args); - } - - return cp; -} - -export { - arrayComputed, - ArrayComputedProperty -}; diff --git a/packages/ember-runtime/lib/computed/reduce_computed.js b/packages/ember-runtime/lib/computed/reduce_computed.js deleted file mode 100644 index a84adb1530e..00000000000 --- a/packages/ember-runtime/lib/computed/reduce_computed.js +++ /dev/null @@ -1,883 +0,0 @@ -import Ember from 'ember-metal/core'; // Ember.assert -import { get as e_get } from 'ember-metal/property_get'; -import { - guidFor, - meta as metaFor -} from 'ember-metal/utils'; -import EmberError from 'ember-metal/error'; -import { - propertyWillChange, - propertyDidChange -} from 'ember-metal/property_events'; -import expandProperties from 'ember-metal/expand_properties'; -import { - addObserver, - removeObserver, - addBeforeObserver, - removeBeforeObserver -} from 'ember-metal/observer'; -import { - ComputedProperty, - cacheFor -} from 'ember-metal/computed'; -import TrackedArray from 'ember-runtime/system/tracked_array'; -import EmberArray from 'ember-runtime/mixins/array'; -import run from 'ember-metal/run_loop'; - -var cacheSet = cacheFor.set; -var cacheGet = cacheFor.get; -var cacheRemove = cacheFor.remove; -var a_slice = [].slice; -// Here we explicitly don't allow `@each.foo`; it would require some special -// testing, but there's no particular reason why it should be disallowed. -var eachPropertyPattern = /^(.*)\.@each\.(.*)/; -var doubleEachPropertyPattern = /(.*\.@each){2,}/; -var arrayBracketPattern = /\.\[\]$/; - -function get(obj, key) { - if (key === '@this') { - return obj; - } - - return e_get(obj, key); -} - -/* - Tracks changes to dependent arrays, as well as to properties of items in - dependent arrays. - - @class DependentArraysObserver -*/ -function DependentArraysObserver(callbacks, cp, instanceMeta, context, propertyName, sugarMeta) { - // user specified callbacks for `addedItem` and `removedItem` - this.callbacks = callbacks; - - // the computed property: remember these are shared across instances - this.cp = cp; - - // the ReduceComputedPropertyInstanceMeta this DependentArraysObserver is - // associated with - this.instanceMeta = instanceMeta; - - // A map of array guids to dependentKeys, for the given context. We track - // this because we want to set up the computed property potentially before the - // dependent array even exists, but when the array observer fires, we lack - // enough context to know what to update: we can recover that context by - // getting the dependentKey. - this.dependentKeysByGuid = {}; - - // a map of dependent array guids -> TrackedArray instances. We use - // this to lazily recompute indexes for item property observers. - this.trackedArraysByGuid = {}; - - // We suspend observers to ignore replacements from `reset` when totally - // recomputing. Unfortunately we cannot properly suspend the observers - // because we only have the key; instead we make the observers no-ops - this.suspended = false; - - // This is used to coalesce item changes from property observers within a - // single item. - this.changedItems = {}; - // This is used to coalesce item changes for multiple items that depend on - // some shared state. - this.changedItemCount = 0; -} - -function ItemPropertyObserverContext(dependentArray, index, trackedArray) { - Ember.assert('Internal error: trackedArray is null or undefined', trackedArray); - - this.dependentArray = dependentArray; - this.index = index; - this.item = dependentArray.objectAt(index); - this.trackedArray = trackedArray; - this.beforeObserver = null; - this.observer = null; - this.destroyed = false; -} - -DependentArraysObserver.prototype = { - setValue(newValue) { - this.instanceMeta.setValue(newValue, true); - }, - - getValue() { - return this.instanceMeta.getValue(); - }, - - setupObservers(dependentArray, dependentKey) { - this.dependentKeysByGuid[guidFor(dependentArray)] = dependentKey; - - dependentArray.addArrayObserver(this, { - willChange: 'dependentArrayWillChange', - didChange: 'dependentArrayDidChange' - }); - - if (this.cp._itemPropertyKeys[dependentKey]) { - this.setupPropertyObservers(dependentKey, this.cp._itemPropertyKeys[dependentKey]); - } - }, - - teardownObservers(dependentArray, dependentKey) { - var itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || []; - - delete this.dependentKeysByGuid[guidFor(dependentArray)]; - - this.teardownPropertyObservers(dependentKey, itemPropertyKeys); - - dependentArray.removeArrayObserver(this, { - willChange: 'dependentArrayWillChange', - didChange: 'dependentArrayDidChange' - }); - }, - - suspendArrayObservers(callback, binding) { - var oldSuspended = this.suspended; - this.suspended = true; - callback.call(binding); - this.suspended = oldSuspended; - }, - - setupPropertyObservers(dependentKey, itemPropertyKeys) { - var dependentArray = get(this.instanceMeta.context, dependentKey); - var length = get(dependentArray, 'length'); - var observerContexts = new Array(length); - - this.resetTransformations(dependentKey, observerContexts); - - dependentArray.forEach(function (item, index) { - var observerContext = this.createPropertyObserverContext(dependentArray, index, this.trackedArraysByGuid[dependentKey]); - observerContexts[index] = observerContext; - - itemPropertyKeys.forEach(function (propertyKey) { - addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); - addObserver(item, propertyKey, this, observerContext.observer); - }, this); - }, this); - }, - - teardownPropertyObservers(dependentKey, itemPropertyKeys) { - var dependentArrayObserver = this; - var trackedArray = this.trackedArraysByGuid[dependentKey]; - var beforeObserver, observer, item; - - if (!trackedArray) { return; } - - trackedArray.apply(function (observerContexts, offset, operation) { - if (operation === TrackedArray.DELETE) { return; } - - observerContexts.forEach(function (observerContext) { - observerContext.destroyed = true; - beforeObserver = observerContext.beforeObserver; - observer = observerContext.observer; - item = observerContext.item; - - itemPropertyKeys.forEach(function (propertyKey) { - removeBeforeObserver(item, propertyKey, dependentArrayObserver, beforeObserver); - removeObserver(item, propertyKey, dependentArrayObserver, observer); - }); - }); - }); - }, - - createPropertyObserverContext(dependentArray, index, trackedArray) { - var observerContext = new ItemPropertyObserverContext(dependentArray, index, trackedArray); - - this.createPropertyObserver(observerContext); - - return observerContext; - }, - - createPropertyObserver(observerContext) { - var dependentArrayObserver = this; - - observerContext.beforeObserver = function (obj, keyName) { - return dependentArrayObserver.itemPropertyWillChange(obj, keyName, observerContext.dependentArray, observerContext); - }; - - observerContext.observer = function (obj, keyName) { - return dependentArrayObserver.itemPropertyDidChange(obj, keyName, observerContext.dependentArray, observerContext); - }; - }, - - resetTransformations(dependentKey, observerContexts) { - this.trackedArraysByGuid[dependentKey] = new TrackedArray(observerContexts); - }, - - trackAdd(dependentKey, index, newItems) { - var trackedArray = this.trackedArraysByGuid[dependentKey]; - - if (trackedArray) { - trackedArray.addItems(index, newItems); - } - }, - - trackRemove(dependentKey, index, removedCount) { - var trackedArray = this.trackedArraysByGuid[dependentKey]; - - if (trackedArray) { - return trackedArray.removeItems(index, removedCount); - } - - return []; - }, - - updateIndexes(trackedArray, array) { - var length = get(array, 'length'); - // OPTIMIZE: we could stop updating once we hit the object whose observer - // fired; ie partially apply the transformations - trackedArray.apply(function (observerContexts, offset, operation, operationIndex) { - // we don't even have observer contexts for removed items, even if we did, - // they no longer have any index in the array - if (operation === TrackedArray.DELETE) { return; } - if (operationIndex === 0 && operation === TrackedArray.RETAIN && observerContexts.length === length && offset === 0) { - // If we update many items we don't want to walk the array each time: we - // only need to update the indexes at most once per run loop. - return; - } - - observerContexts.forEach(function (context, index) { - context.index = index + offset; - }); - }); - }, - - dependentArrayWillChange(dependentArray, index, removedCount, addedCount) { - if (this.suspended) { return; } - - var removedItem = this.callbacks.removedItem; - var changeMeta; - var guid = guidFor(dependentArray); - var dependentKey = this.dependentKeysByGuid[guid]; - var itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || []; - var length = get(dependentArray, 'length'); - var normalizedIndex = normalizeIndex(index, length, 0); - var normalizedRemoveCount = normalizeRemoveCount(normalizedIndex, length, removedCount); - var item, itemIndex, sliceIndex, observerContexts; - - observerContexts = this.trackRemove(dependentKey, normalizedIndex, normalizedRemoveCount); - - function removeObservers(propertyKey) { - observerContexts[sliceIndex].destroyed = true; - removeBeforeObserver(item, propertyKey, this, observerContexts[sliceIndex].beforeObserver); - removeObserver(item, propertyKey, this, observerContexts[sliceIndex].observer); - } - - for (sliceIndex = normalizedRemoveCount - 1; sliceIndex >= 0; --sliceIndex) { - itemIndex = normalizedIndex + sliceIndex; - if (itemIndex >= length) { break; } - - item = dependentArray.objectAt(itemIndex); - - itemPropertyKeys.forEach(removeObservers, this); - - changeMeta = new ChangeMeta(dependentArray, item, itemIndex, this.instanceMeta.propertyName, this.cp, normalizedRemoveCount); - this.setValue(removedItem.call( - this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); - } - this.callbacks.flushedChanges.call(this.instanceMeta.context, this.getValue(), this.instanceMeta.sugarMeta); - }, - - dependentArrayDidChange(dependentArray, index, removedCount, addedCount) { - if (this.suspended) { return; } - - var addedItem = this.callbacks.addedItem; - var guid = guidFor(dependentArray); - var dependentKey = this.dependentKeysByGuid[guid]; - var observerContexts = new Array(addedCount); - var itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey]; - var length = get(dependentArray, 'length'); - var normalizedIndex = normalizeIndex(index, length, addedCount); - var endIndex = normalizedIndex + addedCount; - var changeMeta, observerContext; - - dependentArray.slice(normalizedIndex, endIndex).forEach(function (item, sliceIndex) { - if (itemPropertyKeys) { - observerContext = this.createPropertyObserverContext(dependentArray, normalizedIndex + sliceIndex, - this.trackedArraysByGuid[dependentKey]); - observerContexts[sliceIndex] = observerContext; - - itemPropertyKeys.forEach(function (propertyKey) { - addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); - addObserver(item, propertyKey, this, observerContext.observer); - }, this); - } - - changeMeta = new ChangeMeta(dependentArray, item, normalizedIndex + sliceIndex, this.instanceMeta.propertyName, this.cp, addedCount); - this.setValue(addedItem.call( - this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); - }, this); - this.callbacks.flushedChanges.call(this.instanceMeta.context, this.getValue(), this.instanceMeta.sugarMeta); - this.trackAdd(dependentKey, normalizedIndex, observerContexts); - }, - - itemPropertyWillChange(obj, keyName, array, observerContext) { - var guid = guidFor(obj); - - if (!this.changedItems[guid]) { - this.changedItems[guid] = { - array: array, - observerContext: observerContext, - obj: obj, - previousValues: {} - }; - } - - ++this.changedItemCount; - this.changedItems[guid].previousValues[keyName] = get(obj, keyName); - }, - - itemPropertyDidChange(obj, keyName, array, observerContext) { - if (--this.changedItemCount === 0) { - this.flushChanges(); - } - }, - - flushChanges() { - var changedItems = this.changedItems; - var key, c, changeMeta; - - for (key in changedItems) { - c = changedItems[key]; - if (c.observerContext.destroyed) { continue; } - - this.updateIndexes(c.observerContext.trackedArray, c.observerContext.dependentArray); - - changeMeta = new ChangeMeta(c.array, c.obj, c.observerContext.index, this.instanceMeta.propertyName, this.cp, changedItems.length, c.previousValues); - this.setValue( - this.callbacks.removedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); - this.setValue( - this.callbacks.addedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); - } - - this.changedItems = {}; - this.callbacks.flushedChanges.call(this.instanceMeta.context, this.getValue(), this.instanceMeta.sugarMeta); - } -}; - -function normalizeIndex(index, length, newItemsOffset) { - if (index < 0) { - return Math.max(0, length + index); - } else if (index < length) { - return index; - } else { // index > length - return Math.min(length - newItemsOffset, index); - } -} - -function normalizeRemoveCount(index, length, removedCount) { - return Math.min(removedCount, length - index); -} - -function ChangeMeta(dependentArray, item, index, propertyName, property, changedCount, previousValues) { - this.arrayChanged = dependentArray; - this.index = index; - this.item = item; - this.propertyName = propertyName; - this.property = property; - this.changedCount = changedCount; - - if (previousValues) { - // previous values only available for item property changes - this.previousValues = previousValues; - } -} - -function addItems(dependentArray, callbacks, cp, propertyName, meta) { - dependentArray.forEach(function (item, index) { - meta.setValue(callbacks.addedItem.call( - this, meta.getValue(), item, new ChangeMeta(dependentArray, item, index, propertyName, cp, dependentArray.length), meta.sugarMeta)); - }, this); - callbacks.flushedChanges.call(this, meta.getValue(), meta.sugarMeta); -} - -function reset(cp, propertyName) { - var hadMeta = cp._hasInstanceMeta(this, propertyName); - var meta = cp._instanceMeta(this, propertyName); - - if (hadMeta) { meta.setValue(cp.resetValue(meta.getValue())); } - - if (cp.options.initialize) { - cp.options.initialize.call(this, meta.getValue(), { - property: cp, - propertyName: propertyName - }, meta.sugarMeta); - } -} - -function partiallyRecomputeFor(obj, dependentKey) { - if (arrayBracketPattern.test(dependentKey)) { - return false; - } - - var value = get(obj, dependentKey); - return EmberArray.detect(value); -} - -function ReduceComputedPropertyInstanceMeta(context, propertyName, initialValue) { - this.context = context; - this.propertyName = propertyName; - var contextMeta = metaFor(context); - var contextCache = contextMeta.cache; - if (!contextCache) { contextCache = contextMeta.cache = {}; } - this.cache = contextCache; - this.dependentArrays = {}; - this.sugarMeta = {}; - this.initialValue = initialValue; -} - -ReduceComputedPropertyInstanceMeta.prototype = { - getValue() { - var value = cacheGet(this.cache, this.propertyName); - - if (value !== undefined) { - return value; - } else { - return this.initialValue; - } - }, - - setValue(newValue, triggerObservers) { - // This lets sugars force a recomputation, handy for very simple - // implementations of eg max. - if (newValue === cacheGet(this.cache, this.propertyName)) { - return; - } - - if (triggerObservers) { - propertyWillChange(this.context, this.propertyName); - } - - if (newValue === undefined) { - cacheRemove(this.cache, this.propertyName); - } else { - cacheSet(this.cache, this.propertyName, newValue); - } - - if (triggerObservers) { - propertyDidChange(this.context, this.propertyName); - } - } -}; - -/** - A computed property whose dependent keys are arrays and which is updated with - "one at a time" semantics. - - @class ReduceComputedProperty - @namespace Ember - @extends Ember.ComputedProperty - @constructor - @private -*/ - -export { ReduceComputedProperty }; // TODO: default export - -function ReduceComputedProperty(options) { - var cp = this; - - // use options._suppressDeprecation to allow us to deprecate - // arrayComputed and reduceComputed themselves, but not the - // default internal macros which will be reimplemented as plain - // array methods - if (this._isArrayComputed) { - Ember.deprecate( - 'Ember.arrayComputed is deprecated. Replace it with plain array methods', - options._suppressDeprecation - ); - } else { - Ember.deprecate( - 'Ember.reduceComputed is deprecated. Replace it with plain array methods', - options._suppressDeprecation - ); - } - - this.options = options; - this._dependentKeys = null; - this._cacheable = true; - // A map of dependentKey -> [itemProperty, ...] that tracks what properties of - // items in the array we must track to update this property. - this._itemPropertyKeys = {}; - this._previousItemPropertyKeys = {}; - - this.readOnly(); - - this.recomputeOnce = function(propertyName) { - // What we really want to do is coalesce by . - // We need a form of `scheduleOnce` that accepts an arbitrary token to - // coalesce by, in addition to the target and method. - run.once(this, recompute, propertyName); - }; - - var recompute = function(propertyName) { - var meta = cp._instanceMeta(this, propertyName); - var callbacks = cp._callbacks(); - - reset.call(this, cp, propertyName); - - meta.dependentArraysObserver.suspendArrayObservers(function () { - cp._dependentKeys.forEach(function (dependentKey) { - Ember.assert( - 'dependent array ' + dependentKey + ' must be an `Ember.Array`. ' + - 'If you are not extending arrays, you will need to wrap native arrays with `Ember.A`', - !(Array.isArray(get(this, dependentKey)) && !EmberArray.detect(get(this, dependentKey)))); - - if (!partiallyRecomputeFor(this, dependentKey)) { return; } - - var dependentArray = get(this, dependentKey); - var previousDependentArray = meta.dependentArrays[dependentKey]; - - if (dependentArray === previousDependentArray) { - - // The array may be the same, but our item property keys may have - // changed, so we set them up again. We can't easily tell if they've - // changed: the array may be the same object, but with different - // contents. - if (cp._previousItemPropertyKeys[dependentKey]) { - meta.dependentArraysObserver.teardownPropertyObservers(dependentKey, cp._previousItemPropertyKeys[dependentKey]); - delete cp._previousItemPropertyKeys[dependentKey]; - meta.dependentArraysObserver.setupPropertyObservers(dependentKey, cp._itemPropertyKeys[dependentKey]); - } - } else { - meta.dependentArrays[dependentKey] = dependentArray; - - if (previousDependentArray) { - meta.dependentArraysObserver.teardownObservers(previousDependentArray, dependentKey); - } - - if (dependentArray) { - meta.dependentArraysObserver.setupObservers(dependentArray, dependentKey); - } - } - }, this); - }, this); - - cp._dependentKeys.forEach(function(dependentKey) { - if (!partiallyRecomputeFor(this, dependentKey)) { return; } - - var dependentArray = get(this, dependentKey); - - if (dependentArray) { - addItems.call(this, dependentArray, callbacks, cp, propertyName, meta); - } - }, this); - }; - - - this._getter = function (propertyName) { - Ember.assert('Computed reduce values require at least one dependent key', cp._dependentKeys); - - recompute.call(this, propertyName); - - return cp._instanceMeta(this, propertyName).getValue(); - }; -} - -ReduceComputedProperty.prototype = Object.create(ComputedProperty.prototype); - -function defaultCallback(computedValue) { - return computedValue; -} - -ReduceComputedProperty.prototype._callbacks = function () { - if (!this.callbacks) { - var options = this.options; - - this.callbacks = { - removedItem: options.removedItem || defaultCallback, - addedItem: options.addedItem || defaultCallback, - flushedChanges: options.flushedChanges || defaultCallback - }; - } - - return this.callbacks; -}; - -ReduceComputedProperty.prototype._hasInstanceMeta = function (context, propertyName) { - var contextMeta = context.__ember_meta__; - var cacheMeta = contextMeta && contextMeta.cacheMeta; - return !!(cacheMeta && cacheMeta[propertyName]); -}; - -ReduceComputedProperty.prototype._instanceMeta = function (context, propertyName) { - var contextMeta = context.__ember_meta__; - var cacheMeta = contextMeta.cacheMeta; - var meta = cacheMeta && cacheMeta[propertyName]; - - if (!cacheMeta) { - cacheMeta = contextMeta.cacheMeta = {}; - } - if (!meta) { - meta = cacheMeta[propertyName] = new ReduceComputedPropertyInstanceMeta(context, propertyName, this.initialValue()); - meta.dependentArraysObserver = new DependentArraysObserver(this._callbacks(), this, meta, context, propertyName, meta.sugarMeta); - } - - return meta; -}; - -ReduceComputedProperty.prototype.initialValue = function () { - if (typeof this.options.initialValue === 'function') { - return this.options.initialValue(); - } else { - return this.options.initialValue; - } -}; - -ReduceComputedProperty.prototype.resetValue = function (value) { - return this.initialValue(); -}; - -ReduceComputedProperty.prototype.itemPropertyKey = function (dependentArrayKey, itemPropertyKey) { - this._itemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey] || []; - this._itemPropertyKeys[dependentArrayKey].push(itemPropertyKey); -}; - -ReduceComputedProperty.prototype.clearItemPropertyKeys = function (dependentArrayKey) { - if (this._itemPropertyKeys[dependentArrayKey]) { - this._previousItemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey]; - this._itemPropertyKeys[dependentArrayKey] = []; - } -}; - -ReduceComputedProperty.prototype.property = function () { - var cp = this; - var args = a_slice.call(arguments); - var propertyArgs = {}; - var match, dependentArrayKey; - - args.forEach(function (dependentKey) { - if (doubleEachPropertyPattern.test(dependentKey)) { - throw new EmberError('Nested @each properties not supported: ' + dependentKey); - } else if (match = eachPropertyPattern.exec(dependentKey)) { - dependentArrayKey = match[1]; - - var itemPropertyKeyPattern = match[2]; - var addItemPropertyKey = function (itemPropertyKey) { - cp.itemPropertyKey(dependentArrayKey, itemPropertyKey); - }; - - expandProperties(itemPropertyKeyPattern, addItemPropertyKey); - propertyArgs[guidFor(dependentArrayKey)] = dependentArrayKey; - } else { - propertyArgs[guidFor(dependentKey)] = dependentKey; - } - }); - - var propertyArgsToArray = []; - for (var guid in propertyArgs) { - propertyArgsToArray.push(propertyArgs[guid]); - } - - return ComputedProperty.prototype.property.apply(this, propertyArgsToArray); -}; - -/** - Creates a computed property which operates on dependent arrays and - is updated with "one at a time" semantics. When items are added or - removed from the dependent array(s) a reduce computed only operates - on the change instead of re-evaluating the entire array. - - If there are more than one arguments the first arguments are - considered to be dependent property keys. The last argument is - required to be an options object. The options object can have the - following four properties: - - `initialValue` - A value or function that will be used as the initial - value for the computed. If this property is a function the result of calling - the function will be used as the initial value. This property is required. - - `initialize` - An optional initialize function. Typically this will be used - to set up state on the instanceMeta object. - - `removedItem` - A function that is called each time an element is removed - from the array. - - `addedItem` - A function that is called each time an element is added to - the array. - - - The `initialize` function has the following signature: - - ```javascript - function(initialValue, changeMeta, instanceMeta) - ``` - - `initialValue` - The value of the `initialValue` property from the - options object. - - `changeMeta` - An object which contains meta information about the - computed. It contains the following properties: - - - `property` the computed property - - `propertyName` the name of the property on the object - - `instanceMeta` - An object that can be used to store meta - information needed for calculating your computed. For example a - unique computed might use this to store the number of times a given - element is found in the dependent array. - - - The `removedItem` and `addedItem` functions both have the following signature: - - ```javascript - function(accumulatedValue, item, changeMeta, instanceMeta) - ``` - - `accumulatedValue` - The value returned from the last time - `removedItem` or `addedItem` was called or `initialValue`. - - `item` - the element added or removed from the array - - `changeMeta` - An object which contains meta information about the - change. It contains the following properties: - - - `property` the computed property - - `propertyName` the name of the property on the object - - `index` the index of the added or removed item - - `item` the added or removed item: this is exactly the same as - the second arg - - `arrayChanged` the array that triggered the change. Can be - useful when depending on multiple arrays. - - For property changes triggered on an item property change (when - depKey is something like `someArray.@each.someProperty`), - `changeMeta` will also contain the following property: - - - `previousValues` an object whose keys are the properties that changed on - the item, and whose values are the item's previous values. - - `previousValues` is important Ember coalesces item property changes via - Ember.run.once. This means that by the time removedItem gets called, item has - the new values, but you may need the previous value (eg for sorting & - filtering). - - `instanceMeta` - An object that can be used to store meta - information needed for calculating your computed. For example a - unique computed might use this to store the number of times a given - element is found in the dependent array. - - The `removedItem` and `addedItem` functions should return the accumulated - value. It is acceptable to not return anything (ie return undefined) - to invalidate the computation. This is generally not a good idea for - arrayComputed but it's used in eg max and min. - - Note that observers will be fired if either of these functions return a value - that differs from the accumulated value. When returning an object that - mutates in response to array changes, for example an array that maps - everything from some other array (see `Ember.computed.map`), it is usually - important that the *same* array be returned to avoid accidentally triggering observers. - - Example - - ```javascript - Ember.computed.max = function(dependentKey) { - return Ember.reduceComputed(dependentKey, { - initialValue: -Infinity, - - addedItem: function(accumulatedValue, item, changeMeta, instanceMeta) { - return Math.max(accumulatedValue, item); - }, - - removedItem: function(accumulatedValue, item, changeMeta, instanceMeta) { - if (item < accumulatedValue) { - return accumulatedValue; - } - } - }); - }; - ``` - - Dependent keys may refer to `@this` to observe changes to the object itself, - which must be array-like, rather than a property of the object. This is - mostly useful for array proxies, to ensure objects are retrieved via - `objectAtContent`. This is how you could sort items by properties defined on an item controller. - - Example - - ```javascript - App.PeopleController = Ember.ArrayController.extend({ - itemController: 'person', - - sortedPeople: Ember.computed.sort('@this.@each.reversedName', function(personA, personB) { - // `reversedName` isn't defined on Person, but we have access to it via - // the item controller App.PersonController. If we'd used - // `content.@each.reversedName` above, we would be getting the objects - // directly and not have access to `reversedName`. - // - var reversedNameA = get(personA, 'reversedName'); - var reversedNameB = get(personB, 'reversedName'); - - return Ember.compare(reversedNameA, reversedNameB); - }) - }); - - App.PersonController = Ember.ObjectController.extend({ - reversedName: function() { - return reverse(get(this, 'name')); - }.property('name') - }); - ``` - - Dependent keys whose values are not arrays are treated as regular - dependencies: when they change, the computed property is completely - recalculated. It is sometimes useful to have dependent arrays with similar - semantics. Dependent keys which end in `.[]` do not use "one at a time" - semantics. When an item is added or removed from such a dependency, the - computed property is completely recomputed. - - When the computed property is completely recomputed, the `accumulatedValue` - is discarded, it starts with `initialValue` again, and each item is passed - to `addedItem` in turn. - - Example - - ```javascript - Ember.Object.extend({ - // When `string` is changed, `computed` is completely recomputed. - string: 'a string', - - // When an item is added to `array`, `addedItem` is called. - array: [], - - // When an item is added to `anotherArray`, `computed` is completely - // recomputed. - anotherArray: [], - - computed: Ember.reduceComputed('string', 'array', 'anotherArray.[]', { - addedItem: addedItemCallback, - removedItem: removedItemCallback - }) - }); - ``` - - @method reduceComputed - @for Ember - @param {String} [dependentKeys*] - @param {Object} options - @return {Ember.ComputedProperty} - @deprecated - @public -*/ -export function reduceComputed(options) { - var args; - - if (arguments.length > 1) { - args = a_slice.call(arguments, 0, -1); - options = a_slice.call(arguments, -1)[0]; - } - - if (typeof options !== 'object') { - throw new EmberError('Reduce Computed Property declared without an options hash'); - } - - if (!('initialValue' in options)) { - throw new EmberError('Reduce Computed Property declared without an initial value'); - } - - var cp = new ReduceComputedProperty(options); - - if (args) { - cp.property.apply(cp, args); - } - - return cp; -} diff --git a/packages/ember-runtime/lib/computed/reduce_computed_macros.js b/packages/ember-runtime/lib/computed/reduce_computed_macros.js index 755669f0f0b..b8c599cb4b5 100644 --- a/packages/ember-runtime/lib/computed/reduce_computed_macros.js +++ b/packages/ember-runtime/lib/computed/reduce_computed_macros.js @@ -5,18 +5,47 @@ import Ember from 'ember-metal/core'; // Ember.assert import { get } from 'ember-metal/property_get'; -import { - guidFor -} from 'ember-metal/utils'; import EmberError from 'ember-metal/error'; -import run from 'ember-metal/run_loop'; -import { addObserver } from 'ember-metal/observer'; -import { arrayComputed } from 'ember-runtime/computed/array_computed'; -import { reduceComputed } from 'ember-runtime/computed/reduce_computed'; -import SubArray from 'ember-runtime/system/subarray'; +import { ComputedProperty, computed } from 'ember-metal/computed'; +import { addObserver, removeObserver } from 'ember-metal/observer'; import compare from 'ember-runtime/compare'; +import { isArray } from 'ember-runtime/utils'; -var a_slice = [].slice; +function reduceMacro(dependentKey, callback, initialValue) { + return computed(`${dependentKey}.[]`, function() { + return get(this, dependentKey).reduce(callback, initialValue); + }).readOnly(); +} + +function arrayMacro(dependentKey, callback) { + // This is a bit ugly + var propertyName; + if (/@each/.test(dependentKey)) { + propertyName = dependentKey.replace(/\.@each.*$/, ''); + } else { + propertyName = dependentKey; + dependentKey += '.[]'; + } + + return computed(dependentKey, function() { + var value = get(this, propertyName); + if (isArray(value)) { + return Ember.A(callback(value)); + } else { + return Ember.A(); + } + }).readOnly(); +} + +function multiArrayMacro(dependentKeys, callback) { + var args = dependentKeys.map(key => `${key}.[]`); + + args.push(function() { + return Ember.A(callback.call(this, dependentKeys)); + }); + + return computed.apply(this, args).readOnly(); +} /** A computed property that returns the sum of the value @@ -30,18 +59,7 @@ var a_slice = [].slice; @public */ export function sum(dependentKey) { - return reduceComputed(dependentKey, { - _suppressDeprecation: true, - initialValue: 0, - - addedItem(accumulatedValue, item, changeMeta, instanceMeta) { - return accumulatedValue + item; - }, - - removedItem(accumulatedValue, item, changeMeta, instanceMeta) { - return accumulatedValue - item; - } - }); + return reduceMacro(dependentKey, (sum, item) => sum + item, 0); } /** @@ -79,20 +97,7 @@ export function sum(dependentKey) { @public */ export function max(dependentKey) { - return reduceComputed(dependentKey, { - _suppressDeprecation: true, - initialValue: -Infinity, - - addedItem(accumulatedValue, item, changeMeta, instanceMeta) { - return Math.max(accumulatedValue, item); - }, - - removedItem(accumulatedValue, item, changeMeta, instanceMeta) { - if (item < accumulatedValue) { - return accumulatedValue; - } - } - }); + return reduceMacro(dependentKey, (max, item) => Math.max(max, item), -Infinity); } /** @@ -130,21 +135,7 @@ export function max(dependentKey) { @public */ export function min(dependentKey) { - return reduceComputed(dependentKey, { - _suppressDeprecation: true, - - initialValue: Infinity, - - addedItem(accumulatedValue, item, changeMeta, instanceMeta) { - return Math.min(accumulatedValue, item); - }, - - removedItem(accumulatedValue, item, changeMeta, instanceMeta) { - if (item > accumulatedValue) { - return accumulatedValue; - } - } - }); + return reduceMacro(dependentKey, (min, item) => Math.min(min, item), Infinity); } /** @@ -182,24 +173,7 @@ export function min(dependentKey) { @public */ export function map(dependentKey, callback) { - Ember.assert('Ember.computed.map expects a callback function for its second argument, ' + - 'perhaps you meant to use "mapBy"', typeof callback === 'function'); - - var options = { - _suppressDeprecation: true, - - addedItem(array, item, changeMeta, instanceMeta) { - var mapped = callback.call(this, item, changeMeta.index); - array.insertAt(changeMeta.index, mapped); - return array; - }, - removedItem(array, item, changeMeta, instanceMeta) { - array.removeAt(changeMeta.index, 1); - return array; - } - }; - - return arrayComputed(dependentKey, options); + return arrayMacro(dependentKey, value => value.map(callback)); } /** @@ -236,8 +210,7 @@ export function mapBy(dependentKey, propertyKey) { Ember.assert('Ember.computed.mapBy expects a property string for its second argument, ' + 'perhaps you meant to use "map"', typeof propertyKey === 'string'); - var callback = function(item) { return get(item, propertyKey); }; - return map(dependentKey + '.@each.' + propertyKey, callback); + return map(`${dependentKey}.@each.${propertyKey}`, item => get(item, propertyKey)); } /** @@ -248,7 +221,10 @@ export function mapBy(dependentKey, propertyKey) { @param propertyKey @public */ -export var mapProperty = mapBy; +export function mapProperty() { + Ember.deprecate('Ember.computed.mapProperty is deprecated. Please use Ember.computed.mapBy.'); + return mapBy.apply(this, arguments); +} /** Filters the array by the callback. @@ -288,36 +264,7 @@ export var mapProperty = mapBy; @public */ export function filter(dependentKey, callback) { - var options = { - _suppressDeprecation: true, - - initialize(array, changeMeta, instanceMeta) { - instanceMeta.filteredArrayIndexes = new SubArray(); - }, - - addedItem(array, item, changeMeta, instanceMeta) { - var match = !!callback.call(this, item, changeMeta.index, changeMeta.arrayChanged); - var filterIndex = instanceMeta.filteredArrayIndexes.addItem(changeMeta.index, match); - - if (match) { - array.insertAt(filterIndex, item); - } - - return array; - }, - - removedItem(array, item, changeMeta, instanceMeta) { - var filterIndex = instanceMeta.filteredArrayIndexes.removeItem(changeMeta.index); - - if (filterIndex > -1) { - array.removeAt(filterIndex); - } - - return array; - } - }; - - return arrayComputed(dependentKey, options); + return arrayMacro(dependentKey, value => value.filter(callback)); } /** @@ -360,7 +307,7 @@ export function filterBy(dependentKey, propertyKey, value) { }; } - return filter(dependentKey + '.@each.' + propertyKey, callback); + return filter(`${dependentKey}.@each.${propertyKey}`, callback); } /** @@ -372,7 +319,10 @@ export function filterBy(dependentKey, propertyKey, value) { @deprecated Use `Ember.computed.filterBy` instead @public */ -export var filterProperty = filterBy; +export function filterProperty() { + Ember.deprecate('Ember.computed.filterProperty is deprecated. Please use Ember.computed.filterBy.'); + return filterBy.apply(this, arguments); +} /** A computed property which returns a new array with all the unique @@ -404,41 +354,23 @@ export var filterProperty = filterBy; unique elements from the dependent array @public */ -export function uniq() { - var args = a_slice.call(arguments); - - args.push({ - _suppressDeprecation: true, - - initialize(array, changeMeta, instanceMeta) { - instanceMeta.itemCounts = {}; - }, - - addedItem(array, item, changeMeta, instanceMeta) { - var guid = guidFor(item); - - if (!instanceMeta.itemCounts[guid]) { - instanceMeta.itemCounts[guid] = 1; - array.pushObject(item); - } else { - ++instanceMeta.itemCounts[guid]; - } - return array; - }, - - removedItem(array, item, _, instanceMeta) { - var guid = guidFor(item); - var itemCounts = instanceMeta.itemCounts; - - if (--itemCounts[guid] === 0) { - array.removeObject(item); +export function uniq(...args) { + return multiArrayMacro(args, function(dependentKeys) { + var uniq = Ember.A(); + + dependentKeys.forEach(dependentKey => { + var value = get(this, dependentKey); + if (isArray(value)) { + value.forEach(item => { + if (uniq.indexOf(item) === -1) { + uniq.push(item); + } + }); } + }); - return array; - } + return uniq; }); - - return arrayComputed.apply(null, args); } /** @@ -477,66 +409,37 @@ export var union = uniq; duplicated elements from the dependent arrays @public */ -export function intersect() { - var args = a_slice.call(arguments); - - args.push({ - _suppressDeprecation: true, - - initialize(array, changeMeta, instanceMeta) { - instanceMeta.itemCounts = {}; - }, - - addedItem(array, item, changeMeta, instanceMeta) { - var itemGuid = guidFor(item); - var dependentGuid = guidFor(changeMeta.arrayChanged); - var numberOfDependentArrays = changeMeta.property._dependentKeys.length; - var itemCounts = instanceMeta.itemCounts; - - if (!itemCounts[itemGuid]) { - itemCounts[itemGuid] = {}; - } - - if (itemCounts[itemGuid][dependentGuid] === undefined) { - itemCounts[itemGuid][dependentGuid] = 0; - } - - if (++itemCounts[itemGuid][dependentGuid] === 1 && - numberOfDependentArrays === Object.keys(itemCounts[itemGuid]).length) { - array.addObject(item); - } - - return array; - }, - - removedItem(array, item, changeMeta, instanceMeta) { - var itemGuid = guidFor(item); - var dependentGuid = guidFor(changeMeta.arrayChanged); - var numberOfArraysItemAppearsIn; - var itemCounts = instanceMeta.itemCounts; +export function intersect(...args) { + return multiArrayMacro(args, function(dependentKeys) { + var arrays = dependentKeys.map(dependentKey => { + var array = get(this, dependentKey); + + return isArray(array) ? array : []; + }); + + var results = arrays.pop().filter(candidate => { + for (var i = 0; i < arrays.length; i++) { + var found = false; + var array = arrays[i]; + for (var j = 0; j < array.length; j++) { + if (array[j] === candidate) { + found = true; + break; + } + } - if (itemCounts[itemGuid][dependentGuid] === undefined) { - itemCounts[itemGuid][dependentGuid] = 0; + if (found === false) { return false; } } - if (--itemCounts[itemGuid][dependentGuid] === 0) { - delete itemCounts[itemGuid][dependentGuid]; - numberOfArraysItemAppearsIn = Object.keys(itemCounts[itemGuid]).length; + return true; + }); - if (numberOfArraysItemAppearsIn === 0) { - delete itemCounts[itemGuid]; - } - - array.removeObject(item); - } - return array; - } + return Ember.A(results); }); - - return arrayComputed.apply(null, args); } + /** A computed property which returns a new array with all the properties from the first dependent array that are not in the second @@ -574,83 +477,17 @@ export function setDiff(setAProperty, setBProperty) { throw new EmberError('setDiff requires exactly two dependent arrays.'); } - return arrayComputed(setAProperty, setBProperty, { - _suppressDeprecation: true, - - addedItem(array, item, changeMeta, instanceMeta) { - var setA = get(this, setAProperty); - var setB = get(this, setBProperty); - - if (changeMeta.arrayChanged === setA) { - if (!setB.contains(item)) { - array.addObject(item); - } - } else { - array.removeObject(item); - } - - return array; - }, - - removedItem(array, item, changeMeta, instanceMeta) { - var setA = get(this, setAProperty); - var setB = get(this, setBProperty); - - if (changeMeta.arrayChanged === setB) { - if (setA.contains(item)) { - array.addObject(item); - } - } else { - array.removeObject(item); - } - - return array; - } - }); -} - -function binarySearch(array, item, low, high) { - var mid, midItem, res, guidMid, guidItem; - - if (arguments.length < 4) { - high = get(array, 'length'); - } - - if (arguments.length < 3) { - low = 0; - } - - if (low === high) { - return low; - } - - mid = low + Math.floor((high - low) / 2); - midItem = array.objectAt(mid); + return computed(`${setAProperty}.[]`, `${setBProperty}.[]`, function() { + var setA = this.get(setAProperty); + var setB = this.get(setBProperty); - guidMid = guidFor(midItem); - guidItem = guidFor(item); + if (!isArray(setA)) { return Ember.A(); } + if (!isArray(setB)) { return Ember.A(setA); } - if (guidMid === guidItem) { - return mid; - } - - res = this.order(midItem, item); - - if (res === 0) { - res = guidMid < guidItem ? -1 : 1; - } - - - if (res < 0) { - return this.binarySearch(array, item, mid+1, high); - } else if (res > 0) { - return this.binarySearch(array, item, low, mid); - } - - return mid; + return setA.filter(x => setB.indexOf(x) === -1); + }).readOnly(); } - /** A computed property which returns a new array with all the properties from the first dependent array sorted based on a property @@ -728,146 +565,58 @@ export function sort(itemsKey, sortDefinition) { } function customSort(itemsKey, comparator) { - return arrayComputed(itemsKey, { - _suppressDeprecation: true, - - initialize(array, changeMeta, instanceMeta) { - instanceMeta.order = comparator; - instanceMeta.binarySearch = binarySearch; - instanceMeta.waitingInsertions = []; - instanceMeta.insertWaiting = function() { - var index, item; - var waiting = instanceMeta.waitingInsertions; - instanceMeta.waitingInsertions = []; - for (var i=0; i value.slice().sort(comparator)); } +// This one needs to dynamically set up and tear down observers on the itemsKey +// depending on the sortProperties function propertySort(itemsKey, sortPropertiesKey) { - return arrayComputed(itemsKey, { - _suppressDeprecation: true, - - initialize(array, changeMeta, instanceMeta) { - function setupSortProperties() { - var sortPropertyDefinitions = get(this, sortPropertiesKey); - var sortProperties = instanceMeta.sortProperties = []; - var sortPropertyAscending = instanceMeta.sortPropertyAscending = {}; - var sortProperty, idx, asc; - - Ember.assert('Cannot sort: \'' + sortPropertiesKey + '\' is not an array.', - Array.isArray(sortPropertyDefinitions)); - - changeMeta.property.clearItemPropertyKeys(itemsKey); - - sortPropertyDefinitions.forEach(function (sortPropertyDefinition) { - if ((idx = sortPropertyDefinition.indexOf(':')) !== -1) { - sortProperty = sortPropertyDefinition.substring(0, idx); - asc = sortPropertyDefinition.substring(idx+1).toLowerCase() !== 'desc'; - } else { - sortProperty = sortPropertyDefinition; - asc = true; - } + var cp = new ComputedProperty(function(key) { + function didChange() { + this.notifyPropertyChange(key); + } - sortProperties.push(sortProperty); - sortPropertyAscending[sortProperty] = asc; - changeMeta.property.itemPropertyKey(itemsKey, sortProperty); - }); + var items = itemsKey === '@this' ? this : get(this, itemsKey); + var sortProperties = get(this, sortPropertiesKey); - this.addObserver(sortPropertiesKey + '.@each', this, updateSortPropertiesOnce); - } + // TODO: Ideally we'd only do this if things have changed + if (cp._sortPropObservers) { + cp._sortPropObservers.forEach(args => removeObserver.apply(null, args)); + } - function updateSortPropertiesOnce() { - run.once(this, updateSortProperties, changeMeta.propertyName); - } + cp._sortPropObservers = []; - function updateSortProperties(propertyName) { - setupSortProperties.call(this); - changeMeta.property.recomputeOnce.call(this, propertyName); - } + if (!isArray(sortProperties)) { return items; } - addObserver(this, sortPropertiesKey, updateSortPropertiesOnce); - setupSortProperties.call(this); + // Normalize properties + var normalizedSort = sortProperties.map(p => { + let [prop, direction] = p.split(':'); + direction = direction || 'asc'; - instanceMeta.order = function (itemA, itemB) { - var sortProperty, result, asc; - var keyA = this.keyFor(itemA); - var keyB = this.keyFor(itemB); + return [prop, direction]; + }); - for (var i = 0; i < this.sortProperties.length; ++i) { - sortProperty = this.sortProperties[i]; + // TODO: Ideally we'd only do this if things have changed + // Add observers + normalizedSort.forEach(prop => { + var args = [this, `${itemsKey}.@each.${prop[0]}`, didChange]; + cp._sortPropObservers.push(args); + addObserver.apply(null, args); + }); - result = compare(keyA[sortProperty], keyB[sortProperty]); + return Ember.A(items.slice().sort((itemA, itemB) => { - if (result !== 0) { - asc = this.sortPropertyAscending[sortProperty]; - return asc ? result : (-1 * result); - } + for (var i = 0; i < normalizedSort.length; ++i) { + var [prop, direction] = normalizedSort[i]; + var result = compare(get(itemA, prop), get(itemB, prop)); + if (result !== 0) { + return (direction === 'desc') ? (-1 * result) : result; } + } - return 0; - }; - - instanceMeta.binarySearch = binarySearch; - setupKeyCache(instanceMeta); - }, - - addedItem(array, item, changeMeta, instanceMeta) { - var index = instanceMeta.binarySearch(array, item); - array.insertAt(index, item); - return array; - }, - - removedItem(array, item, changeMeta, instanceMeta) { - var index = instanceMeta.binarySearch(array, item); - array.removeAt(index); - instanceMeta.dropKeyFor(item); - return array; - } + return 0; + })); }); -} - -function setupKeyCache(instanceMeta) { - instanceMeta.keyFor = function(item) { - var guid = guidFor(item); - if (this.keyCache[guid]) { - return this.keyCache[guid]; - } - var sortProperty; - var key = {}; - for (var i = 0; i < this.sortProperties.length; ++i) { - sortProperty = this.sortProperties[i]; - key[sortProperty] = get(item, sortProperty); - } - return this.keyCache[guid] = key; - }; - - instanceMeta.dropKeyFor = function(item) { - var guid = guidFor(item); - this.keyCache[guid] = null; - }; - instanceMeta.keyCache = {}; + return cp.property(`${itemsKey}.[]`, `${sortPropertiesKey}.[]`).readOnly(); } diff --git a/packages/ember-runtime/lib/main.js b/packages/ember-runtime/lib/main.js index 3e985803be5..f5e8b964440 100644 --- a/packages/ember-runtime/lib/main.js +++ b/packages/ember-runtime/lib/main.js @@ -44,15 +44,6 @@ import TargetActionSupport from 'ember-runtime/mixins/target_action_support'; import Evented from 'ember-runtime/mixins/evented'; import PromiseProxyMixin from 'ember-runtime/mixins/promise_proxy'; import SortableMixin from 'ember-runtime/mixins/sortable'; -import { - arrayComputed, - ArrayComputedProperty -} from 'ember-runtime/computed/array_computed'; - -import { - reduceComputed, - ReduceComputedProperty -} from 'ember-runtime/computed/reduce_computed'; import { sum, @@ -112,11 +103,6 @@ Ember.PromiseProxyMixin = PromiseProxyMixin; Ember.Observable = Observable; -Ember.arrayComputed = arrayComputed; -Ember.ArrayComputedProperty = ArrayComputedProperty; -Ember.reduceComputed = reduceComputed; -Ember.ReduceComputedProperty = ReduceComputedProperty; - Ember.typeOf = typeOf; Ember.isArray = Array.isArray; diff --git a/packages/ember-runtime/tests/computed/reduce_computed_macros_test.js b/packages/ember-runtime/tests/computed/reduce_computed_macros_test.js index 48c9232ff1c..4a67a6710df 100644 --- a/packages/ember-runtime/tests/computed/reduce_computed_macros_test.js +++ b/packages/ember-runtime/tests/computed/reduce_computed_macros_test.js @@ -4,390 +4,310 @@ import setProperties from 'ember-metal/set_properties'; import ObjectProxy from 'ember-runtime/system/object_proxy'; import { get } from 'ember-metal/property_get'; import { set } from 'ember-metal/property_set'; -import run from 'ember-metal/run_loop'; import { addObserver } from 'ember-metal/observer'; +import { observer } from 'ember-metal/mixin'; import { - beginPropertyChanges, - endPropertyChanges -} from 'ember-metal/property_events'; -import { observer, Mixin } from 'ember-metal/mixin'; -import { - sum as computedSum, - min as computedMin, - max as computedMax, - map as computedMap, - sort as computedSort, - setDiff as computedSetDiff, - mapBy as computedMapBy, - filter as computedFilter, - filterBy as computedFilterBy, - uniq as computedUniq, - union as computedUnion, - intersect as computedIntersect + sum, + min, + max, + map, + sort, + setDiff, + mapBy, + filter, + filterBy, + uniq, + union, + intersect } from 'ember-runtime/computed/reduce_computed_macros'; -var obj, sorted, sortProps, items, userFnCalls, todos, filtered, union; - -QUnit.module('computedMap', { +var obj; +QUnit.module('map', { setup() { - run(function() { - userFnCalls = 0; - obj = EmberObject.extend({ + obj = EmberObject.extend({ + mapped: map('array.@each.v', (item) => item.v), + mappedObjects: map('arrayObjects.@each.v', (item) => ({ name: item.v.name })) + }).create({ + arrayObjects: Ember.A([ + { v: { name: 'Robert' } }, + { v: { name: 'Leanna' } } + ]), - mapped: computedMap('array.@each.v', function(item) { - ++userFnCalls; - return item.v; - }), - - arrayObjects: Ember.A([ - EmberObject.create({ v: { name: 'Robert' } }), - EmberObject.create({ v: { name: 'Leanna' } })]), - mappedObjects: computedMap('arrayObjects.@each.v', function (item) { - return { - name: item.v.name - }; - }) - }).create({ - array: Ember.A([{ v: 1 }, { v: 3 }, { v: 2 }, { v: 1 }]) - }); + array: Ember.A([ + { v: 1 }, + { v: 3 }, + { v: 2 }, + { v: 1 } + ]) }); }, + teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); -QUnit.test('it maps simple properties', function() { - deepEqual(get(obj, 'mapped'), [1, 3, 2, 1]); - - run(function() { - obj.get('array').pushObject({ v: 5 }); - }); - - deepEqual(get(obj, 'mapped'), [1, 3, 2, 1, 5]); - - run(function() { - obj.get('array').removeAt(3); - }); - - deepEqual(get(obj, 'mapped'), [1, 3, 2, 5]); +QUnit.test('map is readOnly', function() { + QUnit.throws(function() { + obj.set('mapped', 1); + }, /Cannot set read-only property "mapped" on object:/); }); -QUnit.test('it caches properly', function() { - var array = get(obj, 'array'); - get(obj, 'mapped'); - - equal(userFnCalls, 4, 'precond - mapper called expected number of times'); +QUnit.test('it maps simple properties', function() { + deepEqual(obj.get('mapped'), [1, 3, 2, 1]); - run(function() { - array.addObject({ v: 7 }); - }); + obj.get('array').pushObject({ v: 5 }); - equal(userFnCalls, 5, 'precond - mapper called expected number of times'); + deepEqual(obj.get('mapped'), [1, 3, 2, 1, 5]); - get(obj, 'mapped'); + obj.get('array').removeAt(3); - equal(userFnCalls, 5, 'computedMap caches properly'); + deepEqual(obj.get('mapped'), [1, 3, 2, 5]); }); QUnit.test('it maps simple unshifted properties', function() { - var array = Ember.A([]); + var array = Ember.A(); - run(function() { - obj = EmberObject.extend({ - mapped: computedMap('array', (item) => item.toUpperCase()) - }).create({ - array - }); - get(obj, 'mapped'); + obj = EmberObject.extend({ + mapped: map('array', (item) => item.toUpperCase()) + }).create({ + array }); - run(function() { - array.unshiftObject('c'); - array.unshiftObject('b'); - array.unshiftObject('a'); + array.unshiftObject('c'); + array.unshiftObject('b'); + array.unshiftObject('a'); - array.popObject(); - }); + array.popObject(); - deepEqual(get(obj, 'mapped'), ['A', 'B'], 'properties unshifted in sequence are mapped correctly'); + deepEqual(obj.get('mapped'), ['A', 'B'], 'properties unshifted in sequence are mapped correctly'); }); QUnit.test('it passes the index to the callback', function() { - var array = Ember.A(['a', 'b', 'c']); + var array = ['a', 'b', 'c']; - run(function() { - obj = EmberObject.extend({ - mapped: computedMap('array', (item, index) => index) - }).create({ - array - }); - get(obj, 'mapped'); + obj = EmberObject.extend({ + mapped: map('array', (item, index) => index) + }).create({ + array }); - deepEqual(get(obj, 'mapped'), [0, 1, 2], 'index is passed to callback correctly'); + deepEqual(obj.get('mapped'), [0, 1, 2], 'index is passed to callback correctly'); }); QUnit.test('it maps objects', function() { - deepEqual(get(obj, 'mappedObjects'), [{ name: 'Robert' }, { name: 'Leanna' }]); + deepEqual(obj.get('mappedObjects'), [ + { name: 'Robert' }, + { name: 'Leanna' } + ]); - run(function() { - obj.get('arrayObjects').pushObject({ v: { name: 'Eddard' } }); + obj.get('arrayObjects').pushObject({ + v: { name: 'Eddard' } }); - deepEqual(get(obj, 'mappedObjects'), [{ name: 'Robert' }, { name: 'Leanna' }, { name: 'Eddard' }]); + deepEqual(obj.get('mappedObjects'), [ + { name: 'Robert' }, + { name: 'Leanna' }, + { name: 'Eddard' } + ]); - run(function() { - obj.get('arrayObjects').removeAt(1); - }); + obj.get('arrayObjects').removeAt(1); - deepEqual(get(obj, 'mappedObjects'), [{ name: 'Robert' }, { name: 'Eddard' }]); + deepEqual(obj.get('mappedObjects'), [ + { name: 'Robert' }, + { name: 'Eddard' } + ]); - run(function() { - obj.get('arrayObjects').objectAt(0).set('v', { name: 'Stannis' }); - }); + set(obj.get('arrayObjects')[0], 'v', { name: 'Stannis' }); - deepEqual(get(obj, 'mappedObjects'), [{ name: 'Stannis' }, { name: 'Eddard' }]); + deepEqual(obj.get('mappedObjects'), [ + { name: 'Stannis' }, + { name: 'Eddard' } + ]); }); QUnit.test('it maps unshifted objects with property observers', function() { - var array = Ember.A([]); + var array = Ember.A(); var cObj = { v: 'c' }; - run(function() { - obj = EmberObject.extend({ - mapped: computedMap('array.@each.v', function (item) { - return get(item, 'v').toUpperCase(); - }) - }).create({ - array - }); - get(obj, 'mapped'); + obj = EmberObject.extend({ + mapped: map('array.@each.v', (item) => get(item, 'v').toUpperCase()) + }).create({ + array }); - run(function() { - array.unshiftObject(cObj); - array.unshiftObject({ v: 'b' }); - array.unshiftObject({ v: 'a' }); + array.unshiftObject(cObj); + array.unshiftObject({ v: 'b' }); + array.unshiftObject({ v: 'a' }); - set(cObj, 'v', 'd'); - }); + set(cObj, 'v', 'd'); deepEqual(array.mapBy('v'), ['a', 'b', 'd'], 'precond - unmapped array is correct'); - deepEqual(get(obj, 'mapped'), ['A', 'B', 'D'], 'properties unshifted in sequence are mapped correctly'); -}); - -QUnit.test('it complains if you invoke the wrong map macro', function() { - expectAssertion(() => computedMap('array', 'property'), /map expects a callback function/); + deepEqual(obj.get('mapped'), ['A', 'B', 'D'], 'properties unshifted in sequence are mapped correctly'); }); -QUnit.module('computedMapBy', { +QUnit.module('mapBy', { setup() { - run(function() { - obj = EmberObject.extend({ - mapped: computedMapBy('array', 'v') - }).create({ - array: Ember.A([{ v: 1 }, { v: 3 }, { v: 2 }, { v: 1 }]) - }); + obj = EmberObject.extend({ + mapped: mapBy('array', 'v') + }).create({ + array: Ember.A([ + { v: 1 }, + { v: 3 }, + { v: 2 }, + { v: 1 } + ]) }); }, teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); -QUnit.test('it maps properties', function() { - get(obj, 'mapped'); +QUnit.test('mapBy is readOnly', function() { + QUnit.throws(function() { + obj.set('mapped', 1); + }, /Cannot set read-only property "mapped" on object:/); +}); - deepEqual(get(obj, 'mapped'), [1, 3, 2, 1]); +QUnit.test('it maps properties', function() { + deepEqual(obj.get('mapped'), [1, 3, 2, 1]); - run(function() { - obj.get('array').pushObject({ v: 5 }); - }); + obj.get('array').pushObject({ v: 5 }); - deepEqual(get(obj, 'mapped'), [1, 3, 2, 1, 5]); + deepEqual(obj.get('mapped'), [1, 3, 2, 1, 5]); - run(function() { - obj.get('array').removeAt(3); - }); + obj.get('array').removeAt(3); - deepEqual(get(obj, 'mapped'), [1, 3, 2, 5]); + deepEqual(obj.get('mapped'), [1, 3, 2, 5]); }); QUnit.test('it is observable', function() { - get(obj, 'mapped'); var calls = 0; - deepEqual(get(obj, 'mapped'), [1, 3, 2, 1]); - - addObserver(obj, 'mapped.@each', function() { - calls++; - }); + deepEqual(obj.get('mapped'), [1, 3, 2, 1]); - run(function() { - obj.get('array').pushObject({ v: 5 }); - }); + addObserver(obj, 'mapped.@each', () => calls++); - equal(calls, 1, 'computedMapBy is observable'); -}); + obj.get('array').pushObject({ v: 5 }); -QUnit.test('it complains with the wrong arguments', function() { - expectAssertion(() => computedMapBy('array', a => a), /mapBy expects a property string/); + equal(calls, 1, 'mapBy is observable'); }); -QUnit.module('computedFilter', { +QUnit.module('filter', { setup() { - run(function() { - userFnCalls = 0; - obj = EmberObject.extend({ - filtered: computedFilter('array', function(item) { - ++userFnCalls; - return item % 2 === 0; - }) - }).create({ - array: Ember.A([1, 2, 3, 4, 5, 6, 7, 8]) - }); + obj = EmberObject.extend({ + filtered: filter('array', (item) => item % 2 === 0) + }).create({ + array: Ember.A([1, 2, 3, 4, 5, 6, 7, 8]) }); }, teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); -QUnit.test('it filters according to the specified filter function', function() { - var filtered = get(obj, 'filtered'); +QUnit.test('filter is readOnly', function() { + QUnit.throws(function() { + obj.set('filtered', 1); + }, /Cannot set read-only property "filtered" on object:/); +}); - deepEqual(filtered, [2,4,6,8], 'computedFilter filters by the specified function'); +QUnit.test('it filters according to the specified filter function', function() { + deepEqual(obj.get('filtered'), [2,4,6,8], 'filter filters by the specified function'); }); QUnit.test('it passes the index to the callback', function() { - var array = Ember.A(['a', 'b', 'c']); - - run(function() { - obj = EmberObject.extend({ - filtered: computedFilter('array', function (item, index) { return index === 1; }) - }).create({ - array - }); - get(obj, 'filtered'); + obj = EmberObject.extend({ + filtered: filter('array', (item, index) => index === 1) + }).create({ + array: ['a', 'b', 'c'] }); deepEqual(get(obj, 'filtered'), ['b'], 'index is passed to callback correctly'); }); QUnit.test('it passes the array to the callback', function() { - var array = Ember.A(['a', 'b', 'c']); - - run(function() { - obj = EmberObject.extend({ - filtered: computedFilter('array', function (item, index, array) { return index === array.get('length') - 2; }) - }).create({ - array - }); - get(obj, 'filtered'); + obj = EmberObject.extend({ + filtered: filter('array', (item, index, array) => index === get(array, 'length') - 2) + }).create({ + array: Ember.A(['a', 'b', 'c']) }); - deepEqual(get(obj, 'filtered'), ['b'], 'array is passed to callback correctly'); + deepEqual(obj.get('filtered'), ['b'], 'array is passed to callback correctly'); }); QUnit.test('it caches properly', function() { - var array = get(obj, 'array'); - get(obj, 'filtered'); - - equal(userFnCalls, 8, 'precond - filter called expected number of times'); + var array = obj.get('array'); - run(function() { - array.addObject(11); - }); + var filtered = obj.get('filtered'); + ok(filtered === obj.get('filtered')); - equal(userFnCalls, 9, 'precond - filter called expected number of times'); + array.addObject(11); + var newFiltered = obj.get('filtered'); - get(obj, 'filtered'); + ok(filtered !== newFiltered); - equal(userFnCalls, 9, 'computedFilter caches properly'); + ok(obj.get('filtered') === newFiltered); }); QUnit.test('it updates as the array is modified', function() { - var array = get(obj, 'array'); - var filtered = get(obj, 'filtered'); + var array = obj.get('array'); - deepEqual(filtered, [2,4,6,8], 'precond - filtered array is initially correct'); + deepEqual(obj.get('filtered'), [2,4,6,8], 'precond - filtered array is initially correct'); - run(function() { - array.addObject(11); - }); - deepEqual(filtered, [2,4,6,8], 'objects not passing the filter are not added'); + array.addObject(11); + deepEqual(obj.get('filtered'), [2,4,6,8], 'objects not passing the filter are not added'); - run(function() { - array.addObject(12); - }); - deepEqual(filtered, [2,4,6,8,12], 'objects passing the filter are added'); + array.addObject(12); + deepEqual(obj.get('filtered'), [2,4,6,8,12], 'objects passing the filter are added'); - run(function() { - array.removeObject(3); - array.removeObject(4); - }); - deepEqual(filtered, [2,6,8,12], 'objects removed from the dependent array are removed from the computed array'); + array.removeObject(3); + array.removeObject(4); + + deepEqual(obj.get('filtered'), [2,6,8,12], 'objects removed from the dependent array are removed from the computed array'); }); QUnit.test('the dependent array can be cleared one at a time', function() { var array = get(obj, 'array'); - var filtered = get(obj, 'filtered'); - deepEqual(filtered, [2,4,6,8], 'precond - filtered array is initially correct'); + deepEqual(obj.get('filtered'), [2,4,6,8], 'precond - filtered array is initially correct'); - run(function() { - // clear 1-8 but in a random order - array.removeObject(3); - array.removeObject(1); - array.removeObject(2); - array.removeObject(4); - array.removeObject(8); - array.removeObject(6); - array.removeObject(5); - array.removeObject(7); - }); + // clear 1-8 but in a random order + array.removeObject(3); + array.removeObject(1); + array.removeObject(2); + array.removeObject(4); + array.removeObject(8); + array.removeObject(6); + array.removeObject(5); + array.removeObject(7); - deepEqual(filtered, [], 'filtered array cleared correctly'); + deepEqual(obj.get('filtered'), [], 'filtered array cleared correctly'); }); QUnit.test('the dependent array can be `clear`ed directly (#3272)', function() { - var array = get(obj, 'array'); - var filtered = get(obj, 'filtered'); + deepEqual(obj.get('filtered'), [2,4,6,8], 'precond - filtered array is initially correct'); - deepEqual(filtered, [2,4,6,8], 'precond - filtered array is initially correct'); - - run(function() { - array.clear(); - }); + obj.get('array').clear(); - deepEqual(filtered, [], 'filtered array cleared correctly'); + deepEqual(obj.get('filtered'), [], 'filtered array cleared correctly'); }); QUnit.test('it updates as the array is replaced', function() { - get(obj, 'array'); - var filtered = get(obj, 'filtered'); + deepEqual(obj.get('filtered'), [2,4,6,8], 'precond - filtered array is initially correct'); - deepEqual(filtered, [2,4,6,8], 'precond - filtered array is initially correct'); + obj.set('array', [20,21,22,23,24]); - run(function() { - set(obj, 'array', Ember.A([20,21,22,23,24])); - }); - deepEqual(filtered, [20,22,24], 'computed array is updated when array is changed'); + deepEqual(obj.get('filtered'), [20,22,24], 'computed array is updated when array is changed'); }); -QUnit.module('computedFilterBy', { +QUnit.module('filterBy', { setup() { obj = EmberObject.extend({ - a1s: computedFilterBy('array', 'a', 1), - as: computedFilterBy('array', 'a'), - bs: computedFilterBy('array', 'b') + a1s: filterBy('array', 'a', 1), + as: filterBy('array', 'a'), + bs: filterBy('array', 'b') }).create({ array: Ember.A([ { name: 'one', a: 1, b: false }, @@ -398,262 +318,218 @@ QUnit.module('computedFilterBy', { }); }, teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); +QUnit.test('filterBy is readOnly', function() { + QUnit.throws(function() { + obj.set('as', 1); + }, /Cannot set read-only property "as" on object:/); +}); + QUnit.test('properties can be filtered by truthiness', function() { - var array = get(obj, 'array'); - var as = get(obj, 'as'); - var bs = get(obj, 'bs'); + deepEqual(obj.get('as').mapBy('name'), ['one', 'two', 'three'], 'properties can be filtered by existence'); + deepEqual(obj.get('bs').mapBy('name'), ['three', 'four'], 'booleans can be filtered'); - deepEqual(as.mapBy('name'), ['one', 'two', 'three'], 'properties can be filtered by existence'); - deepEqual(bs.mapBy('name'), ['three', 'four'], 'booleans can be filtered'); + set(obj.get('array')[0], 'a', undefined); + set(obj.get('array')[3], 'a', true); - run(function() { - set(array.objectAt(0), 'a', undefined); - set(array.objectAt(3), 'a', true); + set(obj.get('array')[0], 'b', true); + set(obj.get('array')[3], 'b', false); - set(array.objectAt(0), 'b', true); - set(array.objectAt(3), 'b', false); - }); - deepEqual(as.mapBy('name'), ['two', 'three', 'four'], 'arrays computed by filter property respond to property changes'); - deepEqual(bs.mapBy('name'), ['one', 'three'], 'arrays computed by filtered property respond to property changes'); + deepEqual(obj.get('as').mapBy('name'), ['two', 'three', 'four'], 'arrays computed by filter property respond to property changes'); + deepEqual(obj.get('bs').mapBy('name'), ['one', 'three'], 'arrays computed by filtered property respond to property changes'); - run(function() { - array.pushObject({ name: 'five', a: 6, b: true }); - }); - deepEqual(as.mapBy('name'), ['two', 'three', 'four', 'five'], 'arrays computed by filter property respond to added objects'); - deepEqual(bs.mapBy('name'), ['one', 'three', 'five'], 'arrays computed by filtered property respond to added objects'); + obj.get('array').pushObject({ name: 'five', a: 6, b: true }); - run(function() { - array.popObject(); - }); - deepEqual(as.mapBy('name'), ['two', 'three', 'four'], 'arrays computed by filter property respond to removed objects'); - deepEqual(bs.mapBy('name'), ['one', 'three'], 'arrays computed by filtered property respond to removed objects'); + deepEqual(obj.get('as').mapBy('name'), ['two', 'three', 'four', 'five'], 'arrays computed by filter property respond to added objects'); + deepEqual(obj.get('bs').mapBy('name'), ['one', 'three', 'five'], 'arrays computed by filtered property respond to added objects'); - run(function() { - set(obj, 'array', Ember.A([{ name: 'six', a: 12, b: true }])); - }); - deepEqual(as.mapBy('name'), ['six'], 'arrays computed by filter property respond to array changes'); - deepEqual(bs.mapBy('name'), ['six'], 'arrays computed by filtered property respond to array changes'); + obj.get('array').popObject(); + + deepEqual(obj.get('as').mapBy('name'), ['two', 'three', 'four'], 'arrays computed by filter property respond to removed objects'); + deepEqual(obj.get('bs').mapBy('name'), ['one', 'three'], 'arrays computed by filtered property respond to removed objects'); + + obj.set('array', [ + { name: 'six', a: 12, b: true } + ]); + + deepEqual(obj.get('as').mapBy('name'), ['six'], 'arrays computed by filter property respond to array changes'); + deepEqual(obj.get('bs').mapBy('name'), ['six'], 'arrays computed by filtered property respond to array changes'); }); QUnit.test('properties can be filtered by values', function() { - var array = get(obj, 'array'); - var a1s = get(obj, 'a1s'); + deepEqual(obj.get('a1s').mapBy('name'), ['one', 'three'], 'properties can be filtered by matching value'); - deepEqual(a1s.mapBy('name'), ['one', 'three'], 'properties can be filtered by matching value'); + obj.get('array').pushObject({ name: 'five', a: 1 }); - run(function() { - array.pushObject({ name: 'five', a: 1 }); - }); - deepEqual(a1s.mapBy('name'), ['one', 'three', 'five'], 'arrays computed by matching value respond to added objects'); + deepEqual(obj.get('a1s').mapBy('name'), ['one', 'three', 'five'], 'arrays computed by matching value respond to added objects'); - run(function() { - array.popObject(); - }); - deepEqual(a1s.mapBy('name'), ['one', 'three'], 'arrays computed by matching value respond to removed objects'); + obj.get('array').popObject(); - run(function() { - set(array.objectAt(1), 'a', 1); - set(array.objectAt(2), 'a', 2); - }); - deepEqual(a1s.mapBy('name'), ['one', 'two'], 'arrays computed by matching value respond to modified properties'); + deepEqual(obj.get('a1s').mapBy('name'), ['one', 'three'], 'arrays computed by matching value respond to removed objects'); + + set(obj.get('array')[1], 'a', 1); + set(obj.get('array')[2], 'a', 2); + + deepEqual(obj.get('a1s').mapBy('name'), ['one', 'two'], 'arrays computed by matching value respond to modified properties'); }); QUnit.test('properties values can be replaced', function() { obj = EmberObject.extend({ - a1s: computedFilterBy('array', 'a', 1), - a1bs: computedFilterBy('a1s', 'b') - }).create({ - array: Ember.A([]) - }); + a1s: filterBy('array', 'a', 1), + a1bs: filterBy('a1s', 'b') + }).create({ + array: [] + }); - var a1bs = get(obj, 'a1bs'); - deepEqual(a1bs.mapBy('name'), [], 'properties can be filtered by matching value'); + deepEqual(obj.get('a1bs').mapBy('name'), [], 'properties can be filtered by matching value'); - run(function() { - set(obj, 'array', Ember.A([{ name: 'item1', a: 1, b: true }])); - }); + set(obj, 'array', [ + { name: 'item1', a: 1, b: true } + ]); - a1bs = get(obj, 'a1bs'); - deepEqual(a1bs.mapBy('name'), ['item1'], 'properties can be filtered by matching value'); + deepEqual(obj.get('a1bs').mapBy('name'), ['item1'], 'properties can be filtered by matching value'); }); -[['uniq', computedUniq], ['union', computedUnion]].forEach(function (tuple) { - var alias = tuple[0]; - var testedFunc = tuple[1]; +[ + ['uniq', uniq], + ['union', union] +].forEach((tuple) => { + const [name, macro] = tuple; - QUnit.module('computed.' + alias, { + QUnit.module(`computed.${name}`, { setup() { - run(function() { - union = testedFunc('array', 'array2', 'array3'); - obj = EmberObject.extend({ - union: union - }).create({ - array: Ember.A([1,2,3,4,5,6]), - array2: Ember.A([4,5,6,7,8,9,4,5,6,7,8,9]), - array3: Ember.A([1,8,10]) - }); + obj = EmberObject.extend({ + union: macro('array', 'array2', 'array3') + }).create({ + array: Ember.A([1, 2, 3, 4, 5, 6]), + array2: Ember.A([4, 5, 6, 7, 8, 9, 4, 5, 6, 7, 8, 9]), + array3: Ember.A([1, 8, 10]) }); }, teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); + QUnit.test(`${name} is readOnly`, function() { + QUnit.throws(function() { + obj.set('union', 1); + }, /Cannot set read-only property "union" on object:/); + }); + QUnit.test('does not include duplicates', function() { - var array = get(obj, 'array'); - var array2 = get(obj, 'array2'); - get(obj, 'array3'); - var union = get(obj, 'union'); + var array = obj.get('array'); + var array2 = obj.get('array2'); - deepEqual(union, [1,2,3,4,5,6,7,8,9,10], alias + ' does not include duplicates'); + deepEqual(obj.get('union').sort((x, y) => x - y), [1,2,3,4,5,6,7,8,9,10], name + ' does not include duplicates'); - run(function() { - array.pushObject(8); - }); + array.pushObject(8); - deepEqual(union, [1,2,3,4,5,6,7,8,9,10], alias + ' does not add existing items'); + deepEqual(obj.get('union').sort((x, y) => x - y), [1,2,3,4,5,6,7,8,9,10], name + ' does not add existing items'); - run(function() { - array.pushObject(11); - }); + array.pushObject(11); - deepEqual(union, [1,2,3,4,5,6,7,8,9,10,11], alias + ' adds new items'); + deepEqual(obj.get('union').sort((x, y) => x - y), [1,2,3,4,5,6,7,8,9,10,11], name + ' adds new items'); - run(function() { - array2.removeAt(6); // remove 7 - }); + array2.removeAt(6); // remove 7 - deepEqual(union, [1,2,3,4,5,6,7,8,9,10,11], alias + ' does not remove items that are still in the dependent array'); + deepEqual(obj.get('union').sort((x, y) => x - y), [1,2,3,4,5,6,7,8,9,10,11], name + ' does not remove items that are still in the dependent array'); - run(function() { - array2.removeObject(7); - }); + array2.removeObject(7); - deepEqual(union, [1,2,3,4,5,6,8,9,10,11], alias + ' removes items when their last instance is gone'); + deepEqual(obj.get('union').sort((x, y) => x - y), [1,2,3,4,5,6,8,9,10,11], name + ' removes items when their last instance is gone'); }); QUnit.test('has set-union semantics', function() { - var array = get(obj, 'array'); - get(obj, 'array2'); - get(obj, 'array3'); - var union = get(obj, 'union'); - - deepEqual(union, [1,2,3,4,5,6,7,8,9,10], alias + ' is initially correct'); + var array = obj.get('array'); - run(function() { - array.removeObject(6); - }); + deepEqual(obj.get('union').sort((x, y) => x - y), [1,2,3,4,5,6,7,8,9,10], name + ' is initially correct'); - deepEqual(union, [1,2,3,4,5,6,7,8,9,10], 'objects are not removed if they exist in other dependent arrays'); + array.removeObject(6); - run(function() { - array.clear(); - }); + deepEqual(obj.get('union').sort((x, y) => x - y), [1,2,3,4,5,6,7,8,9,10], 'objects are not removed if they exist in other dependent arrays'); - deepEqual(union, [1,4,5,6,7,8,9,10], 'objects are removed when they are no longer in any dependent array'); - }); + array.clear(); - QUnit.test('does not need to query the accumulated array while building it', function() { - var indexOfCalls = []; - var CountIndexOfCalls = Mixin.create({ - indexOf() { - indexOfCalls.push(arguments); - return this._super.apply(this, arguments); - } - }); - union.initialValue = function() { - return CountIndexOfCalls.apply(Ember.A([])); - }; - get(obj, 'union'); - ok(indexOfCalls.length === 0, 'Ember.computed.' + alias + ' should not need to query the union as it is being built'); + deepEqual(obj.get('union').sort((x, y) => x - y), [1,4,5,6,7,8,9,10], 'objects are removed when they are no longer in any dependent array'); }); - }); QUnit.module('computed.intersect', { setup() { - run(function() { - obj = EmberObject.extend({ - intersection: computedIntersect('array', 'array2', 'array3') - }).create({ - array: Ember.A([1,2,3,4,5,6]), - array2: Ember.A([3,3,3,4,5]), - array3: Ember.A([3,5,6,7,8]) - }); + obj = EmberObject.extend({ + intersection: intersect('array', 'array2', 'array3') + }).create({ + array: Ember.A([1,2,3,4,5,6]), + array2: Ember.A([3,3,3,4,5]), + array3: Ember.A([3,5,6,7,8]) }); }, teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); +QUnit.test('intersect is readOnly', function() { + QUnit.throws(function() { + obj.set('intersection', 1); + }, /Cannot set read-only property "intersection" on object:/); +}); + QUnit.test('it has set-intersection semantics', function() { - get(obj, 'array'); - var array2 = get(obj, 'array2'); - var array3 = get(obj, 'array3'); - var intersection = get(obj, 'intersection'); + var array2 = obj.get('array2'); + var array3 = obj.get('array3'); - deepEqual(intersection, [3,5], 'intersection is initially correct'); + deepEqual(obj.get('intersection').sort((x,y) => x - y), [3,5], 'intersection is initially correct'); - run(function() { - array2.shiftObject(); - }); - deepEqual(intersection, [3,5], 'objects are not removed when they are still in all dependent arrays'); + array2.shiftObject(); - run(function() { - array2.shiftObject(); - }); - deepEqual(intersection, [3,5], 'objects are not removed when they are still in all dependent arrays'); + deepEqual(obj.get('intersection').sort((x,y) => x - y), [3,5], 'objects are not removed when they are still in all dependent arrays'); - run(function() { - array2.shiftObject(); - }); - deepEqual(intersection, [5], 'objects are removed once they are gone from all dependent arrays'); + array2.shiftObject(); - run(function() { - array2.pushObject(1); - }); - deepEqual(intersection, [5], 'objects are not added as long as they are missing from any dependent array'); + deepEqual(obj.get('intersection').sort((x,y) => x - y), [3,5], 'objects are not removed when they are still in all dependent arrays'); - run(function() { - array3.pushObject(1); - }); - deepEqual(intersection, [5,1], 'objects added once they belong to all dependent arrays'); -}); + array2.shiftObject(); + deepEqual(obj.get('intersection'), [5], 'objects are removed once they are gone from all dependent arrays'); -QUnit.module('computedSetDiff', { + array2.pushObject(1); + + deepEqual(obj.get('intersection'), [5], 'objects are not added as long as they are missing from any dependent array'); + + array3.pushObject(1); + + deepEqual(obj.get('intersection').sort((x,y) => x - y), [1,5], 'objects added once they belong to all dependent arrays'); +}); + +QUnit.module('setDiff', { setup() { - run(function() { - obj = EmberObject.extend({ - diff: computedSetDiff('array', 'array2') - }).create({ - array: Ember.A([1,2,3,4,5,6,7]), - array2: Ember.A([3,4,5,10]) - }); + obj = EmberObject.extend({ + diff: setDiff('array', 'array2') + }).create({ + array: Ember.A([1,2,3,4,5,6,7]), + array2: Ember.A([3,4,5,10]) }); }, teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); +QUnit.test('setDiff is readOnly', function() { + QUnit.throws(function() { + obj.set('diff', 1); + }, /Cannot set read-only property "diff" on object:/); +}); + QUnit.test('it throws an error if given fewer or more than two dependent properties', function() { throws(function () { EmberObject.extend({ - diff: computedSetDiff('array') + diff: setDiff('array') }).create({ array: Ember.A([1,2,3,4,5,6,7]), array2: Ember.A([3,4,5]) @@ -662,7 +538,7 @@ QUnit.test('it throws an error if given fewer or more than two dependent propert throws(function () { EmberObject.extend({ - diff: computedSetDiff('array', 'array2', 'array3') + diff: setDiff('array', 'array2', 'array3') }).create({ array: Ember.A([1,2,3,4,5,6,7]), array2: Ember.A([3,4,5]), @@ -673,451 +549,512 @@ QUnit.test('it throws an error if given fewer or more than two dependent propert QUnit.test('it has set-diff semantics', function() { - var array1 = get(obj, 'array'); - var array2 = get(obj, 'array2'); - var diff = get(obj, 'diff'); + var array1 = obj.get('array'); + var array2 = obj.get('array2'); - deepEqual(diff, [1, 2, 6, 7], 'set-diff is initially correct'); + deepEqual(obj.get('diff').sort((x, y) => x - y), [1, 2, 6, 7], 'set-diff is initially correct'); - run(function() { - array2.popObject(); - }); - deepEqual(diff, [1,2,6,7], 'removing objects from the remove set has no effect if the object is not in the keep set'); + array2.popObject(); - run(function() { - array2.shiftObject(); - }); - deepEqual(diff, [1, 2, 6, 7, 3], 'removing objects from the remove set adds them if they\'re in the keep set'); + deepEqual(obj.get('diff').sort((x, y) => x - y), [1,2,6,7], 'removing objects from the remove set has no effect if the object is not in the keep set'); - run(function() { - array1.removeObject(3); - }); - deepEqual(diff, [1, 2, 6, 7], 'removing objects from the keep array removes them from the computed array'); + array2.shiftObject(); - run(function() { - array1.pushObject(5); - }); - deepEqual(diff, [1, 2, 6, 7], 'objects added to the keep array that are in the remove array are not added to the computed array'); + deepEqual(obj.get('diff').sort((x, y) => x - y), [1, 2, 3, 6, 7], 'removing objects from the remove set adds them if they\'re in the keep set'); - run(function() { - array1.pushObject(22); - }); - deepEqual(diff, [1, 2, 6, 7, 22], 'objects added to the keep array not in the remove array are added to the computed array'); + array1.removeObject(3); + + deepEqual(obj.get('diff').sort((x, y) => x - y), [1, 2, 6, 7], 'removing objects from the keep array removes them from the computed array'); + + array1.pushObject(5); + + deepEqual(obj.get('diff').sort((x, y) => x - y), [1, 2, 6, 7], 'objects added to the keep array that are in the remove array are not added to the computed array'); + + array1.pushObject(22); + + deepEqual(obj.get('diff').sort((x, y) => x - y), [1, 2, 6, 7, 22], 'objects added to the keep array not in the remove array are added to the computed array'); }); function commonSortTests() { QUnit.test('arrays are initially sorted', function() { - run(function() { - sorted = get(obj, 'sortedItems'); - }); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ], 'array is initially sorted'); + }); - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'array is initially sorted'); + QUnit.test('default sort order is correct', function() { + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ], 'array is initially sorted'); }); QUnit.test('changing the dependent array updates the sorted array', function() { - run(function() { - sorted = get(obj, 'sortedItems'); - }); - - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); - - run(function() { - set(obj, 'items', Ember.A([{ - fname: 'Roose', lname: 'Bolton' - }, { - fname: 'Theon', lname: 'Greyjoy' - }, { - fname: 'Ramsey', lname: 'Bolton' - }, { - fname: 'Stannis', lname: 'Baratheon' - }])); - }); - - deepEqual(sorted.mapBy('fname'), ['Stannis', 'Ramsey', 'Roose', 'Theon'], 'changing dependent array updates sorted array'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ], 'precond - array is initially sorted'); + + obj.set('items', [ + { fname: 'Roose', lname: 'Bolton' }, + { fname: 'Theon', lname: 'Greyjoy' }, + { fname: 'Ramsey', lname: 'Bolton' }, + { fname: 'Stannis', lname: 'Baratheon' } + ]); + + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Stannis', + 'Ramsey', + 'Roose', + 'Theon' + ], 'changing dependent array updates sorted array'); }); QUnit.test('adding to the dependent array updates the sorted array', function() { - run(function() { - sorted = get(obj, 'sortedItems'); - items = get(obj, 'items'); - }); + var items = obj.get('items'); - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ], 'precond - array is initially sorted'); - run(function() { - items.pushObject({ fname: 'Tyrion', lname: 'Lannister' }); + items.pushObject({ + fname: 'Tyrion', + lname: 'Lannister' }); - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Tyrion', 'Bran', 'Robb'], 'Adding to the dependent array updates the sorted array'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Tyrion', + 'Bran', + 'Robb' + ], 'Adding to the dependent array updates the sorted array'); }); QUnit.test('removing from the dependent array updates the sorted array', function() { - run(function() { - sorted = get(obj, 'sortedItems'); - items = get(obj, 'items'); - }); - - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ], 'precond - array is initially sorted'); - run(function() { - items.popObject(); - }); + obj.get('items').popObject(); - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Robb'], 'Removing from the dependent array updates the sorted array'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Robb' + ], 'Removing from the dependent array updates the sorted array'); }); QUnit.test('distinct items may be sort-equal, although their relative order will not be guaranteed', function() { - var jaime, jaimeInDisguise; + // We recreate jaime and "Cersei" here only for test stability: we want + // their guid-ordering to be deterministic + var jaimeInDisguise = { + fname: 'Cersei', + lname: 'Lannister', + age: 34 + }; - run(function() { - // We recreate jaime and "Cersei" here only for test stability: we want - // their guid-ordering to be deterministic - jaimeInDisguise = EmberObject.create({ - fname: 'Cersei', lname: 'Lannister', age: 34 - }); - jaime = EmberObject.create({ - fname: 'Jaime', lname: 'Lannister', age: 34 - }); - items = get(obj, 'items'); + var jaime = { + fname: 'Jaime', + lname: 'Lannister', + age: 34 + }; - items.replace(0, 1, jaime); - items.replace(1, 1, jaimeInDisguise); - sorted = get(obj, 'sortedItems'); - }); + var items = obj.get('items'); - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); + items.replace(0, 1, jaime); + items.replace(1, 1, jaimeInDisguise); - run(function() { - // comparator will now return 0. - // Apparently it wasn't a very good disguise. - jaimeInDisguise.set('fname', 'Jaime'); - }); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ], 'precond - array is initially sorted'); - deepEqual(sorted.mapBy('fname'), ['Jaime', 'Jaime', 'Bran', 'Robb'], 'sorted array is updated'); + set(jaimeInDisguise, 'fname', 'Jaime'); - run(function() { - // comparator will again return non-zero - jaimeInDisguise.set('fname', 'Cersei'); - }); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Jaime', + 'Jaime', + 'Bran', + 'Robb' + ], 'sorted array is updated'); + set(jaimeInDisguise, 'fname', 'Cersei'); - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'sorted array is updated'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ], 'sorted array is updated'); }); QUnit.test('guid sort-order fallback with a search proxy is not confused by non-search ObjectProxys', function() { - var tyrion = { fname: 'Tyrion', lname: 'Lannister' }; + var tyrion = { + fname: 'Tyrion', + lname: 'Lannister' + }; + var tyrionInDisguise = ObjectProxy.create({ - fname: 'Yollo', - lname: '', - content: tyrion - }); + fname: 'Yollo', + lname: '', + content: tyrion + }); - items = get(obj, 'items'); + var items = obj.get('items'); - run(function() { - sorted = get(obj, 'sortedItems'); - items.pushObject(tyrion); - }); + items.pushObject(tyrion); - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Tyrion', 'Bran', 'Robb']); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Tyrion', + 'Bran', + 'Robb' + ]); - run(function() { - items.pushObject(tyrionInDisguise); - }); + items.pushObject(tyrionInDisguise); - deepEqual(sorted.mapBy('fname'), ['Yollo', 'Cersei', 'Jaime', 'Tyrion', 'Bran', 'Robb']); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Yollo', + 'Cersei', + 'Jaime', + 'Tyrion', + 'Bran', + 'Robb' + ]); }); } -QUnit.module('computedSort - sortProperties', { +QUnit.module('sort - sortProperties', { setup() { - run(function() { - obj = EmberObject.extend({ - sortedItems: computedSort('items', 'itemSorting') - }).create({ - itemSorting: Ember.A(['lname', 'fname']), - items: Ember.A([{ - fname: 'Jaime', lname: 'Lannister', age: 34 - }, { - fname: 'Cersei', lname: 'Lannister', age: 34 - }, { - fname: 'Robb', lname: 'Stark', age: 16 - }, { - fname: 'Bran', lname: 'Stark', age: 8 - }]) - }); + obj = EmberObject.extend({ + sortedItems: sort('items', 'itemSorting') + }).create({ + itemSorting: Ember.A(['lname', 'fname']), + items: Ember.A([ + { fname: 'Jaime', lname: 'Lannister', age: 34 }, + { fname: 'Cersei', lname: 'Lannister', age: 34 }, + { fname: 'Robb', lname: 'Stark', age: 16 }, + { fname: 'Bran', lname: 'Stark', age: 8 } + ]) }); }, teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); +QUnit.test('sort is readOnly', function() { + QUnit.throws(function() { + obj.set('sortedItems', 1); + }, /Cannot set read-only property "sortedItems" on object:/); +}); + commonSortTests(); QUnit.test('updating sort properties detaches observers for old sort properties', function() { - var objectToRemove = get(obj, 'items').objectAt(3); + var objectToRemove = obj.get('items')[3]; - run(function() { - sorted = get(obj, 'sortedItems'); - }); - - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ], 'precond - array is initially sorted'); - run(function() { - set(obj, 'itemSorting', Ember.A(['fname:desc'])); - }); + obj.set('itemSorting', Ember.A(['fname:desc'])); - deepEqual(sorted.mapBy('fname'), ['Robb', 'Jaime', 'Cersei', 'Bran'], 'after updating sort properties array is updated'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Robb', + 'Jaime', + 'Cersei', + 'Bran' + ], 'after updating sort properties array is updated'); - run(function() { - get(obj, 'items').removeObject(objectToRemove); - }); + obj.get('items').removeObject(objectToRemove); - deepEqual(sorted.mapBy('fname'), ['Robb', 'Jaime', 'Cersei'], 'after removing item array is updated'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Robb', + 'Jaime', + 'Cersei' + ], 'after removing item array is updated'); - run(function() { - set(objectToRemove, 'lname', 'Updated-Stark'); - }); + set(objectToRemove, 'lname', 'Updated-Stark'); - deepEqual(sorted.mapBy('fname'), ['Robb', 'Jaime', 'Cersei'], 'after changing removed item array is not updated'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Robb', + 'Jaime', + 'Cersei' + ], 'after changing removed item array is not updated'); }); QUnit.test('updating sort properties updates the sorted array', function() { - run(function() { - sorted = get(obj, 'sortedItems'); - }); - - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); - - run(function() { - set(obj, 'itemSorting', Ember.A(['fname:desc'])); - }); - - deepEqual(sorted.mapBy('fname'), ['Robb', 'Jaime', 'Cersei', 'Bran'], 'after updating sort properties array is updated'); -}); - -QUnit.test('updating sort properties in place updates the sorted array', function() { - run(function() { - sorted = get(obj, 'sortedItems'); - sortProps = get(obj, 'itemSorting'); - }); - - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); - - run(function() { - sortProps.clear(); - sortProps.pushObject('fname'); - }); - - deepEqual(sorted.mapBy('fname'), ['Bran', 'Cersei', 'Jaime', 'Robb'], 'after updating sort properties array is updated'); -}); - -QUnit.test('updating new sort properties in place updates the sorted array', function() { - run(function() { - sorted = get(obj, 'sortedItems'); - }); - - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); - - run(function() { - set(obj, 'itemSorting', Ember.A(['age:desc', 'fname:asc'])); - }); - - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Robb', 'Bran'], 'precond - array is correct after item sorting is changed'); - - run(function() { - items = get(obj, 'items'); - - var cersei = items.objectAt(1); - set(cersei, 'age', 29); // how vain - }); - - deepEqual(sorted.mapBy('fname'), ['Jaime', 'Cersei', 'Robb', 'Bran'], 'after updating sort properties array is updated'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ], 'precond - array is initially sorted'); + + obj.set('itemSorting', Ember.A(['fname:desc'])); + + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Robb', + 'Jaime', + 'Cersei', + 'Bran' + ], 'after updating sort properties array is updated'); +}); + +QUnit.test('updating sort properties invalidates the sorted array', function() { + var sortProps = obj.get('itemSorting'); + + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ], 'precond - array is initially sorted'); + + sortProps.clear(); + sortProps.pushObject('fname'); + + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Bran', + 'Cersei', + 'Jaime', + 'Robb' + ], 'after updating sort properties array is updated'); +}); + +QUnit.test('updating new sort properties invalidates the sorted array', function() { + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ], 'precond - array is initially sorted'); + + obj.set('itemSorting', Ember.A(['age:desc', 'fname:asc'])); + + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Robb', + 'Bran' + ], 'precond - array is correct after item sorting is changed'); + + set(obj.get('items')[1], 'age', 29); + + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Jaime', + 'Cersei', + 'Robb', + 'Bran' + ], 'after updating sort properties array is updated'); }); QUnit.test('sort direction defaults to ascending', function() { - run(function() { - sorted = get(obj, 'sortedItems'); - }); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ]); +}); - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); +QUnit.test('sort direction defaults to ascending (with sort property change)', function() { + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ], 'precond - array is initially sorted'); - run(function() { - set(obj, 'itemSorting', Ember.A(['fname'])); - }); + obj.set('itemSorting', Ember.A(['fname'])); - deepEqual(sorted.mapBy('fname'), ['Bran', 'Cersei', 'Jaime', 'Robb'], 'sort direction defaults to ascending'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Bran', + 'Cersei', + 'Jaime', + 'Robb' + ], 'sort direction defaults to ascending'); }); QUnit.test('updating an item\'s sort properties updates the sorted array', function() { - var tyrionInDisguise; - - run(function() { - sorted = get(obj, 'sortedItems'); - items = get(obj, 'items'); - }); + var tyrionInDisguise = obj.get('items')[1]; - tyrionInDisguise = items.objectAt(1); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ], 'precond - array is initially sorted'); - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); + set(tyrionInDisguise, 'fname', 'Tyrion'); - run(function() { - set(tyrionInDisguise, 'fname', 'Tyrion'); - }); - - deepEqual(sorted.mapBy('fname'), ['Jaime', 'Tyrion', 'Bran', 'Robb'], 'updating an item\'s sort properties updates the sorted array'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Jaime', + 'Tyrion', + 'Bran', + 'Robb' + ], 'updating an item\'s sort properties updates the sorted array'); }); QUnit.test('updating several of an item\'s sort properties updated the sorted array', function() { - var sansaInDisguise; - - run(function() { - sorted = get(obj, 'sortedItems'); - items = get(obj, 'items'); - }); - - sansaInDisguise = items.objectAt(1); + var sansaInDisguise = obj.get('items')[1]; - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Bran', + 'Robb' + ], 'precond - array is initially sorted'); - run(function() { - setProperties(sansaInDisguise, { - fname: 'Sansa', - lname: 'Stark' - }); + setProperties(sansaInDisguise, { + fname: 'Sansa', + lname: 'Stark' }); - deepEqual(sorted.mapBy('fname'), ['Jaime', 'Bran', 'Robb', 'Sansa'], 'updating an item\'s sort properties updates the sorted array'); + deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Jaime', + 'Bran', + 'Robb', + 'Sansa' + ], 'updating an item\'s sort properties updates the sorted array'); }); QUnit.test('updating an item\'s sort properties does not error when binary search does a self compare (#3273)', function() { - var jaime, cersei; + var jaime = { + name: 'Jaime', + status: 1 + }; - run(function() { - jaime = EmberObject.create({ - name: 'Jaime', - status: 1 - }); - cersei = EmberObject.create({ - name: 'Cersei', - status: 2 - }); + var cersei = { + name: 'Cersei', + status: 2 + }; - obj = EmberObject.extend({ - sortedPeople: computedSort('people', 'sortProps') - }).create({ - people: Ember.A([jaime, cersei]), - sortProps: Ember.A(['status']) - }); + var obj = EmberObject.extend({ + sortProps: ['status'], + sortedPeople: sort('people', 'sortProps') + }).create({ + people: [jaime, cersei] }); - deepEqual(get(obj, 'sortedPeople'), [jaime, cersei], 'precond - array is initially sorted'); + deepEqual(obj.get('sortedPeople'), [ + jaime, + cersei + ], 'precond - array is initially sorted'); - run(function() { - cersei.set('status', 3); - }); + set(cersei, 'status', 3); - deepEqual(get(obj, 'sortedPeople'), [jaime, cersei], 'array is sorted correctly'); + deepEqual(obj.get('sortedPeople'), [ + jaime, + cersei + ], 'array is sorted correctly'); - run(function() { - cersei.set('status', 2); - }); + set(cersei, 'status', 2); - deepEqual(get(obj, 'sortedPeople'), [jaime, cersei], 'array is sorted correctly'); + deepEqual(obj.get('sortedPeople'), [ + jaime, + cersei + ], 'array is sorted correctly'); }); QUnit.test('array observers do not leak', function() { - var jaime; + var daria = { name: 'Daria' }; + var jane = { name: 'Jane' }; - var daria = EmberObject.create({ - name: 'Daria' - }); + var sisters = [jane, daria]; - var jane = EmberObject.create({ - name: 'Jane' + var sortProps = Ember.A(['name']); + var jaime = EmberObject.extend({ + sortedPeople: sort('sisters', 'sortProps'), + sortProps + }).create({ + sisters }); - var sisters = Ember.A([jane, daria]); - var sortProps; + jaime.get('sortedPeople'); + Ember.run(jaime, 'destroy'); - run(function() { - sortProps = Ember.A(['name']); - jaime = EmberObject.extend({ - sortedPeople: computedSort('sisters', 'sortProps'), - sortProps: sortProps - }).create({ - sisters: sisters + try { + sortProps.pushObject({ + name: 'Anna' }); - }); - - run(function() { - jaime.get('sortedPeople'); - jaime.destroy(); - }); - - run(function() { - try { - sortProps.pushObject({ - name: 'Anna' - }); - ok(true); - } catch (e) { - ok(false, e); - } - }); + ok(true); + } catch (e) { + ok(false, e); + } }); QUnit.test('property paths in sort properties update the sorted array', function () { - var jaime, cersei, sansa; + var jaime = { + relatedObj: { status: 1, firstName: 'Jaime', lastName: 'Lannister' } + }; - run(function () { - jaime = EmberObject.create({ - relatedObj: EmberObject.create({ status: 1, firstName: 'Jaime', lastName: 'Lannister' }) - }); - cersei = EmberObject.create({ - relatedObj: EmberObject.create({ status: 2, firstName: 'Cersei', lastName: 'Lannister' }) - }); - sansa = EmberObject.create({ - relatedObj: EmberObject.create({ status: 3, firstName: 'Sansa', lastName: 'Stark' }) - }); + var cersei = { + relatedObj: { status: 2, firstName: 'Cersei', lastName: 'Lannister' } + }; - obj = EmberObject.extend({ - sortedPeople: computedSort('people', 'sortProps') - }).create({ - people: Ember.A([jaime, cersei, sansa]), - sortProps: Ember.A(['relatedObj.status']) - }); + var sansa = EmberObject.create({ + relatedObj: { status: 3, firstName: 'Sansa', lastName: 'Stark' } }); - deepEqual(get(obj, 'sortedPeople'), [jaime, cersei, sansa], 'precond - array is initially sorted'); - - run(function () { - cersei.set('status', 3); + var obj = EmberObject.extend({ + sortProps: ['relatedObj.status'], + sortedPeople: sort('people', 'sortProps') + }).create({ + people: [jaime, cersei, sansa] }); - deepEqual(get(obj, 'sortedPeople'), [jaime, cersei, sansa], 'array is sorted correctly'); + deepEqual(obj.get('sortedPeople'), [ + jaime, + cersei, + sansa + ], 'precond - array is initially sorted'); - run(function () { - cersei.set('status', 1); - }); + get(cersei, 'status', 3); - deepEqual(get(obj, 'sortedPeople'), [jaime, cersei, sansa], 'array is sorted correctly'); + deepEqual(obj.get('sortedPeople'), [ + jaime, + cersei, + sansa + ], 'array is sorted correctly'); - run(function () { - sansa.set('status', 1); - }); + set(cersei, 'status', 1); - deepEqual(get(obj, 'sortedPeople'), [jaime, cersei, sansa], 'array is sorted correctly'); + deepEqual(obj.get('sortedPeople'), [ + jaime, + cersei, + sansa + ], 'array is sorted correctly'); - run(function () { - obj.set('sortProps', Ember.A(['relatedObj.firstName'])); - }); + sansa.set('status', 1); + + deepEqual(obj.get('sortedPeople'), [jaime, cersei, sansa], 'array is sorted correctly'); + + obj.set('sortProps', ['relatedObj.firstName']); - deepEqual(get(obj, 'sortedPeople'), [cersei, jaime, sansa], 'array is sorted correctly'); + deepEqual(obj.get('sortedPeople'), [cersei, jaime, sansa], 'array is sorted correctly'); }); function sortByLnameFname(a, b) { @@ -1141,350 +1078,265 @@ function sortByFnameAsc(a, b) { return fna > fnb ? 1 : -1; } -QUnit.module('computedSort - sort function', { +QUnit.module('sort - sort function', { setup() { - run(function() { - obj = EmberObject.extend({ - sortedItems: computedSort('items.@each.fname', sortByLnameFname) - }).create({ - items: Ember.A([{ - fname: 'Jaime', lname: 'Lannister', age: 34 - }, { - fname: 'Cersei', lname: 'Lannister', age: 34 - }, { - fname: 'Robb', lname: 'Stark', age: 16 - }, { - fname: 'Bran', lname: 'Stark', age: 8 - }]) - }); + obj = EmberObject.extend({ + sortedItems: sort('items.@each.fname', sortByLnameFname) + }).create({ + items: Ember.A([ + { fname: 'Jaime', lname: 'Lannister', age: 34 }, + { fname: 'Cersei', lname: 'Lannister', age: 34 }, + { fname: 'Robb', lname: 'Stark', age: 16 }, + { fname: 'Bran', lname: 'Stark', age: 8 } + ]) }); }, teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); +QUnit.test('sort (with function) is readOnly', function() { + QUnit.throws(function() { + obj.set('sortedItems', 1); + }, /Cannot set read-only property "sortedItems" on object:/); +}); + commonSortTests(); QUnit.test('changing item properties specified via @each triggers a resort of the modified item', function() { - var tyrionInDisguise; + var items = get(obj, 'items'); - run(function() { - sorted = get(obj, 'sortedItems'); - items = get(obj, 'items'); - }); - - tyrionInDisguise = items.objectAt(1); + var tyrionInDisguise = items[1]; - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); + deepEqual(obj.get('sortedItems').mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); - run(function() { - set(tyrionInDisguise, 'fname', 'Tyrion'); - }); + set(tyrionInDisguise, 'fname', 'Tyrion'); - deepEqual(sorted.mapBy('fname'), ['Jaime', 'Tyrion', 'Bran', 'Robb'], 'updating a specified property on an item resorts it'); + deepEqual(obj.get('sortedItems').mapBy('fname'), ['Jaime', 'Tyrion', 'Bran', 'Robb'], 'updating a specified property on an item resorts it'); }); QUnit.test('changing item properties not specified via @each does not trigger a resort', function() { - var cersei; + var items = obj.get('items'); + var cersei = items[1]; - run(function() { - sorted = get(obj, 'sortedItems'); - items = get(obj, 'items'); - }); - - cersei = items.objectAt(1); - - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); + deepEqual(obj.get('sortedItems').mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'precond - array is initially sorted'); - run(function() { - set(cersei, 'lname', 'Stark'); // plot twist! (possibly not canon) - }); + set(cersei, 'lname', 'Stark'); // plot twist! (possibly not canon) // The array has become unsorted. If your sort function is sensitive to // properties, they *must* be specified as dependent item property keys or // we'll be doing binary searches on unsorted arrays. - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'updating an unspecified property on an item does not resort it'); + deepEqual(obj.get('sortedItems').mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb'], 'updating an unspecified property on an item does not resort it'); }); -QUnit.module('computedSort - stability', { +QUnit.module('sort - stability', { setup() { - run(function() { - obj = EmberObject.extend({ - sortProps: Ember.A(['count', 'name']), - sortedItems: computedSort('items', 'sortProps') - }).create({ - items: Ember.A(Ember.A([{ - name: 'A', count: 1 - }, { - name: 'B', count: 1 - }, { - name: 'C', count: 1 - }, { - name: 'D', count: 1 - }]).map((elt) => EmberObject.create(elt))) - }); + obj = EmberObject.extend({ + sortProps: ['count', 'name'], + sortedItems: sort('items', 'sortProps') + }).create({ + items: [ + { name: 'A', count: 1 }, + { name: 'B', count: 1 }, + { name: 'C', count: 1 }, + { name: 'D', count: 1 } + ] }); }, teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); QUnit.test('sorts correctly as only one property changes', function() { - var sorted; - run(function() { - sorted = obj.get('sortedItems'); - }); - deepEqual(sorted.mapBy('name'), ['A', 'B', 'C', 'D'], 'initial'); - obj.get('items').objectAt(3).set('count', 2); - run(function() { - sorted = obj.get('sortedItems'); - }); - deepEqual(sorted.mapBy('name'), ['A', 'B', 'C', 'D'], 'final'); + deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'B', 'C', 'D'], 'initial'); + + set(obj.get('items')[3], 'count', 2); + + deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'B', 'C', 'D'], 'final'); }); -QUnit.module('computedSort - concurrency', { +QUnit.module('sort - concurrency', { setup() { - run(function() { - obj = EmberObject.extend({ - sortProps: Ember.A(['count']), - sortedItems: computedSort('items', 'sortProps'), - customSortedItems: computedSort('items.@each.count', function(a, b) { - return get(a, 'count') - get(b, 'count'); - }) - }).create({ - items: Ember.A(Ember.A([{ - name: 'A', count: 1 - }, { - name: 'B', count: 2 - }, { - name: 'C', count: 3 - }, { - name: 'D', count: 4 - }]).map((elt) => EmberObject.create(elt))) - }); + obj = EmberObject.extend({ + sortProps: ['count'], + sortedItems: sort('items', 'sortProps'), + customSortedItems: sort('items.@each.count', (a, b) => a.count - b.count) + }).create({ + items: Ember.A([ + { name: 'A', count: 1 }, + { name: 'B', count: 2 }, + { name: 'C', count: 3 }, + { name: 'D', count: 4 } + ]) }); }, + teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); -QUnit.test('sorts correctly when there are concurrent changes', function() { - var sorted; - run(function() { - sorted = obj.get('sortedItems'); - }); +QUnit.test('sorts correctly after mutation to the sort properties', function() { + var sorted = obj.get('sortedItems'); deepEqual(sorted.mapBy('name'), ['A', 'B', 'C', 'D'], 'initial'); - Ember.changeProperties(function() { - obj.get('items').objectAt(1).set('count', 5); - obj.get('items').objectAt(2).set('count', 6); - }); - run(function() { - sorted = obj.get('sortedItems'); - }); - deepEqual(sorted.mapBy('name'), ['A', 'D', 'B', 'C'], 'final'); -}); -QUnit.test('sorts correctly with a user-provided comparator when there are concurrent changes', function() { - var sorted; - run(function() { - sorted = obj.get('customSortedItems'); - deepEqual(sorted.mapBy('name'), ['A', 'B', 'C', 'D'], 'initial'); - }); + set(obj.get('items')[1], 'count', 5); + set(obj.get('items')[2], 'count', 6); - run(function() { - Ember.changeProperties(function() { - obj.get('items').objectAt(1).set('count', 5); - obj.get('items').objectAt(2).set('count', 6); - }); - sorted = obj.get('customSortedItems'); - deepEqual(sorted.mapBy('name'), ['A', 'D', 'B', 'C'], 'final'); - }); + deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'D', 'B', 'C'], 'final'); }); +QUnit.test('sort correctl after mutation to the sor ', function() { + deepEqual(obj.get('customSortedItems').mapBy('name'), ['A', 'B', 'C', 'D'], 'initial'); + set(obj.get('items')[1], 'count', 5); + set(obj.get('items')[2], 'count', 6); -QUnit.module('computedMax', { + deepEqual(obj.get('customSortedItems').mapBy('name'), ['A', 'D', 'B', 'C'], 'final'); + + deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'D', 'B', 'C'], 'final'); +}); + +QUnit.module('max', { setup() { - run(function() { - obj = EmberObject.extend({ - max: computedMax('items') - }).create({ - items: Ember.A([1,2,3]) - }); + obj = EmberObject.extend({ + max: max('items') + }).create({ + items: Ember.A([1,2,3]) }); }, teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); +QUnit.test('max is readOnly', function() { + QUnit.throws(function() { + obj.set('max', 1); + }, /Cannot set read-only property "max" on object:/); +}); + QUnit.test('max tracks the max number as objects are added', function() { - equal(get(obj, 'max'), 3, 'precond - max is initially correct'); + equal(obj.get('max'), 3, 'precond - max is initially correct'); - run(function() { - items = get(obj, 'items'); - }); + var items = obj.get('items'); - run(function() { - items.pushObject(5); - }); + items.pushObject(5); - equal(get(obj, 'max'), 5, 'max updates when a larger number is added'); + equal(obj.get('max'), 5, 'max updates when a larger number is added'); - run(function() { - items.pushObject(2); - }); + items.pushObject(2); - equal(get(obj, 'max'), 5, 'max does not update when a smaller number is added'); + equal(obj.get('max'), 5, 'max does not update when a smaller number is added'); }); QUnit.test('max recomputes when the current max is removed', function() { - equal(get(obj, 'max'), 3, 'precond - max is initially correct'); + equal(obj.get('max'), 3, 'precond - max is initially correct'); - run(function() { - items = get(obj, 'items'); - items.removeObject(2); - }); + obj.get('items').removeObject(2); - equal(get(obj, 'max'), 3, 'max is unchanged when a non-max item is removed'); + equal(obj.get('max'), 3, 'max is unchanged when a non-max item is removed'); - run(function() { - items.removeObject(3); - }); + obj.get('items').removeObject(3); - equal(get(obj, 'max'), 1, 'max is recomputed when the current max is removed'); + equal(obj.get('max'), 1, 'max is recomputed when the current max is removed'); }); -QUnit.module('computedMin', { +QUnit.module('min', { setup() { - run(function() { - obj = EmberObject.extend({ - min: computedMin('items') - }).create({ - items: Ember.A([1,2,3]) - }); + obj = EmberObject.extend({ + min: min('items') + }).create({ + items: Ember.A([1,2,3]) }); }, teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); -QUnit.test('min tracks the min number as objects are added', function() { - equal(get(obj, 'min'), 1, 'precond - min is initially correct'); +QUnit.test('min is readOnly', function() { + QUnit.throws(function() { + obj.set('min', 1); + }, /Cannot set read-only property "min" on object:/); +}); - run(function() { - items = get(obj, 'items'); - }); +QUnit.test('min tracks the min number as objects are added', function() { + equal(obj.get('min'), 1, 'precond - min is initially correct'); - run(function() { - items.pushObject(-2); - }); + obj.get('items').pushObject(-2); - equal(get(obj, 'min'), -2, 'min updates when a smaller number is added'); + equal(obj.get('min'), -2, 'min updates when a smaller number is added'); - run(function() { - items.pushObject(2); - }); + obj.get('items').pushObject(2); - equal(get(obj, 'min'), -2, 'min does not update when a larger number is added'); + equal(obj.get('min'), -2, 'min does not update when a larger number is added'); }); QUnit.test('min recomputes when the current min is removed', function() { - equal(get(obj, 'min'), 1, 'precond - min is initially correct'); + var items = obj.get('items'); - run(function() { - items = get(obj, 'items'); - items.removeObject(2); - }); + equal(obj.get('min'), 1, 'precond - min is initially correct'); - equal(get(obj, 'min'), 1, 'min is unchanged when a non-min item is removed'); + items.removeObject(2); - run(function() { - items.removeObject(1); - }); + equal(obj.get('min'), 1, 'min is unchanged when a non-min item is removed'); - equal(get(obj, 'min'), 3, 'min is recomputed when the current min is removed'); + items.removeObject(1); + + equal(obj.get('min'), 3, 'min is recomputed when the current min is removed'); }); QUnit.module('Ember.arrayComputed - mixed sugar', { setup() { - run(function() { - obj = EmberObject.extend({ - lannisters: computedFilterBy('items', 'lname', 'Lannister'), - sortedLannisters: computedSort('lannisters', 'lannisterSorting'), - starks: computedFilterBy('items', 'lname', 'Stark'), - starkAges: computedMapBy('starks', 'age'), - oldestStarkAge: computedMax('starkAges') - }).create({ - lannisterSorting: Ember.A(['fname']), - items: Ember.A([{ - fname: 'Jaime', lname: 'Lannister', age: 34 - }, { - fname: 'Cersei', lname: 'Lannister', age: 34 - }, { - fname: 'Robb', lname: 'Stark', age: 16 - }, { - fname: 'Bran', lname: 'Stark', age: 8 - }]) - }); + obj = EmberObject.extend({ + lannisters: filterBy('items', 'lname', 'Lannister'), + lannisterSorting: Ember.A(['fname']), + sortedLannisters: sort('lannisters', 'lannisterSorting'), + + starks: filterBy('items', 'lname', 'Stark'), + starkAges: mapBy('starks', 'age'), + oldestStarkAge: max('starkAges') + }).create({ + items: Ember.A([ + { fname: 'Jaime', lname: 'Lannister', age: 34 }, + { fname: 'Cersei', lname: 'Lannister', age: 34 }, + { fname: 'Robb', lname: 'Stark', age: 16 }, + { fname: 'Bran', lname: 'Stark', age: 8 } + ]) }); }, teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); QUnit.test('filtering and sorting can be combined', function() { - run(function() { - items = get(obj, 'items'); - sorted = get(obj, 'sortedLannisters'); - }); + var items = obj.get('items'); - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Jaime'], 'precond - array is initially filtered and sorted'); + deepEqual(obj.get('sortedLannisters').mapBy('fname'), ['Cersei', 'Jaime'], 'precond - array is initially filtered and sorted'); - run(function() { - items.pushObject({ fname: 'Tywin', lname: 'Lannister' }); - items.pushObject({ fname: 'Lyanna', lname: 'Stark' }); - items.pushObject({ fname: 'Gerion', lname: 'Lannister' }); - }); + items.pushObject({ fname: 'Tywin', lname: 'Lannister' }); + items.pushObject({ fname: 'Lyanna', lname: 'Stark' }); + items.pushObject({ fname: 'Gerion', lname: 'Lannister' }); - deepEqual(sorted.mapBy('fname'), ['Cersei', 'Gerion', 'Jaime', 'Tywin'], 'updates propagate to array'); + deepEqual(obj.get('sortedLannisters').mapBy('fname'), ['Cersei', 'Gerion', 'Jaime', 'Tywin'], 'updates propagate to array'); }); QUnit.test('filtering, sorting and reduce (max) can be combined', function() { - run(function() { - items = get(obj, 'items'); - }); + var items = obj.get('items'); - equal(16, get(obj, 'oldestStarkAge'), 'precond - end of chain is initially correct'); + equal(16, obj.get('oldestStarkAge'), 'precond - end of chain is initially correct'); - run(function() { - items.pushObject({ fname: 'Rickon', lname: 'Stark', age: 5 }); - }); + items.pushObject({ fname: 'Rickon', lname: 'Stark', age: 5 }); - equal(16, get(obj, 'oldestStarkAge'), 'chain is updated correctly'); + equal(16, obj.get('oldestStarkAge'), 'chain is updated correctly'); - run(function() { - items.pushObject({ fname: 'Eddard', lname: 'Stark', age: 35 }); - }); + items.pushObject({ fname: 'Eddard', lname: 'Stark', age: 35 }); - equal(35, get(obj, 'oldestStarkAge'), 'chain is updated correctly'); + equal(35, obj.get('oldestStarkAge'), 'chain is updated correctly'); }); function todo(name, priority) { @@ -1507,127 +1359,100 @@ function evenPriorities(todo) { QUnit.module('Ember.arrayComputed - chains', { setup() { obj = EmberObject.extend({ - filtered: computedFilter('sorted.@each.priority', evenPriorities), - sorted: computedSort('todos.@each.priority', priorityComparator) + sorted: sort('todos.@each.priority', priorityComparator), + filtered: filter('sorted.@each.priority', evenPriorities) }).create({ - todos: Ember.A([todo('E', 4), todo('D', 3), todo('C', 2), todo('B', 1), todo('A', 0)]) + todos: Ember.A([ + todo('E', 4), + todo('D', 3), + todo('C', 2), + todo('B', 1), + todo('A', 0) + ]) }); }, teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); QUnit.test('it can filter and sort when both depend on the same item property', function() { - run(function() { - filtered = get(obj, 'filtered'); - sorted = get(obj, 'sorted'); - todos = get(obj, 'todos'); - }); + deepEqual(obj.get('todos').mapBy('name'), ['E', 'D', 'C', 'B', 'A'], 'precond - todos initially correct'); + deepEqual(obj.get('sorted').mapBy('name'), ['A', 'B', 'C', 'D', 'E'], 'precond - sorted initially correct'); + deepEqual(obj.get('filtered').mapBy('name'), ['A', 'C', 'E'], 'precond - filtered initially correct'); - deepEqual(todos.mapProperty('name'), ['E', 'D', 'C', 'B', 'A'], 'precond - todos initially correct'); - deepEqual(sorted.mapProperty('name'), ['A', 'B', 'C', 'D', 'E'], 'precond - sorted initially correct'); - deepEqual(filtered.mapProperty('name'), ['A', 'C', 'E'], 'precond - filtered initially correct'); - - run(function() { - beginPropertyChanges(); - // here we trigger several changes - // A. D.priority 3 -> 6 - // 1. updated sorted from item property change - // a. remove D; reinsert D - // b. update filtered from sorted change - // 2. update filtered from item property change - // - // If 1.b happens before 2 it should invalidate 2 - todos.objectAt(1).set('priority', 6); - endPropertyChanges(); - }); + set(obj.get('todos')[1], 'priority', 6); - deepEqual(todos.mapProperty('name'), ['E', 'D', 'C', 'B', 'A'], 'precond - todos remain correct'); - deepEqual(sorted.mapProperty('name'), ['A', 'B', 'C', 'E', 'D'], 'precond - sorted updated correctly'); - deepEqual(filtered.mapProperty('name'), ['A', 'C', 'E', 'D'], 'filtered updated correctly'); + deepEqual(obj.get('todos').mapBy('name'), ['E', 'D', 'C', 'B', 'A'], 'precond - todos remain correct'); + deepEqual(obj.get('sorted').mapBy('name'), ['A', 'B', 'C', 'E', 'D'], 'precond - sorted updated correctly'); + deepEqual(obj.get('filtered').mapBy('name'), ['A', 'C', 'E', 'D'], 'filtered updated correctly'); }); +var userFnCalls; QUnit.module('Chaining array and reduced CPs', { setup() { - run(function() { - userFnCalls = 0; - obj = EmberObject.extend({ - mapped: computedMapBy('array', 'v'), - max: computedMax('mapped'), - maxDidChange: observer('max', function() { - userFnCalls++; - }) - }).create({ - array: Ember.A([{ v: 1 }, { v: 3 }, { v: 2 }, { v: 1 }]) - }); + userFnCalls = 0; + obj = EmberObject.extend({ + mapped: mapBy('array', 'v'), + max: max('mapped'), + maxDidChange: observer('max', () => userFnCalls++) + }).create({ + array: Ember.A([ + { v: 1 }, + { v: 3 }, + { v: 2 }, + { v: 1 } + ]) }); }, teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); QUnit.test('it computes interdependent array computed properties', function() { - get(obj, 'mapped'); - - equal(get(obj, 'max'), 3, 'sanity - it properly computes the maximum value'); - equal(userFnCalls, 0, 'observer is not called on initialisation'); + equal(obj.get('max'), 3, 'sanity - it properly computes the maximum value'); var calls = 0; - addObserver(obj, 'max', function() { - calls++; - }); - run(function() { - obj.get('array').pushObject({ v: 5 }); - }); + addObserver(obj, 'max', () => calls++); + + obj.get('array').pushObject({ v: 5 }); - equal(get(obj, 'max'), 5, 'maximum value is updated correctly'); + equal(obj.get('max'), 5, 'maximum value is updated correctly'); equal(userFnCalls, 1, 'object defined observers fire'); equal(calls, 1, 'runtime created observers fire'); }); -QUnit.module('computedSum', { +QUnit.module('sum', { setup() { - run(function() { - obj = EmberObject.extend({ - total: computedSum('array') - }).create({ - array: Ember.A([1, 2, 3]) - }); + obj = EmberObject.extend({ + total: sum('array') + }).create({ + array: Ember.A([1, 2, 3]) }); }, + teardown() { - run(function() { - obj.destroy(); - }); + Ember.run(obj, 'destroy'); } }); +QUnit.test('sum is readOnly', function() { + QUnit.throws(function() { + obj.set('total', 1); + }, /Cannot set read-only property "total" on object:/); +}); QUnit.test('sums the values in the dependentKey', function() { - var sum = get(obj, 'total'); - equal(sum, 6, 'sums the values'); + equal(obj.get('total'), 6, 'sums the values'); }); QUnit.test('updates when array is modified', function() { - var sum = function() { - return get(obj, 'total'); - }; + obj.get('array').pushObject(1); - run(function() { - get(obj, 'array').pushObject(1); - }); - - equal(sum(), 7, 'recomputed when elements are added'); + equal(obj.get('total'), 7, 'recomputed when elements are added'); - run(function() { - get(obj, 'array').popObject(); - }); + obj.get('array').popObject(); - equal(sum(), 6, 'recomputes when elements are removed'); + equal(obj.get('total'), 6, 'recomputes when elements are removed'); }); diff --git a/packages/ember-runtime/tests/computed/reduce_computed_test.js b/packages/ember-runtime/tests/computed/reduce_computed_test.js deleted file mode 100644 index d7f62b55142..00000000000 --- a/packages/ember-runtime/tests/computed/reduce_computed_test.js +++ /dev/null @@ -1,1078 +0,0 @@ -import Ember from 'ember-metal/core'; -import { - get, - getWithDefault -} from 'ember-metal/property_get'; -import { set } from 'ember-metal/property_set'; -import { meta as metaFor } from 'ember-metal/utils'; -import run from 'ember-metal/run_loop'; -import { observer } from 'ember-metal/mixin'; -import EmberObject from 'ember-runtime/system/object'; -import { - ComputedProperty, - computed -} from 'ember-metal/computed'; -import { arrayComputed } from 'ember-runtime/computed/array_computed'; -import { reduceComputed } from 'ember-runtime/computed/reduce_computed'; -import ArrayProxy from 'ember-runtime/system/array_proxy'; -import SubArray from 'ember-runtime/system/subarray'; - -var obj, addCalls, removeCalls, callbackItems, shared; - -QUnit.module('arrayComputed - [DEPRECATED]', { - setup() { - addCalls = removeCalls = 0; - - expectDeprecation(function() { - - obj = EmberObject.extend({ - // Users would obviously just use `Ember.computed.map` - // This implementation is fine for these tests, but doesn't properly work as - // it's not index based. - evenNumbers: arrayComputed('numbers', { - addedItem(array, item) { - addCalls++; - if (item % 2 === 0) { - array.pushObject(item); - } - return array; - }, - removedItem(array, item) { - removeCalls++; - array.removeObject(item); - return array; - } - }), - - evenNumbersMultiDep: arrayComputed('numbers', 'otherNumbers', { - addedItem(array, item) { - if (item % 2 === 0) { - array.pushObject(item); - } - return array; - } - }), - - evenNestedNumbers: arrayComputed({ - addedItem(array, item, keyName) { - var value = item.get('v'); - if (value % 2 === 0) { - array.pushObject(value); - } - return array; - }, - removedItem(array, item, keyName) { - array.removeObject(item.get('v')); - return array; - } - }).property('nestedNumbers.@each.v') - }).create({ - numbers: Ember.A([1, 2, 3, 4, 5, 6]), - otherNumbers: Ember.A([7, 8, 9]), - nestedNumbers: Ember.A([1,2,3,4,5,6].map((n) => EmberObject.create({ p: 'otherProperty', v: n }))) - }); - - }, 'Ember.arrayComputed is deprecated. Replace it with plain array methods'); - - }, - - teardown() { - run(function() { - obj.destroy(); - }); - } -}); - -QUnit.test('reduceComputed is deprecated', function() { - expectDeprecation(/Ember.reduceComputed is deprecated/); - reduceComputed({ initialValue: 0 }); -}); - -QUnit.test('arrayComputed is deprecated', function() { - expectDeprecation(/Ember.arrayComputed is deprecated/); - arrayComputed({}); -}); - -QUnit.test('array computed properties are instances of ComputedProperty', function() { - expectDeprecation(/Ember.arrayComputed is deprecated/); - - ok(arrayComputed({}) instanceof ComputedProperty); -}); - -QUnit.test('when the dependent array is null or undefined, `addedItem` is not called and only the initial value is returned', function() { - expectDeprecation(/Ember.arrayComputed is deprecated/); - - obj = EmberObject.extend({ - doubledNumbers: arrayComputed('numbers', { - addedItem(array, n) { - addCalls++; - array.pushObject(n * 2); - return array; - } - }) - }).create({ - numbers: null - }); - - deepEqual(get(obj, 'doubledNumbers'), [], 'When the dependent array is null, the initial value is returned'); - equal(addCalls, 0, '`addedItem` is not called when the dependent array is null'); - - run(function() { - set(obj, 'numbers', Ember.A([1,2])); - }); - - deepEqual(get(obj, 'doubledNumbers'), [2,4], 'An initially null dependent array can still be set later'); - equal(addCalls, 2, '`addedItem` is called when the dependent array is initially set'); -}); - -QUnit.test('on first retrieval, array computed properties are computed', function() { - deepEqual(get(obj, 'evenNumbers'), [2,4,6], 'array computed properties are correct on first invocation'); -}); - -QUnit.test('on first retrieval, array computed properties with multiple dependent keys are computed', function() { - deepEqual(get(obj, 'evenNumbersMultiDep'), [2, 4, 6, 8], 'array computed properties are correct on first invocation'); -}); - -QUnit.test('on first retrieval, array computed properties dependent on nested objects are computed', function() { - deepEqual(get(obj, 'evenNestedNumbers'), [2,4,6], 'array computed properties are correct on first invocation'); -}); - -QUnit.test('after the first retrieval, array computed properties observe additions to dependent arrays', function() { - var numbers = get(obj, 'numbers'); - // set up observers - var evenNumbers = get(obj, 'evenNumbers'); - - run(function() { - numbers.pushObjects([7, 8]); - }); - - deepEqual(evenNumbers, [2, 4, 6, 8], 'array computed properties watch dependent arrays'); -}); - -QUnit.test('after the first retrieval, array computed properties observe removals from dependent arrays', function() { - var numbers = get(obj, 'numbers'); - // set up observers - var evenNumbers = get(obj, 'evenNumbers'); - - run(function() { - numbers.removeObjects([3, 4]); - }); - - deepEqual(evenNumbers, [2, 6], 'array computed properties watch dependent arrays'); -}); - -QUnit.test('after first retrieval, array computed properties can observe properties on array items', function() { - var nestedNumbers = get(obj, 'nestedNumbers'); - var evenNestedNumbers = get(obj, 'evenNestedNumbers'); - - deepEqual(evenNestedNumbers, [2, 4, 6], 'precond -- starts off with correct values'); - - run(function() { - nestedNumbers.objectAt(0).set('v', 22); - }); - - deepEqual(nestedNumbers.mapBy('v'), [22, 2, 3, 4, 5, 6], 'nested numbers is updated'); - deepEqual(evenNestedNumbers, [2, 4, 6, 22], 'adds new number'); -}); - -QUnit.test('changes to array computed properties happen synchronously', function() { - var nestedNumbers = get(obj, 'nestedNumbers'); - var evenNestedNumbers = get(obj, 'evenNestedNumbers'); - - deepEqual(evenNestedNumbers, [2, 4, 6], 'precond -- starts off with correct values'); - - run(function() { - nestedNumbers.objectAt(0).set('v', 22); - deepEqual(nestedNumbers.mapBy('v'), [22, 2, 3, 4, 5, 6], 'nested numbers is updated'); - deepEqual(evenNestedNumbers, [2, 4, 6, 22], 'adds new number'); - }); -}); - -QUnit.test('multiple dependent keys can be specified via brace expansion', function() { - var obj = EmberObject.extend({ - foo: reduceComputed({ - initialValue: Ember.A(), - addedItem(array, item) { - array.pushObject('a:' + item); - return array; - }, - removedItem(array, item) { - array.pushObject('r:' + item); - return array; - } - }).property('{bar,baz}') - }).create({ - bar: Ember.A(), - baz: Ember.A() - }); - - deepEqual(get(obj, 'foo'), [], 'initially empty'); - - get(obj, 'bar').pushObject(1); - - deepEqual(get(obj, 'foo'), ['a:1'], 'added item from brace-expanded dependency'); - - get(obj, 'baz').pushObject(2); - - deepEqual(get(obj, 'foo'), ['a:1', 'a:2'], 'added item from brace-expanded dependency'); - - get(obj, 'bar').popObject(); - - deepEqual(get(obj, 'foo'), ['a:1', 'a:2', 'r:1'], 'removed item from brace-expanded dependency'); - - get(obj, 'baz').popObject(); - - deepEqual(get(obj, 'foo'), ['a:1', 'a:2', 'r:1', 'r:2'], 'removed item from brace-expanded dependency'); -}); - -QUnit.test('multiple item property keys can be specified via brace expansion', function() { - var expected = Ember.A(); - var item = { propA: 'A', propB: 'B', propC: 'C' }; - var obj = EmberObject.extend({ - foo: reduceComputed({ - initialValue: Ember.A(), - addedItem(array, item, changeMeta) { - array.pushObject('a:' + get(item, 'propA') + ':' + get(item, 'propB') + ':' + get(item, 'propC')); - return array; - }, - removedItem(array, item, changeMeta) { - array.pushObject('r:' + get(item, 'propA') + ':' + get(item, 'propB') + ':' + get(item, 'propC')); - return array; - } - }).property('bar.@each.{propA,propB}') - }).create({ - bar: Ember.A([item]) - }); - - expected.pushObjects(['a:A:B:C']); - deepEqual(get(obj, 'foo'), expected, 'initially added dependent item'); - - set(item, 'propA', 'AA'); - - expected.pushObjects(['r:AA:B:C', 'a:AA:B:C']); - deepEqual(get(obj, 'foo'), expected, 'observing item property key specified via brace expansion'); - - set(item, 'propB', 'BB'); - - expected.pushObjects(['r:AA:BB:C', 'a:AA:BB:C']); - deepEqual(get(obj, 'foo'), expected, 'observing item property key specified via brace expansion'); - - set(item, 'propC', 'CC'); - - deepEqual(get(obj, 'foo'), expected, 'not observing unspecified item properties'); -}); - -QUnit.test('doubly nested item property keys (@each.foo.@each) are not supported', function() { - run(function() { - obj = EmberObject.extend({ - people: arrayComputed({ - addedItem(array, item) { - array.pushObject(get(item, 'first.firstObject')); - return array; - } - }).property('peopleByOrdinalPosition.@each.first'), - names: arrayComputed({ - addedItem(array, item) { - equal(get(item, 'name'), 'Jaime Lannister'); - array.pushObject(item.get('name')); - return array; - } - }).property('people.@each.name') - }).create({ - peopleByOrdinalPosition: Ember.A([{ first: Ember.A([EmberObject.create({ name: 'Jaime Lannister' })]) }]) - }); - }); - - equal(obj.get('names.firstObject'), 'Jaime Lannister', 'Doubly nested item properties can be retrieved manually'); - - throws(function() { - obj = EmberObject.extend({ - names: arrayComputed({ - addedItem(array, item) { - array.pushObject(item); - return array; - } - }).property('people.@each.first.@each.name') - }).create({ - people: [{ first: Ember.A([EmberObject.create({ name: 'Jaime Lannister' })]) }] - }); - }, /Nested @each/, 'doubly nested item property keys are not supported'); -}); - -QUnit.test('after the first retrieval, array computed properties observe dependent arrays', function() { - get(obj, 'numbers'); - var evenNumbers = get(obj, 'evenNumbers'); - - deepEqual(evenNumbers, [2, 4, 6], 'precond -- starts off with correct values'); - - run(function() { - set(obj, 'numbers', Ember.A([20, 23, 28])); - }); - - deepEqual(evenNumbers, [20, 28], 'array computed properties watch dependent arrays'); -}); - -QUnit.test('array observers are torn down when dependent arrays change', function() { - var numbers = get(obj, 'numbers'); - get(obj, 'evenNumbers'); - - equal(addCalls, 6, 'precond - add has been called for each item in the array'); - equal(removeCalls, 0, 'precond - removed has not been called'); - - run(function() { - set(obj, 'numbers', Ember.A([20, 23, 28])); - }); - - equal(addCalls, 9, 'add is called for each item in the new array'); - equal(removeCalls, 0, 'remove is not called when the array is reset'); - - numbers.replace(0, numbers.get('length'), Ember.A([7,8,9,10])); - - equal(addCalls, 9, 'add is not called'); - equal(removeCalls, 0, 'remove is not called'); -}); - -QUnit.test('modifying properties on dependent array items triggers observers exactly once', function() { - var numbers = get(obj, 'numbers'); - var evenNumbers = get(obj, 'evenNumbers'); - - equal(addCalls, 6, 'precond - add has not been called for each item in the array'); - equal(removeCalls, 0, 'precond - removed has not been called'); - - run(function() { - numbers.replace(0, 2, [7,8,9,10]); - }); - - equal(addCalls, 10, 'add is called for each item added'); - equal(removeCalls, 2, 'removed is called for each item removed'); - deepEqual(evenNumbers, [4,6,8,10], 'sanity check - dependent arrays are updated'); -}); - -QUnit.test('multiple array computed properties on the same object can observe dependent arrays', function() { - var numbers = get(obj, 'numbers'); - var otherNumbers = get(obj, 'otherNumbers'); - - deepEqual(get(obj, 'evenNumbers'), [2,4,6], 'precond - evenNumbers is initially correct'); - deepEqual(get(obj, 'evenNumbersMultiDep'), [2, 4, 6, 8], 'precond - evenNumbersMultiDep is initially correct'); - - run(function() { - numbers.pushObject(12); - otherNumbers.pushObject(14); - }); - - deepEqual(get(obj, 'evenNumbers'), [2,4,6,12], 'evenNumbers is updated'); - deepEqual(get(obj, 'evenNumbersMultiDep'), [2, 4, 6, 8, 12, 14], 'evenNumbersMultiDep is updated'); -}); - -QUnit.test('an error is thrown when a reduceComputed is defined without an initialValue property', function() { - var defineExploder = function() { - EmberObject.extend({ - exploder: reduceComputed('collection', { - initialize(initialValue, changeMeta, instanceMeta) {}, - - addedItem(accumulatedValue, item, changeMeta, instanceMeta) { - return item; - }, - - removedItem(accumulatedValue, item, changeMeta, instanceMeta) { - return item; - } - }) - }).create({ - collection: Ember.A() - }); - }; - - throws(defineExploder, /declared\ without\ an\ initial\ value/, 'an error is thrown when the reduceComputed is defined without an initialValue'); -}); - -QUnit.test('dependent arrays with multiple item properties are not double-counted', function() { - var obj = EmberObject.extend({ - items: Ember.A([{ foo: true }, { bar: false }, { bar: true }]), - countFooOrBar: reduceComputed({ - initialValue: 0, - addedItem(acc) { - ++addCalls; - return acc; - }, - - removedItem(acc) { - ++removeCalls; - return acc; - } - }).property('items.@each.foo', 'items.@each.bar', 'items') - }).create(); - - equal(0, addCalls, 'precond - no adds yet'); - equal(0, removeCalls, 'precond - no removes yet'); - - get(obj, 'countFooOrBar'); - - equal(3, addCalls, 'all items added once'); - equal(0, removeCalls, 'no removes yet'); -}); - -QUnit.test('dependent arrays can use `replace` with an out of bounds index to add items', function() { - var dependentArray = Ember.A(); - var array; - - obj = EmberObject.extend({ - dependentArray: dependentArray, - computed: arrayComputed('dependentArray', { - addedItem(acc, item, changeMeta) { - acc.insertAt(changeMeta.index, item); - return acc; - }, - removedItem(acc) { return acc; } - }) - }).create(); - - array = get(obj, 'computed'); - - deepEqual(array, [], 'precond - computed array is initially empty'); - - dependentArray.replace(100, 0, [1, 2]); - - deepEqual(array, [1, 2], 'index >= length treated as a push'); - - dependentArray.replace(-100, 0, [3, 4]); - - deepEqual(array, [3, 4, 1, 2], 'index < 0 treated as an unshift'); -}); - -QUnit.test('dependent arrays can use `replace` with a negative index to remove items indexed from the right', function() { - var dependentArray = Ember.A([1,2,3,4,5]); - var array; - - obj = EmberObject.extend({ - dependentArray: dependentArray, - computed: arrayComputed('dependentArray', { - addedItem(acc, item) { return acc; }, - removedItem(acc, item) { - acc.pushObject(item); - return acc; - } - }) - }).create(); - - array = get(obj, 'computed'); - - deepEqual(array, [], 'precond - no items have been removed initially'); - - dependentArray.replace(-3, 2); - - deepEqual(array, [4,3], 'index < 0 used as a right index for removal'); -}); - -QUnit.test('dependent arrays that call `replace` with an out of bounds index to remove items is a no-op', function() { - var dependentArray = Ember.A([1, 2]); - var array; - - obj = EmberObject.extend({ - dependentArray: dependentArray, - computed: arrayComputed('dependentArray', { - addedItem(acc, item, changeMeta) { return acc; }, - removedItem(acc) { - ok(false, 'no items have been removed'); - } - }) - }).create(); - - array = get(obj, 'computed'); - - deepEqual(array, [], 'precond - computed array is initially empty'); - - dependentArray.replace(100, 2); -}); - -QUnit.test('dependent arrays that call `replace` with a too-large removedCount a) works and b) still right-truncates', function() { - var dependentArray = Ember.A([1, 2]); - var array; - - obj = EmberObject.extend({ - dependentArray: dependentArray, - computed: arrayComputed('dependentArray', { - addedItem(acc, item) { return acc; }, - removedItem(acc, item) { - acc.pushObject(item); - return acc; - } - }) - }).create(); - - array = get(obj, 'computed'); - - deepEqual(array, [], 'precond - computed array is initially empty'); - - dependentArray.replace(1, 200); - - deepEqual(array, [2], 'array was correctly right-truncated'); -}); - -QUnit.test('removedItem is not erroneously called for dependent arrays during a recomputation', function() { - function addedItem(array, item, changeMeta) { - array.insertAt(changeMeta.index, item); - return array; - } - - function removedItem(array, item, changeMeta) { - ok(get(array, 'length') > changeMeta.index, 'removedItem not called with invalid index'); - array.removeAt(changeMeta.index, 1); - return array; - } - - var options = { - addedItem: addedItem, - removedItem: removedItem - }; - - obj = EmberObject.extend({ - dependentArray: Ember.A([1, 2]), - identity0: arrayComputed('dependentArray', options), - identity1: arrayComputed('identity0', options) - }).create(); - - get(obj, 'identity1'); - run(function() { - obj.notifyPropertyChange('dependentArray'); - }); - - ok(true, 'removedItem not invoked with invalid index'); -}); - -QUnit.module('arrayComputed - recomputation DKs', { - setup() { - expectDeprecation(function() { - - obj = EmberObject.extend({ - people: Ember.A([{ - name: 'Jaime Lannister', - title: 'Kingsguard' - }, { - name: 'Cersei Lannister', - title: 'Queen' - }]), - - titles: arrayComputed('people', { - addedItem(acc, person) { - acc.pushObject(get(person, 'title')); - return acc; - } - }) - }).create(); - }, 'Ember.arrayComputed is deprecated. Replace it with plain array methods'); - - }, - teardown() { - run(function() { - obj.destroy(); - }); - } -}); - -QUnit.test('recomputations from `arrayComputed` observers add back dependent keys', function() { - var meta = metaFor(obj); - get(obj, 'people'); - var titles; - - equal(meta.deps, undefined, 'precond - nobody depends on people\''); - equal(meta.watching.people, undefined, 'precond - nobody is watching people'); - - titles = get(obj, 'titles'); - - deepEqual(titles, ['Kingsguard', 'Queen'], 'precond - value is correct'); - - ok(meta.deps.people !== undefined, 'people has dependencies'); - deepEqual(Object.keys(meta.deps.people), ['titles'], 'only titles depends on people'); - equal(meta.deps.people.titles, 1, 'titles depends on people exactly once'); - equal(meta.watching.people, 2, 'people has two watchers: the array listener and titles'); - - run(function() { - set(obj, 'people', Ember.A()); - }); - - // Regular CPs are invalidated when their dependent keys change, but array - // computeds keep refs up to date - deepEqual(titles, [], 'value is correct'); - equal(meta.cache.titles, titles, 'value remains cached'); - ok(meta.deps.people !== undefined, 'people has dependencies'); - deepEqual(Object.keys(meta.deps.people), ['titles'], 'meta.deps.people is unchanged'); - equal(meta.deps.people.titles, 1, 'deps.people.titles is unchanged'); - equal(meta.watching.people, 2, 'watching.people is unchanged'); -}); - -QUnit.module('Ember.arryComputed - self chains', { - setup() { - var a = EmberObject.create({ name: 'a' }); - var b = EmberObject.create({ name: 'b' }); - - expectDeprecation(function() { - - obj = ArrayProxy.extend({ - names: arrayComputed('@this.@each.name', { - addedItem(array, item, changeMeta, instanceMeta) { - var mapped = get(item, 'name'); - array.insertAt(changeMeta.index, mapped); - return array; - }, - removedItem(array, item, changeMeta, instanceMeta) { - array.removeAt(changeMeta.index, 1); - return array; - } - }) - }).create({ - content: Ember.A([a, b]) - }); - }, 'Ember.arrayComputed is deprecated. Replace it with plain array methods'); - }, - teardown() { - run(function() { - obj.destroy(); - }); - } -}); - -QUnit.test('@this can be used to treat the object as the array itself', function() { - var names = get(obj, 'names'); - - deepEqual(names, ['a', 'b'], 'precond - names is initially correct'); - - run(function() { - obj.objectAt(1).set('name', 'c'); - }); - - deepEqual(names, ['a', 'c'], '@this can be used with item property observers'); - - run(function() { - obj.pushObject({ name: 'd' }); - }); - - deepEqual(names, ['a', 'c', 'd'], '@this observes new items'); -}); - -QUnit.module('arrayComputed - changeMeta property observers', { - setup() { - callbackItems = []; - run(function() { - expectDeprecation(function() { - obj = EmberObject.extend({ - itemsN: arrayComputed('items.@each.n', { - addedItem(array, item, changeMeta, instanceMeta) { - callbackItems.push('add:' + changeMeta.index + ':' + get(changeMeta.item, 'n')); - }, - removedItem(array, item, changeMeta, instanceMeta) { - callbackItems.push('remove:' + changeMeta.index + ':' + get(changeMeta.item, 'n')); - } - }) - }).create({ - items: Ember.A([EmberObject.create({ n: 'zero' }), EmberObject.create({ n: 'one' })]) - }); - }, 'Ember.arrayComputed is deprecated. Replace it with plain array methods'); - }); - }, - teardown() { - run(function() { - obj.destroy(); - }); - } -}); - -QUnit.test('changeMeta includes item and index', function() { - var expected, items, item; - - items = get(obj, 'items'); - - // initial computation add0 add1 - run(function() { - obj.get('itemsN'); - }); - - // add2 - run(function() { - items.pushObject(EmberObject.create({ n: 'two' })); - }); - - // remove2 - run(function() { - items.popObject(); - }); - - // remove0 add0 - run(function() { - set(get(items, 'firstObject'), 'n', 'zero\''); - }); - - expected = ['add:0:zero', 'add:1:one', 'add:2:two', 'remove:2:two', 'remove:0:zero\'', 'add:0:zero\'']; - deepEqual(callbackItems, expected, 'changeMeta includes items'); - - // [zero', one] -> [zero', one, five, six] - // add2 add3 - run(function() { - items.pushObject(EmberObject.create({ n: 'five' })); - items.pushObject(EmberObject.create({ n: 'six' })); - }); - - // remove0 add0 - run(function() { - items.objectAt(0).set('n', 'zero\'\''); - }); - - expected = expected.concat(['add:2:five', 'add:3:six', 'remove:0:zero\'\'', 'add:0:zero\'\'']); - deepEqual(callbackItems, expected, 'changeMeta includes items'); - - // [zero'', one, five, six] -> [zero'', five, six] - // remove1 - run(function() { - item = items.objectAt(1); - items.removeAt(1, 1); - }); - - run(function() { - // observer should have been removed from the deleted item - item.set('n', 'ten thousand'); - }); - - // [zero'', five, six] -> [zero'', five, seven] - // remove2 add2 - run(function() { - items.objectAt(2).set('n', 'seven'); - }); - - // observer should have been added to the new item - expected = expected.concat(['remove:1:one', 'remove:2:seven', 'add:2:seven']); - deepEqual(callbackItems, expected, 'changeMeta includes items'); - - // reset (does not call remove) - run(function() { - item = items.objectAt(1); - set(obj, 'items', Ember.A([])); - }); - - run(function() { - // observers should have been removed from the items in the old array - set(item, 'n', 'eleven thousand'); - }); - - deepEqual(callbackItems, expected, 'items removed from the array had observers removed'); -}); - -QUnit.test('changeMeta includes changedCount and arrayChanged', function() { - var obj = EmberObject.extend({ - lettersArrayComputed: arrayComputed('letters', { - addedItem(array, item, changeMeta, instanceMeta) { - callbackItems.push('add:' + changeMeta.changedCount + ':' + changeMeta.arrayChanged.join('')); - }, - removedItem(array, item, changeMeta, instanceMeta) { - callbackItems.push('remove:' + changeMeta.changedCount + ':' + changeMeta.arrayChanged.join('')); - } - }) - }).create({ - letters: Ember.A(['a', 'b']) - }); - - var letters = get(obj, 'letters'); - - obj.get('lettersArrayComputed'); - letters.pushObject('c'); - letters.popObject(); - letters.replace(0, 1, ['d']); - letters.removeAt(0, letters.length); - - var expected = ['add:2:ab', 'add:2:ab', 'add:1:abc', 'remove:1:abc', 'remove:1:ab', 'add:1:db', 'remove:2:db', 'remove:2:db']; - deepEqual(callbackItems, expected, 'changeMeta has count and changed'); -}); - -QUnit.test('`updateIndexes` is not over-eager about skipping retain:n (#4620)', function() { - var tracked = Ember.A(); - obj = EmberObject.extend({ - content: Ember.A([{ n: 'one' }, { n: 'two' }]), - items: arrayComputed('content.@each.n', { - addedItem(array, item, changeMeta) { - tracked.push('+' + get(item, 'n') + '@' + changeMeta.index); - array.insertAt(changeMeta.index, item); - return array; - }, - removedItem(array, item, changeMeta) { - tracked.push('-' + (changeMeta.previousValues ? changeMeta.previousValues.n : get(item, 'n')) + '@' + changeMeta.index); - array.removeAt(changeMeta.index); - return array; - } - }) - }).create(); - - run(function () { - obj.get('items'); - }); - - deepEqual(tracked, ['+one@0', '+two@1'], 'precond - array is set up correctly'); - - run(function () { - obj.get('content').shiftObject(); - }); - - deepEqual(tracked, ['+one@0', '+two@1', '-one@0'], 'array handles unshift correctly'); - - run(function () { - set(obj, 'content.lastObject.n', 'three'); - }); - - deepEqual(tracked, ['+one@0', '+two@1', '-one@0', '-two@0', '+three@0'], 'array handles a change when operations are delete:m retain:n-m'); -}); - -QUnit.test('when initialValue is undefined, everything works as advertised', function() { - var chars = EmberObject.extend({ - firstUpper: reduceComputed('letters', { - initialValue: undefined, - - initialize(initialValue, changeMeta, instanceMeta) { - instanceMeta.matchingItems = Ember.A(); - instanceMeta.subArray = new SubArray(); - instanceMeta.firstMatch = function() { - return getWithDefault(instanceMeta.matchingItems, 'firstObject', initialValue); - }; - }, - - addedItem(accumulatedValue, item, changeMeta, instanceMeta) { - var filterIndex; - filterIndex = instanceMeta.subArray.addItem(changeMeta.index, item.toUpperCase() === item); - if (filterIndex > -1) { - instanceMeta.matchingItems.insertAt(filterIndex, item); - } - return instanceMeta.firstMatch(); - }, - - removedItem(accumulatedValue, item, changeMeta, instanceMeta) { - var filterIndex = instanceMeta.subArray.removeItem(changeMeta.index); - if (filterIndex > -1) { - instanceMeta.matchingItems.removeAt(filterIndex); - } - return instanceMeta.firstMatch(); - } - }) - }).create({ - letters: Ember.A() - }); - equal(get(chars, 'firstUpper'), undefined, 'initialValue is undefined'); - - get(chars, 'letters').pushObjects(['a', 'b', 'c']); - - equal(get(chars, 'firstUpper'), undefined, 'result is undefined when no matches are present'); - - get(chars, 'letters').pushObjects(['A', 'B', 'C']); - - equal(get(chars, 'firstUpper'), 'A', 'result is the first match when matching objects are present'); - - get(chars, 'letters').removeAt(3); - - equal(get(chars, 'firstUpper'), 'B', 'result is the next match when the first matching object is removed'); -}); - -QUnit.module('arrayComputed - completely invalidating dependencies', { - setup() { - addCalls = removeCalls = 0; - } -}); - -QUnit.test('non-array dependencies completely invalidate a reduceComputed CP', function() { - var dependentArray = Ember.A(); - - expectDeprecation(/Ember.arrayComputed is deprecated/); - - obj = EmberObject.extend({ - nonArray: 'v0', - dependentArray: dependentArray, - - computed: arrayComputed('dependentArray', 'nonArray', { - addedItem(array) { - ++addCalls; - return array; - }, - - removedItem(array) { - --removeCalls; - return array; - } - }) - }).create(); - - get(obj, 'computed'); - - equal(addCalls, 0, 'precond - add has not initially been called'); - equal(removeCalls, 0, 'precond - remove has not initially been called'); - - dependentArray.pushObjects([1, 2]); - - equal(addCalls, 2, 'add called one-at-a-time for dependent array changes'); - equal(removeCalls, 0, 'remove not called'); - - run(function() { - set(obj, 'nonArray', 'v1'); - }); - - equal(addCalls, 4, 'array completely recomputed when non-array dependency changed'); - equal(removeCalls, 0, 'remove not called'); -}); - -QUnit.test('array dependencies specified with `.[]` completely invalidate a reduceComputed CP', function() { - expectDeprecation(/Ember.arrayComputed is deprecated/); - - var dependentArray = Ember.A(); - var totallyInvalidatingDependentArray = Ember.A(); - - obj = EmberObject.extend({ - totallyInvalidatingDependentArray: totallyInvalidatingDependentArray, - dependentArray: dependentArray, - - computed: arrayComputed('dependentArray', 'totallyInvalidatingDependentArray.[]', { - addedItem(array, item) { - ok(item !== 3, 'totally invalidating items are never passed to the one-at-a-time callbacks'); - ++addCalls; - return array; - }, - - removedItem(array, item) { - ok(item !== 3, 'totally invalidating items are never passed to the one-at-a-time callbacks'); - --removeCalls; - return array; - } - }) - }).create(); - - get(obj, 'computed'); - - equal(addCalls, 0, 'precond - add has not initially been called'); - equal(removeCalls, 0, 'precond - remove has not initially been called'); - - dependentArray.pushObjects([1, 2]); - - equal(addCalls, 2, 'add called one-at-a-time for dependent array changes'); - equal(removeCalls, 0, 'remove not called'); - - run(function() { - totallyInvalidatingDependentArray.pushObject(3); - }); - - equal(addCalls, 4, 'array completely recomputed when totally invalidating dependent array modified'); - equal(removeCalls, 0, 'remove not called'); -}); - -QUnit.test('returning undefined in addedItem/removedItem completely invalidates a reduceComputed CP', function() { - expectDeprecation(/Ember.reduceComputed is deprecated/); - - var dependentArray = Ember.A([3,2,1]); - var counter = 0; - - obj = EmberObject.extend({ - dependentArray: dependentArray, - - computed: reduceComputed('dependentArray', { - initialValue: Infinity, - - addedItem(accumulatedValue, item, changeMeta, instanceMeta) { - return Math.min(accumulatedValue, item); - }, - - removedItem(accumulatedValue, item, changeMeta, instanceMeta) { - if (item > accumulatedValue) { - return accumulatedValue; - } - } - }), - - computedDidChange: observer('computed', function() { - counter++; - }) - }).create(); - - get(obj, 'computed'); - equal(get(obj, 'computed'), 1); - equal(counter, 0); - - dependentArray.pushObject(10); - equal(get(obj, 'computed'), 1); - equal(counter, 0); - - dependentArray.removeObject(10); - equal(get(obj, 'computed'), 1); - equal(counter, 0); - - dependentArray.removeObject(1); - equal(get(obj, 'computed'), 2); - equal(counter, 1); -}); - -if (!Ember.EXTEND_PROTOTYPES && !Ember.EXTEND_PROTOTYPES.Array) { - QUnit.test('reduceComputed complains about array dependencies that are not `Ember.Array`s', function() { - expectDeprecation(/Ember.reduceComputed is deprecated/); - - var Type = EmberObject.extend({ - rc: reduceComputed('array', { - initialValue: 0, - addedItem(v) { - return v; - }, - removedItem(v) { - return v; - } - }) - }); - - expectAssertion(function() { - obj = Type.create({ array: [] }); - get(obj, 'rc'); - }, /must be an `Ember.Array`/, 'Ember.reduceComputed complains about dependent non-extended native arrays'); - }); -} - -QUnit.module('arrayComputed - misc', { - setup() { - callbackItems = []; - - shared = Ember.Object.create({ - flag: false - }); - - var Item = Ember.Object.extend({ - shared: shared, - flag: computed('shared.flag', function () { - return this.get('shared.flag'); - }) - }); - - expectDeprecation(function() { - obj = Ember.Object.extend({ - upstream: Ember.A([ - Item.create(), - Item.create() - ]), - arrayCP: arrayComputed('upstream.@each.flag', { - addedItem(array, item) { - callbackItems.push('add:' + item.get('flag')); - return array; - }, - - removedItem(array, item) { - callbackItems.push('remove:' + item.get('flag')); - return array; - } - }) - }).create(); - }, 'Ember.arrayComputed is deprecated. Replace it with plain array methods'); - }, - - teardown() { - run(function () { - obj.destroy(); - }); - } -}); - -QUnit.test('item property change flushes are gated by a semaphore', function() { - obj.get('arrayCP'); - deepEqual(callbackItems, ['add:false', 'add:false'], 'precond - calls are initially correct'); - - callbackItems.splice(0, 2); - - shared.set('flag', true); - deepEqual(callbackItems, ['remove:true', 'add:true', 'remove:true', 'add:true'], 'item property flushes that depend on a shared prop are gated by a semaphore'); -}); diff --git a/packages/ember-runtime/tests/controllers/item_controller_class_test.js b/packages/ember-runtime/tests/controllers/item_controller_class_test.js index 2d8738474f5..508624abf40 100644 --- a/packages/ember-runtime/tests/controllers/item_controller_class_test.js +++ b/packages/ember-runtime/tests/controllers/item_controller_class_test.js @@ -380,5 +380,7 @@ QUnit.test('item controllers can be used to provide properties for array compute sorted: sort('@this', 'sortProperties') }); - deepEqual(arrayController.get('sorted').mapProperty('model.name'), ['Jaime', 'Cersei'], 'ArrayController items can be sorted on itemController properties'); + var sortedNames = arrayController.get('sorted').mapBy('model.name'); + + deepEqual(sortedNames, ['Jaime', 'Cersei'], 'ArrayController items can be sorted on itemController properties'); }); From 9c52115bf3a2f20bf560dfe8b7b5d26aa1e8372c Mon Sep 17 00:00:00 2001 From: Stefan Penner Date: Thu, 25 Jun 2015 22:47:51 -0700 Subject: [PATCH 2/2] only invoke didChange if a node is still present. --- packages/ember-metal/lib/chains.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ember-metal/lib/chains.js b/packages/ember-metal/lib/chains.js index 662e7333ad2..9da4e325819 100644 --- a/packages/ember-metal/lib/chains.js +++ b/packages/ember-metal/lib/chains.js @@ -354,9 +354,13 @@ ChainNode.prototype = { // then notify chains... var chains = this._chains; + var node; if (chains) { for (var key in chains) { - chains[key].didChange(events); + node = chains[key]; + if (node !== undefined) { + node.didChange(events); + } } }