import {createFullOverrideContext, updateOverrideContexts, updateOverrideContext, indexOf} from './repeat-utilities'; import {mergeSplice} from 'aurelia-binding'; import { Repeat } from './repeat'; /** * A strategy for repeating a template over an array. */ export class ArrayRepeatStrategy { /** * Gets an observer for the specified collection. * @param observerLocator The observer locator instance. * @param items The items to be observed. */ getCollectionObserver(observerLocator, items) { return observerLocator.getArrayObserver(items); } /** * Handle the repeat's collection instance changing. * @param repeat The repeater instance. * @param items The new array instance. */ instanceChanged(repeat, items) { const $repeat = repeat as Repeat; const itemsLength = items.length; // if the new instance does not contain any items, // just remove all views and don't do any further processing if (!items || itemsLength === 0) { $repeat.removeAllViews(true, !$repeat.viewsRequireLifecycle); return; } const children = $repeat.views(); const viewsLength = children.length; // likewise, if we previously didn't have any views, // simply make them and return if (viewsLength === 0) { this._standardProcessInstanceChanged($repeat, items); return; } if ($repeat.viewsRequireLifecycle) { const childrenSnapshot = children.slice(0); const itemNameInBindingContext = $repeat.local; const matcher = $repeat.matcher(); // the cache of the current state (it will be transformed along with the views to keep track of indicies) let itemsPreviouslyInViews = []; const viewsToRemove = []; for (let index = 0; index < viewsLength; index++) { const view = childrenSnapshot[index]; const oldItem = view.bindingContext[itemNameInBindingContext]; if (indexOf(items, oldItem, matcher) === -1) { // remove the item if no longer in the new instance of items viewsToRemove.push(view); } else { // or add the item to the cache list itemsPreviouslyInViews.push(oldItem); } } let updateViews; let removePromise; if (itemsPreviouslyInViews.length > 0) { removePromise = $repeat.removeViews(viewsToRemove, true, !$repeat.viewsRequireLifecycle); updateViews = () => { // update views (create new and move existing) for (let index = 0; index < itemsLength; index++) { const item = items[index]; const indexOfView = indexOf(itemsPreviouslyInViews, item, matcher, index); let view; if (indexOfView === -1) { // create views for new items const overrideContext = createFullOverrideContext($repeat, items[index], index, itemsLength); $repeat.insertView(index, overrideContext.bindingContext, overrideContext); // reflect the change in our cache list so indicies are valid itemsPreviouslyInViews.splice(index, 0, undefined); } else if (indexOfView === index) { // leave unchanged items view = children[indexOfView]; itemsPreviouslyInViews[indexOfView] = undefined; } else { // move the element to the right place view = children[indexOfView]; $repeat.moveView(indexOfView, index); itemsPreviouslyInViews.splice(indexOfView, 1); itemsPreviouslyInViews.splice(index, 0, undefined); } if (view) { updateOverrideContext(view.overrideContext, index, itemsLength); } } // remove extraneous elements in case of duplicates, // also update binding contexts if objects changed using the matcher function this._inPlaceProcessItems($repeat, items); }; } else { // if all of the items are different, remove all and add all from scratch removePromise = $repeat.removeAllViews(true, !$repeat.viewsRequireLifecycle); updateViews = () => this._standardProcessInstanceChanged($repeat, items); } if (removePromise instanceof Promise) { removePromise.then(updateViews); } else { updateViews(); } } else { // no lifecycle needed, use the fast in-place processing this._inPlaceProcessItems($repeat, items); } } /** * @internal */ _standardProcessInstanceChanged(repeat, items) { for (let i = 0, ii = items.length; i < ii; i++) { let overrideContext = createFullOverrideContext(repeat, items[i], i, ii); repeat.addView(overrideContext.bindingContext, overrideContext); } } /** * @internal */ _inPlaceProcessItems(repeat, items) { let itemsLength = items.length; let viewsLength = repeat.viewCount(); // remove unneeded views. while (viewsLength > itemsLength) { viewsLength--; repeat.removeView(viewsLength, true, !repeat.viewsRequireLifecycle); } // avoid repeated evaluating the property-getter for the "local" property. let local = repeat.local; // re-evaluate bindings on existing views. for (let i = 0; i < viewsLength; i++) { let view = repeat.view(i); let last = i === itemsLength - 1; let middle = i !== 0 && !last; let bindingContext = view.bindingContext; let overrideContext = view.overrideContext; // any changes to the binding context? if (bindingContext[local] === items[i] && overrideContext.$middle === middle && overrideContext.$last === last) { // no changes. continue... continue; } // update the binding context and refresh the bindings. bindingContext[local] = items[i]; overrideContext.$middle = middle; overrideContext.$last = last; repeat.updateBindings(view); } // add new views for (let i = viewsLength; i < itemsLength; i++) { let overrideContext = createFullOverrideContext(repeat, items[i], i, itemsLength); repeat.addView(overrideContext.bindingContext, overrideContext); } } /** * Handle the repeat's collection instance mutating. * @param repeat The repeat instance. * @param array The modified array. * @param splices Records of array changes. */ instanceMutated(repeat, array, splices) { if (repeat.__queuedSplices) { for (let i = 0, ii = splices.length; i < ii; ++i) { let {index, removed, addedCount} = splices[i]; mergeSplice(repeat.__queuedSplices, index, removed, addedCount); } // Array.prototype.slice is used here to clone the array repeat.__array = array.slice(0); return; } // Array.prototype.slice is used here to clone the array let maybePromise = this._runSplices(repeat, array.slice(0), splices); if (maybePromise instanceof Promise) { let queuedSplices = repeat.__queuedSplices = []; let runQueuedSplices = () => { if (!queuedSplices.length) { repeat.__queuedSplices = undefined; repeat.__array = undefined; return; } let nextPromise = this._runSplices(repeat, repeat.__array, queuedSplices) || Promise.resolve(); queuedSplices = repeat.__queuedSplices = []; nextPromise.then(runQueuedSplices); }; maybePromise.then(runQueuedSplices); } } /** * Run a normalised set of splices against the viewSlot children. * @param repeat The repeat instance. * @param array The modified array. * @param splices Records of array changes. * @return {Promise|undefined} A promise if animations have to be run. * @pre The splices must be normalised so as: * * Any item added may not be later removed. * * Removals are ordered by asending index * @internal */ _runSplices(repeat, array, splices) { let removeDelta = 0; let rmPromises = []; for (let i = 0, ii = splices.length; i < ii; ++i) { let splice = splices[i]; let removed = splice.removed; for (let j = 0, jj = removed.length; j < jj; ++j) { // the rmPromises.length correction works due to the ordered removal precondition let viewOrPromise = repeat.removeView(splice.index + removeDelta + rmPromises.length, true); if (viewOrPromise instanceof Promise) { rmPromises.push(viewOrPromise); } } removeDelta -= splice.addedCount; } if (rmPromises.length > 0) { return Promise.all(rmPromises).then(() => { let spliceIndexLow = this._handleAddedSplices(repeat, array, splices); updateOverrideContexts(repeat.views(), spliceIndexLow); }); } let spliceIndexLow = this._handleAddedSplices(repeat, array, splices); updateOverrideContexts(repeat.views(), spliceIndexLow); return undefined; } /** * @internal */ _handleAddedSplices(repeat, array, splices) { let spliceIndex: number; let spliceIndexLow: number; let arrayLength = array.length; for (let i = 0, ii = splices.length; i < ii; ++i) { let splice = splices[i]; let addIndex = spliceIndex = splice.index; let end = splice.index + splice.addedCount; if (typeof spliceIndexLow === 'undefined' || spliceIndexLow === null || spliceIndexLow > splice.index) { spliceIndexLow = spliceIndex; } for (; addIndex < end; ++addIndex) { let overrideContext = createFullOverrideContext(repeat, array[addIndex], addIndex, arrayLength); repeat.insertView(addIndex, overrideContext.bindingContext, overrideContext); } } return spliceIndexLow; } }