UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

287 lines (264 loc) 10.7 kB
import '../-internals/meta/lib/meta.js'; import { e as isObject } from '../../shared-chunks/mandatory-setter-BiXq-dpN.js'; import { isDevelopingApp } from '@embroider/macros'; import '../debug/index.js'; import '../../@glimmer/destroyable/index.js'; import { consumeTag, validateTag, tagFor, valueForTag, combine } from '../../@glimmer/validator/index.js'; import { Q as PROPERTY_DID_CHANGE, g as get, o as objectAt, a as tagForProperty } from '../../shared-chunks/cache-DORQczuy.js'; import { a as alias } from '../../shared-chunks/alias-sGqO9HFc.js'; import { a as replace, b as arrayContentWillChange, c as arrayContentDidChange, d as addArrayObserver, e as removeArrayObserver } from '../../shared-chunks/array-BIqISULL.js'; import '../../shared-chunks/env-mInZ1DuF.js'; import EmberObject from '../object/index.js'; import EmberArray, { MutableArray } from './index.js'; import { setCustomTagFor } from '../../@glimmer/manager/index.js'; import { assert } from '../debug/lib/assert.js'; /** @module @ember/array/proxy */ function isMutable(obj) { return Array.isArray(obj) || typeof obj.replace === 'function'; } const ARRAY_OBSERVER_MAPPING = { willChange: '_arrangedContentArrayWillChange', didChange: '_arrangedContentArrayDidChange' }; function customTagForArrayProxy(proxy, key) { (isDevelopingApp() && !(proxy instanceof ArrayProxy) && assert('[BUG] Expected a proxy', proxy instanceof ArrayProxy)); if (key === '[]') { proxy._revalidate(); return proxy._arrTag; } else if (key === 'length') { proxy._revalidate(); return proxy._lengthTag; } return tagFor(proxy, key); } /** An ArrayProxy wraps any other object that implements `Array` and/or `MutableArray,` forwarding all requests. This makes it very useful for a number of binding use cases or other cases where being able to swap out the underlying array is useful. A simple example of usage: ```javascript import { A } from '@ember/array'; import ArrayProxy from '@ember/array/proxy'; let pets = ['dog', 'cat', 'fish']; let ap = ArrayProxy.create({ content: A(pets) }); ap.get('firstObject'); // 'dog' ap.set('content', ['amoeba', 'paramecium']); ap.get('firstObject'); // 'amoeba' ``` This class can also be useful as a layer to transform the contents of an array, as they are accessed. This can be done by overriding `objectAtContent`: ```javascript import { A } from '@ember/array'; import ArrayProxy from '@ember/array/proxy'; let pets = ['dog', 'cat', 'fish']; let ap = ArrayProxy.create({ content: A(pets), objectAtContent: function(idx) { return this.get('content').objectAt(idx).toUpperCase(); } }); ap.get('firstObject'); // . 'DOG' ``` When overriding this class, it is important to place the call to `_super` *after* setting `content` so the internal observers have a chance to fire properly: ```javascript import { A } from '@ember/array'; import ArrayProxy from '@ember/array/proxy'; export default ArrayProxy.extend({ init() { this.set('content', A(['dog', 'cat', 'fish'])); this._super(...arguments); } }); ``` @class ArrayProxy @extends EmberObject @uses MutableArray @public */ class ArrayProxy extends EmberObject { /* `this._objectsDirtyIndex` determines which indexes in the `this._objects` cache are dirty. If `this._objectsDirtyIndex === -1` then no indexes are dirty. Otherwise, an index `i` is dirty if `i >= this._objectsDirtyIndex`. Calling `objectAt` with a dirty index will cause the `this._objects` cache to be recomputed. */ /** @internal */ _objectsDirtyIndex = 0; /** @internal */ _objects = null; /** @internal */ _lengthDirty = true; /** @internal */ _length = 0; /** @internal */ _arrangedContent = null; /** @internal */ _arrangedContentIsUpdating = false; /** @internal */ _arrangedContentTag = null; /** @internal */ _arrangedContentRevision = null; /** @internal */ _lengthTag = null; /** @internal */ _arrTag = null; init(props) { super.init(props); setCustomTagFor(this, customTagForArrayProxy); } [PROPERTY_DID_CHANGE]() { this._revalidate(); } willDestroy() { this._removeArrangedContentArrayObserver(); } objectAtContent(idx) { let arrangedContent = get(this, 'arrangedContent'); (isDevelopingApp() && !(arrangedContent) && assert('[BUG] Called objectAtContent without content', arrangedContent)); return objectAt(arrangedContent, idx); } // See additional docs for `replace` from `MutableArray`: // https://api.emberjs.com/ember/release/classes/MutableArray/methods/replace?anchor=replace replace(idx, amt, objects) { (isDevelopingApp() && !(get(this, 'arrangedContent') === get(this, 'content')) && assert('Mutating an arranged ArrayProxy is not allowed', get(this, 'arrangedContent') === get(this, 'content'))); this.replaceContent(idx, amt, objects); } replaceContent(idx, amt, objects) { let content = get(this, 'content'); (isDevelopingApp() && !(content) && assert('[BUG] Called replaceContent without content', content)); (isDevelopingApp() && !(isMutable(content)) && assert('Mutating a non-mutable array is not allowed', isMutable(content))); replace(content, idx, amt, objects); } // Overriding objectAt is not supported. objectAt(idx) { this._revalidate(); if (this._objects === null) { this._objects = []; } if (this._objectsDirtyIndex !== -1 && idx >= this._objectsDirtyIndex) { let arrangedContent = get(this, 'arrangedContent'); if (arrangedContent) { let length = this._objects.length = get(arrangedContent, 'length'); for (let i = this._objectsDirtyIndex; i < length; i++) { // SAFETY: This is expected to only ever return an instance of T. In other words, there should // be no gaps in the array. Unfortunately, we can't actually assert for it since T could include // any types, including null or undefined. this._objects[i] = this.objectAtContent(i); } } else { this._objects.length = 0; } this._objectsDirtyIndex = -1; } return this._objects[idx]; } // Overriding length is not supported. get length() { this._revalidate(); if (this._lengthDirty) { let arrangedContent = get(this, 'arrangedContent'); this._length = arrangedContent ? get(arrangedContent, 'length') : 0; this._lengthDirty = false; } (isDevelopingApp() && !(this._lengthTag) && assert('[BUG] _lengthTag is not set', this._lengthTag)); consumeTag(this._lengthTag); return this._length; } set length(value) { let length = this.length; let removedCount = length - value; let added; if (removedCount === 0) { return; } else if (removedCount < 0) { added = new Array(-removedCount); removedCount = 0; } let content = get(this, 'content'); if (content) { (isDevelopingApp() && !(isMutable(content)) && assert('Mutating a non-mutable array is not allowed', isMutable(content))); replace(content, value, removedCount, added); this._invalidate(); } } _updateArrangedContentArray(arrangedContent) { let oldLength = this._objects === null ? 0 : this._objects.length; let newLength = arrangedContent ? get(arrangedContent, 'length') : 0; this._removeArrangedContentArrayObserver(); arrayContentWillChange(this, 0, oldLength, newLength); this._invalidate(); arrayContentDidChange(this, 0, oldLength, newLength, false); this._addArrangedContentArrayObserver(arrangedContent); } _addArrangedContentArrayObserver(arrangedContent) { if (arrangedContent && !arrangedContent.isDestroyed) { (isDevelopingApp() && !(arrangedContent !== this) && assert("Can't set ArrayProxy's content to itself", arrangedContent !== this)); (isDevelopingApp() && !(function (arr) { return Array.isArray(arr) || EmberArray.detect(arr); }(arrangedContent)) && assert(`ArrayProxy expects a native Array, EmberArray, or ArrayProxy, but you passed ${typeof arrangedContent}`, function (arr) { return Array.isArray(arr) || EmberArray.detect(arr); }(arrangedContent))); (isDevelopingApp() && !(!arrangedContent.isDestroyed) && assert('ArrayProxy expected its contents to not be destroyed', !arrangedContent.isDestroyed)); addArrayObserver(arrangedContent, this, ARRAY_OBSERVER_MAPPING); this._arrangedContent = arrangedContent; } } _removeArrangedContentArrayObserver() { if (this._arrangedContent) { removeArrayObserver(this._arrangedContent, this, ARRAY_OBSERVER_MAPPING); } } _arrangedContentArrayWillChange() {} _arrangedContentArrayDidChange(_proxy, idx, removedCnt, addedCnt) { arrayContentWillChange(this, idx, removedCnt, addedCnt); let dirtyIndex = idx; if (dirtyIndex < 0) { let length = get(this._arrangedContent, 'length'); dirtyIndex += length + removedCnt - addedCnt; } if (this._objectsDirtyIndex === -1 || this._objectsDirtyIndex > dirtyIndex) { this._objectsDirtyIndex = dirtyIndex; } this._lengthDirty = true; arrayContentDidChange(this, idx, removedCnt, addedCnt, false); } _invalidate() { this._objectsDirtyIndex = 0; this._lengthDirty = true; } _revalidate() { if (this._arrangedContentIsUpdating === true) return; if (this._arrangedContentTag === null || !validateTag(this._arrangedContentTag, this._arrangedContentRevision)) { let arrangedContent = this.get('arrangedContent'); if (this._arrangedContentTag === null) { // This is the first time the proxy has been setup, only add the observer // don't trigger any events this._addArrangedContentArrayObserver(arrangedContent); } else { this._arrangedContentIsUpdating = true; this._updateArrangedContentArray(arrangedContent); this._arrangedContentIsUpdating = false; } let arrangedContentTag = this._arrangedContentTag = tagFor(this, 'arrangedContent'); this._arrangedContentRevision = valueForTag(this._arrangedContentTag); if (isObject(arrangedContent)) { this._lengthTag = combine([arrangedContentTag, tagForProperty(arrangedContent, 'length')]); this._arrTag = combine([arrangedContentTag, tagForProperty(arrangedContent, '[]')]); } else { this._lengthTag = this._arrTag = arrangedContentTag; } } } } ArrayProxy.reopen(MutableArray, { arrangedContent: alias('content') }); export { ArrayProxy as default };