diff --git a/packages/ember-extension-support/lib/data_adapter.js b/packages/ember-extension-support/lib/data_adapter.js index fe59fe340c5..c364eb2b759 100644 --- a/packages/ember-extension-support/lib/data_adapter.js +++ b/packages/ember-extension-support/lib/data_adapter.js @@ -1,13 +1,12 @@ import { getOwner } from 'ember-utils'; -import { get, run } from 'ember-metal'; +import { get, run, objectAt } from 'ember-metal'; import { String as StringUtils, Namespace, Object as EmberObject, A as emberA, addArrayObserver, - removeArrayObserver, - objectAt + removeArrayObserver } from 'ember-runtime'; /** diff --git a/packages/ember-glimmer/lib/utils/iterable.ts b/packages/ember-glimmer/lib/utils/iterable.ts index a0a60928179..23e0adcd60f 100644 --- a/packages/ember-glimmer/lib/utils/iterable.ts +++ b/packages/ember-glimmer/lib/utils/iterable.ts @@ -6,11 +6,16 @@ import { UpdatableTag, } from '@glimmer/reference'; import { Opaque } from '@glimmer/util'; -import { get, isProxy, tagFor, tagForProperty } from 'ember-metal'; import { - _contentFor, - isEmberArray, + get, + isProxy, objectAt, + tagFor, + tagForProperty +} from 'ember-metal'; +import { + _contentFor, + isEmberArray } from 'ember-runtime'; import { guidFor } from 'ember-utils'; import { isEachIn } from '../helpers/each-in'; diff --git a/packages/ember-metal/lib/array.js b/packages/ember-metal/lib/array.js new file mode 100644 index 00000000000..69d2552c3ff --- /dev/null +++ b/packages/ember-metal/lib/array.js @@ -0,0 +1,7 @@ +export function objectAt(content, idx) { + if (typeof content.objectAt === 'function') { + return content.objectAt(idx); + } else { + return content[idx]; + } +} diff --git a/packages/ember-metal/lib/chains.js b/packages/ember-metal/lib/chains.js index d73ab440841..e6165f036fd 100644 --- a/packages/ember-metal/lib/chains.js +++ b/packages/ember-metal/lib/chains.js @@ -2,6 +2,7 @@ import { get } from './property_get'; import { descriptorFor, meta as metaFor, peekMeta } from './meta'; import { watchKey, unwatchKey } from './watch_key'; import { cacheFor } from './computed'; +import { eachProxyFor } from './each_proxy'; const FIRST_KEY = /^([^\.]+)/; @@ -326,7 +327,9 @@ function lazyGet(obj, key) { } // Use `get` if the return value is an EachProxy or an uncacheable value. - if (isVolatile(obj, key, meta)) { + if (key === '@each') { + return eachProxyFor(obj); + } else if (isVolatile(obj, key, meta)) { return get(obj, key); // Otherwise attempt to get the cached value of the computed property } else { diff --git a/packages/ember-metal/lib/each_proxy.js b/packages/ember-metal/lib/each_proxy.js new file mode 100644 index 00000000000..203c6da2d58 --- /dev/null +++ b/packages/ember-metal/lib/each_proxy.js @@ -0,0 +1,131 @@ +import { assert } from 'ember-debug'; +import { get } from './property_get'; +import { notifyPropertyChange } from './property_events'; +import { addObserver, removeObserver } from './observer'; +import { meta, peekMeta } from './meta'; +import { objectAt } from './array'; + +const EACH_PROXIES = new WeakMap(); + +export function eachProxyFor(array) { + let eachProxy = EACH_PROXIES.get(array); + if (eachProxy === undefined) { + eachProxy = new EachProxy(array); + EACH_PROXIES.set(array, eachProxy); + } + return eachProxy; +} + +export function eachProxyArrayWillChange(array, idx, removedCnt, addedCnt) { + let eachProxy = EACH_PROXIES.get(array); + if (eachProxy !== undefined) { + eachProxy.arrayWillChange(array, idx, removedCnt, addedCnt); + } +} + +export function eachProxyArrayDidChange(array, idx, removedCnt, addedCnt) { + let eachProxy = EACH_PROXIES.get(array); + if (eachProxy !== undefined) { + eachProxy.arrayDidChange(array, idx, removedCnt, addedCnt); + } +} + +class EachProxy { + constructor(content) { + this._content = content; + this._keys = undefined; + meta(this); + } + + // .......................................................... + // ARRAY CHANGES + // Invokes whenever the content array itself changes. + + arrayWillChange(content, idx, removedCnt, addedCnt) { // eslint-disable-line no-unused-vars + let keys = this._keys; + let lim = removedCnt > 0 ? idx + removedCnt : -1; + for (let key in keys) { + if (lim > 0) { + removeObserverForContentKey(content, key, this, idx, lim); + } + } + } + + arrayDidChange(content, idx, removedCnt, addedCnt) { + let keys = this._keys; + let lim = addedCnt > 0 ? idx + addedCnt : -1; + let meta = peekMeta(this); + for (let key in keys) { + if (lim > 0) { + addObserverForContentKey(content, key, this, idx, lim); + } + notifyPropertyChange(this, key, meta); + } + } + + // .......................................................... + // LISTEN FOR NEW OBSERVERS AND OTHER EVENT LISTENERS + // Start monitoring keys based on who is listening... + + willWatchProperty(property) { + this.beginObservingContentKey(property); + } + + didUnwatchProperty(property) { + this.stopObservingContentKey(property); + } + + // .......................................................... + // CONTENT KEY OBSERVING + // Actual watch keys on the source content. + + beginObservingContentKey(keyName) { + let keys = this._keys; + if (!keys) { + keys = this._keys = Object.create(null); + } + + if (!keys[keyName]) { + keys[keyName] = 1; + let content = this._content; + let len = get(content, 'length'); + + addObserverForContentKey(content, keyName, this, 0, len); + } else { + keys[keyName]++; + } + } + + stopObservingContentKey(keyName) { + let keys = this._keys; + if (keys && (keys[keyName] > 0) && (--keys[keyName] <= 0)) { + let content = this._content; + let len = get(content, 'length'); + + removeObserverForContentKey(content, keyName, this, 0, len); + } + } + + contentKeyDidChange(obj, keyName) { + notifyPropertyChange(this, keyName); + } +} + +function addObserverForContentKey(content, keyName, proxy, idx, loc) { + while (--loc >= idx) { + let item = objectAt(content, loc); + if (item) { + assert(`When using @each to observe the array \`${toString(content)}\`, the array must return an object`, typeof item === 'object'); + addObserver(item, keyName, proxy, 'contentKeyDidChange'); + } + } +} + +function removeObserverForContentKey(content, keyName, proxy, idx, loc) { + while (--loc >= idx) { + let item = objectAt(content, loc); + if (item) { + removeObserver(item, keyName, proxy, 'contentKeyDidChange'); + } + } +} diff --git a/packages/ember-metal/lib/index.d.ts b/packages/ember-metal/lib/index.d.ts index 88adb90d73e..6615f388447 100644 --- a/packages/ember-metal/lib/index.d.ts +++ b/packages/ember-metal/lib/index.d.ts @@ -33,6 +33,8 @@ export function get(obj: any, keyName: string): any; export function set(obj: any, keyName: string, value: any, tolerant?: boolean): void; +export function objectAt(arr: any, i: number): any; + export function computed(...args: Array): any; export function didRender(object: any, key: string, reference: any): boolean; diff --git a/packages/ember-metal/lib/index.js b/packages/ember-metal/lib/index.js index b94e5889612..7eb4ec6f6cd 100644 --- a/packages/ember-metal/lib/index.js +++ b/packages/ember-metal/lib/index.js @@ -39,6 +39,12 @@ export { set, trySet } from './property_set'; +export { objectAt } from './array'; +export { + eachProxyFor, + eachProxyArrayWillChange, + eachProxyArrayDidChange +} from './each_proxy'; export { addListener, hasListeners, diff --git a/packages/ember-runtime/lib/index.d.ts b/packages/ember-runtime/lib/index.d.ts index cc7401443d0..bd8cc8ebd5b 100644 --- a/packages/ember-runtime/lib/index.d.ts +++ b/packages/ember-runtime/lib/index.d.ts @@ -18,8 +18,6 @@ export const String: { loc(s: string, ...args: string[]): string; }; -export function objectAt(arr: any, i: number): any; - export function isEmberArray(arr: any): boolean; export function _contentFor(proxy: any): any; diff --git a/packages/ember-runtime/lib/index.js b/packages/ember-runtime/lib/index.js index d43cced3ff2..ab57fb0d994 100644 --- a/packages/ember-runtime/lib/index.js +++ b/packages/ember-runtime/lib/index.js @@ -12,7 +12,6 @@ export { default as compare } from './compare'; export { default as isEqual } from './is-equal'; export { default as Array, - objectAt, isEmberArray, addArrayObserver, removeArrayObserver, diff --git a/packages/ember-runtime/lib/mixins/array.js b/packages/ember-runtime/lib/mixins/array.js index 10b9cd95448..62ddb46f91f 100644 --- a/packages/ember-runtime/lib/mixins/array.js +++ b/packages/ember-runtime/lib/mixins/array.js @@ -6,6 +6,8 @@ import { symbol, toString } from 'ember-utils'; import { get, set, + objectAt, + replace, computed, isNone, aliasMethod, @@ -15,19 +17,16 @@ import { removeListener, sendEvent, hasListeners, - addObserver, - removeObserver, - meta, peekMeta, + eachProxyFor, + eachProxyArrayWillChange, + eachProxyArrayDidChange, beginPropertyChanges, endPropertyChanges } from 'ember-metal'; -import { assert } from 'ember-debug'; +import { assert, deprecate } from 'ember-debug'; import Enumerable from './enumerable'; import compare from '../compare'; -import { - replace -} from 'ember-metal'; import { ENV } from 'ember-environment'; import Observable from '../mixins/observable'; import Copyable from '../mixins/copyable'; @@ -58,10 +57,6 @@ export function removeArrayObserver(array, target, opts) { return arrayObserversHelper(array, target, opts, removeListener, true); } -export function objectAt(content, idx) { - return typeof content.objectAt === 'function' ? content.objectAt(idx) : content[idx]; -} - export function arrayContentWillChange(array, startIdx, removeAmt, addAmt) { // if no args are passed assume everything changes if (startIdx === undefined) { @@ -77,9 +72,7 @@ export function arrayContentWillChange(array, startIdx, removeAmt, addAmt) { } } - if (array.__each) { - array.__each.arrayWillChange(array, startIdx, removeAmt, addAmt); - } + eachProxyArrayWillChange(array, startIdx, removeAmt, addAmt); sendEvent(array, '@array:before', [array, startIdx, removeAmt, addAmt]); @@ -107,9 +100,7 @@ export function arrayContentDidChange(array, startIdx, removeAmt, addAmt) { notifyPropertyChange(array, '[]'); - if (array.__each) { - array.__each.arrayDidChange(array, startIdx, removeAmt, addAmt); - } + eachProxyArrayDidChange(array, startIdx, removeAmt, addAmt); sendEvent(array, '@array:change', [array, startIdx, removeAmt, addAmt]); @@ -1237,125 +1228,19 @@ const ArrayMixin = Mixin.create(Enumerable, { @public */ '@each': computed(function() { - // TODO use Symbol or add to meta - if (!this.__each) { - this.__each = new EachProxy(this); - } - - return this.__each; - }).volatile().readOnly() -}); - -/** - This is the object instance returned when you get the `@each` property on an - array. It uses the unknownProperty handler to automatically create - EachArray instances for property names. - @class EachProxy - @private -*/ -function EachProxy(content) { - this._content = content; - this._keys = undefined; - meta(this); -} - -EachProxy.prototype = { - __defineNonEnumerable(property) { - this[property.name] = property.descriptor.value; - }, - - // .......................................................... - // ARRAY CHANGES - // Invokes whenever the content array itself changes. - - arrayWillChange(content, idx, removedCnt, addedCnt) { // eslint-disable-line no-unused-vars - let keys = this._keys; - let lim = removedCnt > 0 ? idx + removedCnt : -1; - for (let key in keys) { - if (lim > 0) { - removeObserverForContentKey(content, key, this, idx, lim); - } - } - }, - - arrayDidChange(content, idx, removedCnt, addedCnt) { - let keys = this._keys; - let lim = addedCnt > 0 ? idx + addedCnt : -1; - let meta = peekMeta(this); - for (let key in keys) { - if (lim > 0) { - addObserverForContentKey(content, key, this, idx, lim); + deprecate( + `Getting the '@each' property on object ${toString(this)} is deprecated`, + false, + { + id: 'ember-metal.getting-each', + until: '3.5.0', + url: 'https://emberjs.com/deprecations/v3.x#toc_getting-the-each-property' } - notifyPropertyChange(this, key, meta); - } - }, - - // .......................................................... - // LISTEN FOR NEW OBSERVERS AND OTHER EVENT LISTENERS - // Start monitoring keys based on who is listening... - - willWatchProperty(property) { - this.beginObservingContentKey(property); - }, + ); - didUnwatchProperty(property) { - this.stopObservingContentKey(property); - }, - - // .......................................................... - // CONTENT KEY OBSERVING - // Actual watch keys on the source content. - - beginObservingContentKey(keyName) { - let keys = this._keys; - if (!keys) { - keys = this._keys = Object.create(null); - } - - if (!keys[keyName]) { - keys[keyName] = 1; - let content = this._content; - let len = get(content, 'length'); - - addObserverForContentKey(content, keyName, this, 0, len); - } else { - keys[keyName]++; - } - }, - - stopObservingContentKey(keyName) { - let keys = this._keys; - if (keys && (keys[keyName] > 0) && (--keys[keyName] <= 0)) { - let content = this._content; - let len = get(content, 'length'); - - removeObserverForContentKey(content, keyName, this, 0, len); - } - }, - - contentKeyDidChange(obj, keyName) { - notifyPropertyChange(this, keyName); - } -}; - -function addObserverForContentKey(content, keyName, proxy, idx, loc) { - while (--loc >= idx) { - let item = objectAt(content, loc); - if (item) { - assert(`When using @each to observe the array \`${toString(content)}\`, the array must return an object`, typeof item === 'object'); - addObserver(item, keyName, proxy, 'contentKeyDidChange'); - } - } -} - -function removeObserverForContentKey(content, keyName, proxy, idx, loc) { - while (--loc >= idx) { - let item = objectAt(content, loc); - if (item) { - removeObserver(item, keyName, proxy, 'contentKeyDidChange'); - } - } -} + return eachProxyFor(this); + }).readOnly() +}); const OUT_OF_RANGE_EXCEPTION = 'Index out of range'; diff --git a/packages/ember-runtime/lib/system/array_proxy.js b/packages/ember-runtime/lib/system/array_proxy.js index a5334de94c1..222ce1b7dde 100644 --- a/packages/ember-runtime/lib/system/array_proxy.js +++ b/packages/ember-runtime/lib/system/array_proxy.js @@ -4,6 +4,7 @@ import { get, + objectAt, computed, alias, PROPERTY_DID_CHANGE @@ -15,8 +16,7 @@ import EmberObject from './object'; import { MutableArray } from '../mixins/array'; import { addArrayObserver, - removeArrayObserver, - objectAt + removeArrayObserver } from '../mixins/array'; import { assert } from 'ember-debug'; diff --git a/packages/ember-runtime/lib/system/core_object.js b/packages/ember-runtime/lib/system/core_object.js index 45bf3ec6d91..43f9452f540 100644 --- a/packages/ember-runtime/lib/system/core_object.js +++ b/packages/ember-runtime/lib/system/core_object.js @@ -101,7 +101,6 @@ function makeCtor() { property === 'willWatchProperty' || property === 'didUnwatchProperty' || property === 'didAddListener' || - property === '__each' || property in target ) { return Reflect.get(target, property, receiver); diff --git a/packages/ember-runtime/tests/mixins/array_test.js b/packages/ember-runtime/tests/mixins/array_test.js index c6daa7107a5..b6a9bd45e1d 100644 --- a/packages/ember-runtime/tests/mixins/array_test.js +++ b/packages/ember-runtime/tests/mixins/array_test.js @@ -1,6 +1,7 @@ import { get, set, + objectAt, addObserver, observer as emberObserver, computed @@ -12,8 +13,7 @@ import EmberArray, { addArrayObserver, removeArrayObserver, arrayContentDidChange, - arrayContentWillChange, - objectAt + arrayContentWillChange } from '../../mixins/array'; import { A as emberA } from '../../mixins/array'; @@ -288,6 +288,14 @@ QUnit.test('adding an object should notify (@each.isDone)', function(assert) { assert.equal(called, 1, 'calls observer when object is pushed'); }); +QUnit.test('getting @each is deprecated', function(assert) { + assert.expect(1); + + expectDeprecation(() => { + get(ary, '@each'); + }, /Getting the '@each' property on object .* is deprecated/); +}); + QUnit.test('@each is readOnly', function(assert) { assert.expect(1); @@ -328,8 +336,10 @@ QUnit.test('modifying the array should also indicate the isDone prop itself has // important because it tests the case where we don't have an isDone // EachArray materialized but just want to know when the property has // changed. - - let each = get(ary, '@each'); + let each; + expectDeprecation(() => { + each = get(ary, '@each'); + }); let count = 0; addObserver(each, 'isDone', () => count++); diff --git a/packages/ember-runtime/tests/suites/array/objectAt.js b/packages/ember-runtime/tests/suites/array/objectAt.js index 83d1179e34e..107f5e1da0b 100644 --- a/packages/ember-runtime/tests/suites/array/objectAt.js +++ b/packages/ember-runtime/tests/suites/array/objectAt.js @@ -1,5 +1,4 @@ import { SuiteModuleBuilder } from '../suite'; -import { objectAt } from '../../../mixins/array'; const suite = SuiteModuleBuilder.create(); @@ -11,7 +10,7 @@ suite.test('should return object at specified index', function(assert) { let len = expected.length; for (let idx = 0; idx < len; idx++) { - assert.equal(objectAt(obj, idx), expected[idx], `obj.objectAt(${idx}) should match`); + assert.equal(obj.objectAt(idx), expected[idx], `obj.objectAt(${idx}) should match`); } }); @@ -19,10 +18,10 @@ suite.test('should return undefined when requesting objects beyond index', funct let obj; obj = this.newObject(this.newFixture(3)); - assert.equal(objectAt(obj, 5), undefined, 'should return undefined for obj.objectAt(5) when len = 3'); + assert.equal(obj.objectAt(obj, 5), undefined, 'should return undefined for obj.objectAt(5) when len = 3'); obj = this.newObject([]); - assert.equal(objectAt(obj, 0), undefined, 'should return undefined for obj.objectAt(0) when len = 0'); + assert.equal(obj.objectAt(obj, 0), undefined, 'should return undefined for obj.objectAt(0) when len = 0'); }); export default suite; diff --git a/packages/ember-runtime/tests/system/array_proxy/arranged_content_test.js b/packages/ember-runtime/tests/system/array_proxy/arranged_content_test.js index b717f9f9803..12d631797ca 100644 --- a/packages/ember-runtime/tests/system/array_proxy/arranged_content_test.js +++ b/packages/ember-runtime/tests/system/array_proxy/arranged_content_test.js @@ -1,7 +1,6 @@ -import { run, computed } from 'ember-metal'; +import { run, computed, objectAt } from 'ember-metal'; import ArrayProxy from '../../../system/array_proxy'; import { A as emberA } from '../../../mixins/array'; -import { objectAt } from '../../../mixins/array'; let array;