Skip to content

Commit 750f519

Browse files
Merge pull request #379 from bigopon/fix-matcher
fix(repeat): properly extract matcher binding
2 parents 0cd7d50 + 83be482 commit 750f519

12 files changed

+416
-79
lines changed

src/array-repeat-strategy.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {createFullOverrideContext, updateOverrideContexts, updateOverrideContext, indexOf} from './repeat-utilities';
22
import {mergeSplice} from 'aurelia-binding';
3+
import { Repeat } from './repeat';
34

45
/**
56
* A strategy for repeating a template over an array.
@@ -20,29 +21,30 @@ export class ArrayRepeatStrategy {
2021
* @param items The new array instance.
2122
*/
2223
instanceChanged(repeat, items) {
24+
const $repeat = repeat as Repeat;
2325
const itemsLength = items.length;
2426

2527
// if the new instance does not contain any items,
2628
// just remove all views and don't do any further processing
2729
if (!items || itemsLength === 0) {
28-
repeat.removeAllViews(true, !repeat.viewsRequireLifecycle);
30+
$repeat.removeAllViews(true, !$repeat.viewsRequireLifecycle);
2931
return;
3032
}
3133

32-
const children = repeat.views();
34+
const children = $repeat.views();
3335
const viewsLength = children.length;
3436

3537
// likewise, if we previously didn't have any views,
3638
// simply make them and return
3739
if (viewsLength === 0) {
38-
this._standardProcessInstanceChanged(repeat, items);
40+
this._standardProcessInstanceChanged($repeat, items);
3941
return;
4042
}
4143

42-
if (repeat.viewsRequireLifecycle) {
44+
if ($repeat.viewsRequireLifecycle) {
4345
const childrenSnapshot = children.slice(0);
44-
const itemNameInBindingContext = repeat.local;
45-
const matcher = repeat.matcher();
46+
const itemNameInBindingContext = $repeat.local;
47+
const matcher = $repeat.matcher();
4648

4749
// the cache of the current state (it will be transformed along with the views to keep track of indicies)
4850
let itemsPreviouslyInViews = [];
@@ -65,7 +67,7 @@ export class ArrayRepeatStrategy {
6567
let removePromise;
6668

6769
if (itemsPreviouslyInViews.length > 0) {
68-
removePromise = repeat.removeViews(viewsToRemove, true, !repeat.viewsRequireLifecycle);
70+
removePromise = $repeat.removeViews(viewsToRemove, true, !$repeat.viewsRequireLifecycle);
6971
updateViews = () => {
7072
// update views (create new and move existing)
7173
for (let index = 0; index < itemsLength; index++) {
@@ -74,16 +76,16 @@ export class ArrayRepeatStrategy {
7476
let view;
7577

7678
if (indexOfView === -1) { // create views for new items
77-
const overrideContext = createFullOverrideContext(repeat, items[index], index, itemsLength);
78-
repeat.insertView(index, overrideContext.bindingContext, overrideContext);
79+
const overrideContext = createFullOverrideContext($repeat, items[index], index, itemsLength);
80+
$repeat.insertView(index, overrideContext.bindingContext, overrideContext);
7981
// reflect the change in our cache list so indicies are valid
8082
itemsPreviouslyInViews.splice(index, 0, undefined);
8183
} else if (indexOfView === index) { // leave unchanged items
8284
view = children[indexOfView];
8385
itemsPreviouslyInViews[indexOfView] = undefined;
8486
} else { // move the element to the right place
8587
view = children[indexOfView];
86-
repeat.moveView(indexOfView, index);
88+
$repeat.moveView(indexOfView, index);
8789
itemsPreviouslyInViews.splice(indexOfView, 1);
8890
itemsPreviouslyInViews.splice(index, 0, undefined);
8991
}
@@ -95,12 +97,12 @@ export class ArrayRepeatStrategy {
9597

9698
// remove extraneous elements in case of duplicates,
9799
// also update binding contexts if objects changed using the matcher function
98-
this._inPlaceProcessItems(repeat, items);
100+
this._inPlaceProcessItems($repeat, items);
99101
};
100102
} else {
101103
// if all of the items are different, remove all and add all from scratch
102-
removePromise = repeat.removeAllViews(true, !repeat.viewsRequireLifecycle);
103-
updateViews = () => this._standardProcessInstanceChanged(repeat, items);
104+
removePromise = $repeat.removeAllViews(true, !$repeat.viewsRequireLifecycle);
105+
updateViews = () => this._standardProcessInstanceChanged($repeat, items);
104106
}
105107

106108
if (removePromise instanceof Promise) {
@@ -110,7 +112,7 @@ export class ArrayRepeatStrategy {
110112
}
111113
} else {
112114
// no lifecycle needed, use the fast in-place processing
113-
this._inPlaceProcessItems(repeat, items);
115+
this._inPlaceProcessItems($repeat, items);
114116
}
115117
}
116118

src/aurelia-templating-resources.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
} from './repeat-utilities';
3838
import {viewsRequireLifecycle} from './analyze-view-factory';
3939
import {injectAureliaHideStyleAtHead} from './aurelia-hide-style';
40+
import './interfaces';
4041

4142
function configure(config: any) {
4243
injectAureliaHideStyleAtHead();

src/interfaces.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { BindingExpression } from 'aurelia-binding';
2+
import { TargetInstruction } from 'aurelia-templating';
3+
4+
/**@internal */
5+
declare module 'aurelia-templating' {
6+
interface ViewFactory {
7+
instructions: Record<string, TargetInstruction>;
8+
template: DocumentFragment;
9+
}
10+
}
11+
12+
/**@internal */
13+
declare module 'aurelia-binding' {
14+
interface BindingExpression {
15+
targetProperty: string;
16+
}
17+
}

src/repeat.ts

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*eslint no-loop-func:0, no-unused-vars:0*/
22
import {inject} from 'aurelia-dependency-injection';
3-
import {ObserverLocator} from 'aurelia-binding';
3+
import {ObserverLocator, BindingExpression} from 'aurelia-binding';
44
import {
55
BoundViewFactory,
66
TargetInstruction,
@@ -9,7 +9,8 @@ import {
99
customAttribute,
1010
bindable,
1111
templateController,
12-
View
12+
View,
13+
ViewFactory
1314
} from 'aurelia-templating';
1415
import {RepeatStrategyLocator} from './repeat-strategy-locator';
1516
import {
@@ -28,6 +29,16 @@ import {AbstractRepeater} from './abstract-repeater';
2829
@templateController
2930
@inject(BoundViewFactory, TargetInstruction, ViewSlot, ViewResources, ObserverLocator, RepeatStrategyLocator)
3031
export class Repeat extends AbstractRepeater {
32+
33+
/**
34+
* Setting this to `true` to enable legacy behavior, where a repeat would take first `matcher` binding
35+
* any where inside its view if there's no `matcher` binding on the repeated element itself.
36+
*
37+
* Default value is true to avoid breaking change
38+
* @default true
39+
*/
40+
static useInnerMatcher = true;
41+
3142
/**
3243
* List of items to bind the repeater to.
3344
*
@@ -259,24 +270,34 @@ export class Repeat extends AbstractRepeater {
259270
}
260271

261272
/**
273+
* Capture and remove matcher binding is a way to cache matcher binding + reduce redundant work
274+
* caused by multiple unnecessary matcher bindings
262275
* @internal
263276
*/
264277
_captureAndRemoveMatcherBinding() {
265-
if (this.viewFactory.viewFactory) {
266-
const instructions = this.viewFactory.viewFactory.instructions;
267-
const instructionIds = Object.keys(instructions);
268-
for (let i = 0; i < instructionIds.length; i++) {
269-
const expressions = instructions[instructionIds[i]].expressions;
270-
if (expressions) {
271-
for (let ii = 0; ii < expressions.length; ii++) {
272-
if (expressions[ii].targetProperty === 'matcher') {
273-
const matcherBinding = expressions[ii];
274-
expressions.splice(ii, 1);
275-
return matcherBinding;
276-
}
277-
}
278-
}
278+
const viewFactory: ViewFactory = this.viewFactory.viewFactory;
279+
if (viewFactory) {
280+
const template = viewFactory.template;
281+
const instructions = viewFactory.instructions;
282+
// legacy behavior enabled when Repeat.useInnerMathcer === true
283+
if (Repeat.useInnerMatcher) {
284+
return extractMatcherBindingExpression(instructions);
285+
}
286+
// if the template has more than 1 immediate child element
287+
// it's a repeat put on a <template/> element
288+
// not valid for matcher binding
289+
if (template.children.length > 1) {
290+
return undefined;
279291
}
292+
// if the root element does not have any instruction
293+
// it means there's no matcher binding
294+
// no need to do any further work
295+
const repeatedElement = template.firstElementChild;
296+
if (!repeatedElement.hasAttribute('au-target-id')) {
297+
return undefined;
298+
}
299+
const repeatedElementTargetId = repeatedElement.getAttribute('au-target-id');
300+
return extractMatcherBindingExpression(instructions, repeatedElementTargetId);
280301
}
281302

282303
return undefined;
@@ -286,7 +307,12 @@ export class Repeat extends AbstractRepeater {
286307
viewCount() { return this.viewSlot.children.length; }
287308
views() { return this.viewSlot.children; }
288309
view(index) { return this.viewSlot.children[index]; }
289-
matcher() { return this.matcherBinding ? this.matcherBinding.sourceExpression.evaluate(this.scope, this.matcherBinding.lookupFunctions) : null; }
310+
matcher() {
311+
const matcherBinding = this.matcherBinding;
312+
return matcherBinding
313+
? matcherBinding.sourceExpression.evaluate(this.scope, matcherBinding.lookupFunctions)
314+
: null;
315+
}
290316

291317
addView(bindingContext, overrideContext) {
292318
let view = this.viewFactory.create();
@@ -332,3 +358,28 @@ export class Repeat extends AbstractRepeater {
332358
}
333359
}
334360
}
361+
362+
/**
363+
* Iterate a record of TargetInstruction and their expressions to find first binding expression that targets property named "matcher"
364+
*/
365+
const extractMatcherBindingExpression = (instructions: Record<string, TargetInstruction>, targetedElementId?: string): BindingExpression | undefined => {
366+
const instructionIds = Object.keys(instructions);
367+
for (let i = 0; i < instructionIds.length; i++) {
368+
const instructionId = instructionIds[i];
369+
// matcher binding can only works when root element is not a <template/>
370+
// checking first el child
371+
if (targetedElementId !== undefined && instructionId !== targetedElementId) {
372+
continue;
373+
}
374+
const expressions = instructions[instructionId].expressions as BindingExpression[];
375+
if (expressions) {
376+
for (let ii = 0; ii < expressions.length; ii++) {
377+
if (expressions[ii].targetProperty === 'matcher') {
378+
const matcherBindingExpression = expressions[ii];
379+
expressions.splice(ii, 1);
380+
return matcherBindingExpression;
381+
}
382+
}
383+
}
384+
}
385+
};

test/map-repeat-strategy.spec.ts

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,57 +45,64 @@ describe('MapRepeatStrategy', () => {
4545
view3.bindingContext = { item: ['john', 'doe'] };
4646
view3.overrideContext = {};
4747
viewSlot.children = [view1, view2, view3];
48-
viewFactorySpy = spyOn(viewFactory, 'create').and.callFake(() => {});
48+
viewFactorySpy = spyOn(viewFactory, 'create').and.callFake(() => {/**/});
4949
});
50-
50+
5151
it('should correctly handle adding item (i.e Map.prototype.set())', () => {
52-
repeat = new Repeat(new ViewFactoryMock(), instructionMock, viewSlot, viewResourcesMock, new ObserverLocator());
52+
repeat = new Repeat(
53+
new ViewFactoryMock(),
54+
instructionMock,
55+
viewSlot,
56+
viewResourcesMock,
57+
new ObserverLocator(),
58+
null
59+
);
5360
let bindingContext = {};
5461
repeat.scope = { bindingContext, overrideContext: createOverrideContext(bindingContext) };
5562
records = [
56-
{"type": "add", "object": {}, "key": 'norf'}
57-
]
63+
{'type': 'add', 'object': {}, 'key': 'norf'}
64+
];
5865
items = new Map([['foo', 'bar'], ['qux', 'qax'], ['john', 'doe'], ['norf', 'narf']]);
59-
spyOn(viewSlot, 'removeAt').and.callFake(() => { return new ViewMock();});
66+
spyOn(viewSlot, 'removeAt').and.callFake(() => { return new ViewMock(); });
6067
strategy.instanceMutated(repeat, items, records);
61-
68+
6269
expect(viewSlot.children.length).toBe(4);
6370
expect(viewSlot.children[3].bindingContext.key).toBe('norf');
6471
expect(viewSlot.children[3].overrideContext.$index).toBe(3);
6572
expect(viewSlot.children[3].overrideContext.$first).toBe(false);
6673
expect(viewSlot.children[3].overrideContext.$last).toBe(true);
6774
});
68-
75+
6976
it('should correctly handle clear items (i.e Map.prototype.clear())', () => {
7077
let view4 = new ViewMock();
7178
view4.bindingContext = { item: ['norf', 'narf'] };
7279
view4.overrideContext = {};
7380
let viewSlotMock = new ViewSlotMock();
7481
viewSlotMock.children = [view1, view2, view3, view4];
75-
repeat = new Repeat(new ViewFactoryMock(), instructionMock, viewSlotMock, viewResourcesMock, new ObserverLocator());
82+
repeat = new Repeat(new ViewFactoryMock(), instructionMock, viewSlotMock, viewResourcesMock, new ObserverLocator(), null);
7683
let bindingContext = {};
7784
repeat.scope = { bindingContext, overrideContext: createOverrideContext(bindingContext) };
7885
records = [
79-
{"type": "clear", "object": {}}
80-
]
86+
{'type': 'clear', 'object': {}}
87+
];
8188
items = new Map();
8289
strategy.instanceMutated(repeat, items, records);
83-
90+
8491
expect(viewSlotMock.children.length).toBe(0);
8592
});
86-
93+
8794
it('should correctly handle adding items after clear (issue 287)', () => {
8895
viewSlot.children = [view1, view2, view3];
89-
repeat = new Repeat(new ViewFactoryMock(), instructionMock, viewSlot, viewResourcesMock, new ObserverLocator());
96+
repeat = new Repeat(new ViewFactoryMock(), instructionMock, viewSlot, viewResourcesMock, new ObserverLocator(), null);
9097
let bindingContext = {};
9198
repeat.scope = { bindingContext, overrideContext: createOverrideContext(bindingContext) };
9299
records = [
93-
{"type": "clear", "object": {}},
94-
{"type": "add", "object": {}, "key": 'foo'},
95-
{"type": "add", "object": {}, "key": 'qux'},
96-
{"type": "add", "object": {}, "key": 'john'},
97-
{"type": "add", "object": {}, "key": 'norf'}
98-
]
100+
{'type': 'clear', 'object': {}},
101+
{'type': 'add', 'object': {}, 'key': 'foo'},
102+
{'type': 'add', 'object': {}, 'key': 'qux'},
103+
{'type': 'add', 'object': {}, 'key': 'john'},
104+
{'type': 'add', 'object': {}, 'key': 'norf'}
105+
];
99106
items = new Map([['foo', 'bar'], ['qux', 'qax'], ['john', 'doe'], ['norf', 'narf']]);
100107
strategy.instanceMutated(repeat, items, records);
101108

0 commit comments

Comments
 (0)