Skip to content
Permalink
Browse files

fix(repeat): properly find matcher binding

  • Loading branch information...
bigopon committed May 23, 2019
1 parent 5a95971 commit a5796443016d2c87269dd05801c057fb45e5dc44
Showing with 88 additions and 23 deletions.
  1. +1 −0 src/aurelia-templating-resources.ts
  2. +17 −0 src/interfaces.ts
  3. +36 −6 src/repeat.ts
  4. +27 −17 test/repeat.issue-378.spec.ts
  5. +7 −0 test/test-utilities.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();
@@ -0,0 +1,17 @@
import { BindingExpression } from 'aurelia-binding';
import { TargetInstruction } from 'aurelia-templating';

/**@internal */
declare module 'aurelia-templating' {
interface ViewFactory {
instructions: Record<string, TargetInstruction>;
template: DocumentFragment;
}
}

/**@internal */
declare module 'aurelia-binding' {
interface BindingExpression {
targetProperty: string;
}
}
@@ -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 {
@@ -259,14 +260,38 @@ 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 viewFactory: ViewFactory = this.viewFactory.viewFactory;
if (viewFactory) {
const template = viewFactory.template;
const instructions = viewFactory.instructions;
const instructionIds = Object.keys(instructions);
// if the template has more than 1 immediate child element
// it's a repeat put on a <template/> element
// not valid for matcher binding
if (template.children.length > 1) {
return undefined;
}
// if the root element does not have any instruction
// it means there's no matcher binding
// no need to do any further work
const repeatedElement = template.firstElementChild;
if (!repeatedElement.hasAttribute('au-target-id')) {
return undefined;
}
const repeatedElementTargetId = repeatedElement.getAttribute('au-target-id');
for (let i = 0; i < instructionIds.length; i++) {
const expressions = instructions[instructionIds[i]].expressions;
const instructionId = instructionIds[i];
// matcher binding can only works when root element is not a <template/>
// checking first el child
if (instructionId !== repeatedElementTargetId) {
continue;
}
const expressions = instructions[instructionId].expressions as BindingExpression[];
if (expressions) {
for (let ii = 0; ii < expressions.length; ii++) {
if (expressions[ii].targetProperty === 'matcher') {
@@ -286,7 +311,12 @@ export class Repeat extends AbstractRepeater {
viewCount() { return this.viewSlot.children.length; }
views() { return this.viewSlot.children; }
view(index) { return this.viewSlot.children[index]; }
matcher() { return this.matcherBinding ? this.matcherBinding.sourceExpression.evaluate(this.scope, this.matcherBinding.lookupFunctions) : null; }
matcher() {
const matcherBinding = this.matcherBinding;
return matcherBinding
? matcherBinding.sourceExpression.evaluate(this.scope, matcherBinding.lookupFunctions)
: null;
}

addView(bindingContext, overrideContext) {
let view = this.viewFactory.create();
@@ -1,39 +1,49 @@
import './setup';
import { StageComponent } from 'aurelia-testing';
import { bootstrap } from 'aurelia-bootstrapper';
import { waitForFrames } from './test-utilities';

// https://github.com/aurelia/templating-resources/issues/378
fdescribe('repeat.issue-378.spec.ts', () => {
it('with matcher', async () => {
it('works with matcher, on a real element, + array', async () => {
const model = new class {
products = [
{ id: 0, name: 'Motherboard' },
{ id: 1, name: 'CPU' },
{ id: 2, name: 'Memory' }
];

productMatcher = (a, b) => {
return a.id === b.id;
}

selectedProduct = { id: 1, name: 'CPU' };
};
const component = StageComponent
.withResources()
.inView(`<label repeat.for="product of products">
.inView(`<label repeat.for="product of products" a.bind="product">
<input
type="radio"
name="group2"
model.bind="product"
matcherss.bind="productMatcher"
matcher.bind="productMatcher"
checked.bind="selectedProduct" />
\${product.id} - \${product.name}
</label>`)
.boundTo(new class {
products = [
{ id: 0, name: 'Motherboard' },
{ id: 1, name: 'CPU' },
{ id: 2, name: 'Memory' }
];

productMatcher = (a, b) => {
console.log('matcher');
return a.id === b.id;
}

selectedProduct = { id: 1, name: 'CPU' };
});
.boundTo(model);

await component.create(bootstrap);
const radios = document.querySelectorAll('input[type=radio]') as ArrayLike<HTMLInputElement>;
expect(radios.length).toBe(3);
expect(radios[1].checked).toBe(true);

radios[2].click();
await waitForFrames(1);

expect(radios[1].checked).toBe(false);
expect(radios[2].checked).toBe(true);
expect(model.selectedProduct).toBe(model.products[2]);

component.dispose();
});
});
@@ -0,0 +1,7 @@


export const waitForFrames = async (frameCount: number) => {
while (frameCount-- > 0) {
await new Promise(r => requestAnimationFrame(r));
}
};

0 comments on commit a579644

Please sign in to comment.
You can’t perform that action at this time.