diff --git a/src/array-repeat-strategy.ts b/src/array-repeat-strategy.ts index f190ee6..b7df4b0 100644 --- a/src/array-repeat-strategy.ts +++ b/src/array-repeat-strategy.ts @@ -1,5 +1,6 @@ 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. @@ -20,29 +21,30 @@ export class ArrayRepeatStrategy { * @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); + $repeat.removeAllViews(true, !$repeat.viewsRequireLifecycle); return; } - const children = repeat.views(); + 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); + this._standardProcessInstanceChanged($repeat, items); return; } - if (repeat.viewsRequireLifecycle) { + if ($repeat.viewsRequireLifecycle) { const childrenSnapshot = children.slice(0); - const itemNameInBindingContext = repeat.local; - const matcher = repeat.matcher(); + 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 = []; @@ -65,7 +67,7 @@ export class ArrayRepeatStrategy { let removePromise; if (itemsPreviouslyInViews.length > 0) { - removePromise = repeat.removeViews(viewsToRemove, true, !repeat.viewsRequireLifecycle); + removePromise = $repeat.removeViews(viewsToRemove, true, !$repeat.viewsRequireLifecycle); updateViews = () => { // update views (create new and move existing) for (let index = 0; index < itemsLength; index++) { @@ -74,8 +76,8 @@ export class ArrayRepeatStrategy { let view; if (indexOfView === -1) { // create views for new items - const overrideContext = createFullOverrideContext(repeat, items[index], index, itemsLength); - repeat.insertView(index, overrideContext.bindingContext, overrideContext); + 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 @@ -83,7 +85,7 @@ export class ArrayRepeatStrategy { itemsPreviouslyInViews[indexOfView] = undefined; } else { // move the element to the right place view = children[indexOfView]; - repeat.moveView(indexOfView, index); + $repeat.moveView(indexOfView, index); itemsPreviouslyInViews.splice(indexOfView, 1); itemsPreviouslyInViews.splice(index, 0, undefined); } @@ -95,12 +97,12 @@ export class ArrayRepeatStrategy { // remove extraneous elements in case of duplicates, // also update binding contexts if objects changed using the matcher function - this._inPlaceProcessItems(repeat, items); + 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); + removePromise = $repeat.removeAllViews(true, !$repeat.viewsRequireLifecycle); + updateViews = () => this._standardProcessInstanceChanged($repeat, items); } if (removePromise instanceof Promise) { @@ -110,7 +112,7 @@ export class ArrayRepeatStrategy { } } else { // no lifecycle needed, use the fast in-place processing - this._inPlaceProcessItems(repeat, items); + this._inPlaceProcessItems($repeat, items); } } diff --git a/src/aurelia-templating-resources.ts b/src/aurelia-templating-resources.ts index ac11261..4527d19 100644 --- a/src/aurelia-templating-resources.ts +++ b/src/aurelia-templating-resources.ts @@ -37,6 +37,7 @@ import { } from './repeat-utilities'; import {viewsRequireLifecycle} from './analyze-view-factory'; import {injectAureliaHideStyleAtHead} from './aurelia-hide-style'; +import './interfaces'; function configure(config: any) { injectAureliaHideStyleAtHead(); diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 0000000..23377ab --- /dev/null +++ b/src/interfaces.ts @@ -0,0 +1,17 @@ +import { BindingExpression } from 'aurelia-binding'; +import { TargetInstruction } from 'aurelia-templating'; + +/**@internal */ +declare module 'aurelia-templating' { + interface ViewFactory { + instructions: Record; + template: DocumentFragment; + } +} + +/**@internal */ +declare module 'aurelia-binding' { + interface BindingExpression { + targetProperty: string; + } +} diff --git a/src/repeat.ts b/src/repeat.ts index bae8992..b8bf089 100644 --- a/src/repeat.ts +++ b/src/repeat.ts @@ -1,6 +1,6 @@ /*eslint no-loop-func:0, no-unused-vars:0*/ import {inject} from 'aurelia-dependency-injection'; -import {ObserverLocator} from 'aurelia-binding'; +import {ObserverLocator, BindingExpression} from 'aurelia-binding'; import { BoundViewFactory, TargetInstruction, @@ -9,7 +9,8 @@ import { customAttribute, bindable, templateController, - View + View, + ViewFactory } from 'aurelia-templating'; import {RepeatStrategyLocator} from './repeat-strategy-locator'; import { @@ -28,6 +29,16 @@ import {AbstractRepeater} from './abstract-repeater'; @templateController @inject(BoundViewFactory, TargetInstruction, ViewSlot, ViewResources, ObserverLocator, RepeatStrategyLocator) export class Repeat extends AbstractRepeater { + + /** + * Setting this to `true` to enable legacy behavior, where a repeat would take first `matcher` binding + * any where inside its view if there's no `matcher` binding on the repeated element itself. + * + * Default value is true to avoid breaking change + * @default true + */ + static useInnerMatcher = true; + /** * List of items to bind the repeater to. * @@ -259,24 +270,34 @@ export class Repeat extends AbstractRepeater { } /** + * Capture and remove matcher binding is a way to cache matcher binding + reduce redundant work + * caused by multiple unnecessary matcher bindings * @internal */ _captureAndRemoveMatcherBinding() { - if (this.viewFactory.viewFactory) { - const instructions = this.viewFactory.viewFactory.instructions; - const instructionIds = Object.keys(instructions); - for (let i = 0; i < instructionIds.length; i++) { - const expressions = instructions[instructionIds[i]].expressions; - if (expressions) { - for (let ii = 0; ii < expressions.length; ii++) { - if (expressions[ii].targetProperty === 'matcher') { - const matcherBinding = expressions[ii]; - expressions.splice(ii, 1); - return matcherBinding; - } - } - } + const viewFactory: ViewFactory = this.viewFactory.viewFactory; + if (viewFactory) { + const template = viewFactory.template; + const instructions = viewFactory.instructions; + // legacy behavior enabled when Repeat.useInnerMathcer === true + if (Repeat.useInnerMatcher) { + return extractMatcherBindingExpression(instructions); + } + // if the template has more than 1 immediate child element + // it's a repeat put on a