angular-cn/packages/compiler/test/render3/view/binding_spec.ts

182 lines
7.4 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as e from '../../../src/expression_parser/ast';
import * as a from '../../../src/render3/r3_ast';
import {DirectiveMeta} from '../../../src/render3/view/t2_api';
import {R3TargetBinder} from '../../../src/render3/view/t2_binder';
import {parseTemplate} from '../../../src/render3/view/template';
import {CssSelector, SelectorMatcher} from '../../../src/selector';
import {findExpression} from './util';
function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta> {
const matcher = new SelectorMatcher<DirectiveMeta>();
matcher.addSelectables(CssSelector.parse('[ngFor][ngForOf]'), {
name: 'NgFor',
exportAs: null,
inputs: {'ngForOf': 'ngForOf'},
outputs: {},
isComponent: false,
});
fix(ivy): match microsyntax template directives correctly (#29698) Previously, Template.templateAttrs was introduced to capture attribute bindings which originated from microsyntax (e.g. bindings in *ngFor="..."). This means that a Template node can have two different structures, depending on whether it originated from microsyntax or from a literal <ng-template>. In the literal case, the node behaves much like an Element node, it has attributes, inputs, and outputs which determine which directives apply. In the microsyntax case, though, only the templateAttrs should be used to determine which directives apply. Previously, both the t2_binder and the TemplateDefinitionBuilder were using the wrong set of attributes to match directives - combining the attributes, inputs, outputs, and templateAttrs of the Template node regardless of its origin. In the TDB's case this wasn't a problem, since the TDB collects a global Set of directives used in the template, so it didn't matter whether the directive was also recognized on the <ng-template>. t2_binder's API distinguishes between directives on specific nodes, though, so it's more sensitive to mismatching. In particular, this showed up as an assertion failure in template type- checking in certain cases, when a directive was accidentally matched on a microsyntax template element and also had a binding which referenced a variable declared in the microsyntax. This resulted in the type-checker attempting to generate a reference to a variable that didn't exist in that scope. The fix is to distinguish between the two cases and select the appropriate set of attributes to match on accordingly. Testing strategy: tested in the t2_binder tests. PR Close #29698
2019-04-04 16:19:38 -04:00
matcher.addSelectables(CssSelector.parse('[dir]'), {
name: 'Dir',
exportAs: null,
inputs: {},
outputs: {},
isComponent: false,
});
matcher.addSelectables(CssSelector.parse('[hasOutput]'), {
name: 'HasOutput',
exportAs: null,
inputs: {},
outputs: {'outputBinding': 'outputBinding'},
isComponent: false,
});
matcher.addSelectables(CssSelector.parse('[hasInput]'), {
name: 'HasInput',
exportAs: null,
inputs: {'inputBinding': 'inputBinding'},
outputs: {},
isComponent: false,
});
return matcher;
}
describe('t2 binding', () => {
it('should bind a simple template', () => {
const template = parseTemplate('<div *ngFor="let item of items">{{item.name}}</div>', '', {});
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta>());
const res = binder.bind({template: template.nodes});
const itemBinding = (findExpression(template.nodes, '{{item.name}}') !as e.Interpolation)
.expressions[0] as e.PropertyRead;
const item = itemBinding.receiver;
const itemTarget = res.getExpressionTarget(item);
if (!(itemTarget instanceof a.Variable)) {
return fail('Expected item to point to a Variable');
}
expect(itemTarget.value).toBe('$implicit');
const itemTemplate = res.getTemplateOfSymbol(itemTarget);
expect(itemTemplate).not.toBeNull();
expect(res.getNestingLevel(itemTemplate !)).toBe(1);
});
it('should match directives when binding a simple template', () => {
const template = parseTemplate('<div *ngFor="let item of items">{{item.name}}</div>', '', {});
const binder = new R3TargetBinder(makeSelectorMatcher());
const res = binder.bind({template: template.nodes});
const tmpl = template.nodes[0] as a.Template;
const directives = res.getDirectivesOfNode(tmpl) !;
expect(directives).not.toBeNull();
expect(directives.length).toBe(1);
expect(directives[0].name).toBe('NgFor');
});
fix(ivy): match microsyntax template directives correctly (#29698) Previously, Template.templateAttrs was introduced to capture attribute bindings which originated from microsyntax (e.g. bindings in *ngFor="..."). This means that a Template node can have two different structures, depending on whether it originated from microsyntax or from a literal <ng-template>. In the literal case, the node behaves much like an Element node, it has attributes, inputs, and outputs which determine which directives apply. In the microsyntax case, though, only the templateAttrs should be used to determine which directives apply. Previously, both the t2_binder and the TemplateDefinitionBuilder were using the wrong set of attributes to match directives - combining the attributes, inputs, outputs, and templateAttrs of the Template node regardless of its origin. In the TDB's case this wasn't a problem, since the TDB collects a global Set of directives used in the template, so it didn't matter whether the directive was also recognized on the <ng-template>. t2_binder's API distinguishes between directives on specific nodes, though, so it's more sensitive to mismatching. In particular, this showed up as an assertion failure in template type- checking in certain cases, when a directive was accidentally matched on a microsyntax template element and also had a binding which referenced a variable declared in the microsyntax. This resulted in the type-checker attempting to generate a reference to a variable that didn't exist in that scope. The fix is to distinguish between the two cases and select the appropriate set of attributes to match on accordingly. Testing strategy: tested in the t2_binder tests. PR Close #29698
2019-04-04 16:19:38 -04:00
it('should match directives on namespaced elements', () => {
const template = parseTemplate('<svg><text dir>SVG</text></svg>', '', {});
const matcher = new SelectorMatcher<DirectiveMeta>();
matcher.addSelectables(CssSelector.parse('text[dir]'), {
name: 'Dir',
exportAs: null,
inputs: {},
outputs: {},
isComponent: false,
});
const binder = new R3TargetBinder(matcher);
const res = binder.bind({template: template.nodes});
const svgNode = template.nodes[0] as a.Element;
const textNode = svgNode.children[0] as a.Element;
const directives = res.getDirectivesOfNode(textNode) !;
expect(directives).not.toBeNull();
expect(directives.length).toBe(1);
expect(directives[0].name).toBe('Dir');
});
fix(ivy): match microsyntax template directives correctly (#29698) Previously, Template.templateAttrs was introduced to capture attribute bindings which originated from microsyntax (e.g. bindings in *ngFor="..."). This means that a Template node can have two different structures, depending on whether it originated from microsyntax or from a literal <ng-template>. In the literal case, the node behaves much like an Element node, it has attributes, inputs, and outputs which determine which directives apply. In the microsyntax case, though, only the templateAttrs should be used to determine which directives apply. Previously, both the t2_binder and the TemplateDefinitionBuilder were using the wrong set of attributes to match directives - combining the attributes, inputs, outputs, and templateAttrs of the Template node regardless of its origin. In the TDB's case this wasn't a problem, since the TDB collects a global Set of directives used in the template, so it didn't matter whether the directive was also recognized on the <ng-template>. t2_binder's API distinguishes between directives on specific nodes, though, so it's more sensitive to mismatching. In particular, this showed up as an assertion failure in template type- checking in certain cases, when a directive was accidentally matched on a microsyntax template element and also had a binding which referenced a variable declared in the microsyntax. This resulted in the type-checker attempting to generate a reference to a variable that didn't exist in that scope. The fix is to distinguish between the two cases and select the appropriate set of attributes to match on accordingly. Testing strategy: tested in the t2_binder tests. PR Close #29698
2019-04-04 16:19:38 -04:00
it('should not match directives intended for an element on a microsyntax template', () => {
const template = parseTemplate('<div *ngFor="let item of items" dir></div>', '', {});
const binder = new R3TargetBinder(makeSelectorMatcher());
const res = binder.bind({template: template.nodes});
const tmpl = template.nodes[0] as a.Template;
const tmplDirectives = res.getDirectivesOfNode(tmpl) !;
expect(tmplDirectives).not.toBeNull();
expect(tmplDirectives.length).toBe(1);
expect(tmplDirectives[0].name).toBe('NgFor');
const elDirectives = res.getDirectivesOfNode(tmpl.children[0] as a.Element) !;
expect(elDirectives).not.toBeNull();
expect(elDirectives.length).toBe(1);
expect(elDirectives[0].name).toBe('Dir');
});
describe('matching inputs to consuming directives', () => {
it('should work for bound attributes', () => {
const template = parseTemplate('<div hasInput [inputBinding]="myValue"></div>', '', {});
const binder = new R3TargetBinder(makeSelectorMatcher());
const res = binder.bind({template: template.nodes});
const el = template.nodes[0] as a.Element;
const attr = el.inputs[0];
const consumer = res.getConsumerOfBinding(attr) as DirectiveMeta;
expect(consumer.name).toBe('HasInput');
});
it('should work for text attributes on elements', () => {
const template = parseTemplate('<div hasInput inputBinding="text"></div>', '', {});
const binder = new R3TargetBinder(makeSelectorMatcher());
const res = binder.bind({template: template.nodes});
const el = template.nodes[0] as a.Element;
const attr = el.attributes[1];
const consumer = res.getConsumerOfBinding(attr) as DirectiveMeta;
expect(consumer.name).toBe('HasInput');
});
it('should work for text attributes on templates', () => {
const template =
parseTemplate('<ng-template hasInput inputBinding="text"></ng-template>', '', {});
const binder = new R3TargetBinder(makeSelectorMatcher());
const res = binder.bind({template: template.nodes});
const el = template.nodes[0] as a.Element;
const attr = el.attributes[1];
const consumer = res.getConsumerOfBinding(attr) as DirectiveMeta;
expect(consumer.name).toBe('HasInput');
});
it('should bind to the encompassing node when no directive input is matched', () => {
const template = parseTemplate('<span dir></span>', '', {});
const binder = new R3TargetBinder(makeSelectorMatcher());
const res = binder.bind({template: template.nodes});
const el = template.nodes[0] as a.Element;
const attr = el.attributes[0];
const consumer = res.getConsumerOfBinding(attr);
expect(consumer).toEqual(el);
});
});
describe('matching outputs to consuming directives', () => {
it('should work for bound events', () => {
const template =
parseTemplate('<div hasOutput (outputBinding)="myHandler($event)"></div>', '', {});
const binder = new R3TargetBinder(makeSelectorMatcher());
const res = binder.bind({template: template.nodes});
const el = template.nodes[0] as a.Element;
const attr = el.outputs[0];
const consumer = res.getConsumerOfBinding(attr) as DirectiveMeta;
expect(consumer.name).toBe('HasOutput');
});
it('should bind to the encompassing node when no directive output is matched', () => {
const template = parseTemplate('<span dir (fakeOutput)="myHandler($event)"></span>', '', {});
const binder = new R3TargetBinder(makeSelectorMatcher());
const res = binder.bind({template: template.nodes});
const el = template.nodes[0] as a.Element;
const attr = el.outputs[0];
const consumer = res.getConsumerOfBinding(attr);
expect(consumer).toEqual(el);
});
});
});