aurelia-templating-resources
Version:
A standard set of behaviors, converters and other resources for use with the Aurelia templating library.
274 lines (241 loc) • 10 kB
text/typescript
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;
}
}