2018-09-21 22:42:07 +00:00
|
|
|
/**
|
|
|
|
|
* @license
|
2020-05-19 19:08:49 +00:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2018-09-21 22:42:07 +00:00
|
|
|
*
|
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
2024-09-20 15:23:15 +00:00
|
|
|
* found in the LICENSE file at https://angular.dev/license
|
2018-09-21 22:42:07 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import * as e from '../../../src/expression_parser/ast';
|
|
|
|
|
import * as a from '../../../src/render3/r3_ast';
|
2020-09-02 19:18:29 +00:00
|
|
|
import {DirectiveMeta, InputOutputPropertySet} from '../../../src/render3/view/t2_api';
|
2025-04-22 19:52:30 +00:00
|
|
|
import {findMatchingDirectivesAndPipes, R3TargetBinder} from '../../../src/render3/view/t2_binder';
|
2025-04-08 11:36:12 +00:00
|
|
|
import {parseTemplate, ParseTemplateOptions} from '../../../src/render3/view/template';
|
2025-04-22 19:52:30 +00:00
|
|
|
import {CssSelector, SelectorlessMatcher, SelectorMatcher} from '../../../src/directive_matching';
|
2018-09-21 22:42:07 +00:00
|
|
|
|
|
|
|
|
import {findExpression} from './util';
|
|
|
|
|
|
2020-09-02 19:18:29 +00:00
|
|
|
/**
|
|
|
|
|
* A `InputOutputPropertySet` which only uses an identity mapping for fields and properties.
|
|
|
|
|
*/
|
|
|
|
|
class IdentityInputMapping implements InputOutputPropertySet {
|
|
|
|
|
private names: Set<string>;
|
|
|
|
|
|
|
|
|
|
constructor(names: string[]) {
|
|
|
|
|
this.names = new Set(names);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hasBindingPropertyName(propertyName: string): boolean {
|
|
|
|
|
return this.names.has(propertyName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-22 11:33:09 +00:00
|
|
|
function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta[]> {
|
|
|
|
|
const matcher = new SelectorMatcher<DirectiveMeta[]>();
|
|
|
|
|
matcher.addSelectables(CssSelector.parse('[ngFor][ngForOf]'), [
|
|
|
|
|
{
|
|
|
|
|
name: 'NgFor',
|
|
|
|
|
exportAs: null,
|
|
|
|
|
inputs: new IdentityInputMapping(['ngForOf']),
|
|
|
|
|
outputs: new IdentityInputMapping([]),
|
|
|
|
|
isComponent: false,
|
|
|
|
|
isStructural: true,
|
|
|
|
|
selector: '[ngFor][ngForOf]',
|
|
|
|
|
animationTriggerNames: null,
|
2023-11-08 09:57:58 +00:00
|
|
|
ngContentSelectors: null,
|
|
|
|
|
preserveWhitespaces: false,
|
2022-07-22 11:33:09 +00:00
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
matcher.addSelectables(CssSelector.parse('[dir]'), [
|
|
|
|
|
{
|
|
|
|
|
name: 'Dir',
|
2023-09-18 12:05:51 +00:00
|
|
|
exportAs: ['dir'],
|
2022-07-22 11:33:09 +00:00
|
|
|
inputs: new IdentityInputMapping([]),
|
|
|
|
|
outputs: new IdentityInputMapping([]),
|
|
|
|
|
isComponent: false,
|
|
|
|
|
isStructural: false,
|
|
|
|
|
selector: '[dir]',
|
|
|
|
|
animationTriggerNames: null,
|
2023-11-08 09:57:58 +00:00
|
|
|
ngContentSelectors: null,
|
|
|
|
|
preserveWhitespaces: false,
|
2022-07-22 11:33:09 +00:00
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
matcher.addSelectables(CssSelector.parse('[hasOutput]'), [
|
|
|
|
|
{
|
|
|
|
|
name: 'HasOutput',
|
|
|
|
|
exportAs: null,
|
|
|
|
|
inputs: new IdentityInputMapping([]),
|
|
|
|
|
outputs: new IdentityInputMapping(['outputBinding']),
|
|
|
|
|
isComponent: false,
|
|
|
|
|
isStructural: false,
|
|
|
|
|
selector: '[hasOutput]',
|
|
|
|
|
animationTriggerNames: null,
|
2023-11-08 09:57:58 +00:00
|
|
|
ngContentSelectors: null,
|
|
|
|
|
preserveWhitespaces: false,
|
2022-07-22 11:33:09 +00:00
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
matcher.addSelectables(CssSelector.parse('[hasInput]'), [
|
|
|
|
|
{
|
|
|
|
|
name: 'HasInput',
|
|
|
|
|
exportAs: null,
|
|
|
|
|
inputs: new IdentityInputMapping(['inputBinding']),
|
|
|
|
|
outputs: new IdentityInputMapping([]),
|
|
|
|
|
isComponent: false,
|
|
|
|
|
isStructural: false,
|
|
|
|
|
selector: '[hasInput]',
|
|
|
|
|
animationTriggerNames: null,
|
2023-11-08 09:57:58 +00:00
|
|
|
ngContentSelectors: null,
|
|
|
|
|
preserveWhitespaces: false,
|
2022-07-22 11:33:09 +00:00
|
|
|
},
|
|
|
|
|
]);
|
2023-04-11 08:08:48 +00:00
|
|
|
matcher.addSelectables(CssSelector.parse('[sameSelectorAsInput]'), [
|
|
|
|
|
{
|
|
|
|
|
name: 'SameSelectorAsInput',
|
|
|
|
|
exportAs: null,
|
|
|
|
|
inputs: new IdentityInputMapping(['sameSelectorAsInput']),
|
|
|
|
|
outputs: new IdentityInputMapping([]),
|
|
|
|
|
isComponent: false,
|
|
|
|
|
isStructural: false,
|
|
|
|
|
selector: '[sameSelectorAsInput]',
|
|
|
|
|
animationTriggerNames: null,
|
2023-11-08 09:57:58 +00:00
|
|
|
ngContentSelectors: null,
|
|
|
|
|
preserveWhitespaces: false,
|
2023-04-11 08:08:48 +00:00
|
|
|
},
|
|
|
|
|
]);
|
2023-09-18 12:05:51 +00:00
|
|
|
matcher.addSelectables(CssSelector.parse('comp'), [
|
|
|
|
|
{
|
|
|
|
|
name: 'Comp',
|
|
|
|
|
exportAs: null,
|
|
|
|
|
inputs: new IdentityInputMapping([]),
|
|
|
|
|
outputs: new IdentityInputMapping([]),
|
|
|
|
|
isComponent: true,
|
|
|
|
|
isStructural: false,
|
|
|
|
|
selector: 'comp',
|
|
|
|
|
animationTriggerNames: null,
|
2023-11-08 09:57:58 +00:00
|
|
|
ngContentSelectors: null,
|
|
|
|
|
preserveWhitespaces: false,
|
2023-09-18 12:05:51 +00:00
|
|
|
},
|
|
|
|
|
]);
|
2023-07-25 00:06:59 +00:00
|
|
|
|
|
|
|
|
const simpleDirectives = ['a', 'b', 'c', 'd', 'e', 'f'];
|
|
|
|
|
const deferBlockDirectives = ['loading', 'error', 'placeholder'];
|
|
|
|
|
for (const dir of [...simpleDirectives, ...deferBlockDirectives]) {
|
|
|
|
|
const name = dir[0].toUpperCase() + dir.slice(1).toLowerCase();
|
|
|
|
|
matcher.addSelectables(CssSelector.parse(`[${dir}]`), [
|
|
|
|
|
{
|
|
|
|
|
name: `Dir${name}`,
|
|
|
|
|
exportAs: null,
|
|
|
|
|
inputs: new IdentityInputMapping([]),
|
|
|
|
|
outputs: new IdentityInputMapping([]),
|
|
|
|
|
isComponent: false,
|
|
|
|
|
isStructural: true,
|
|
|
|
|
selector: `[${dir}]`,
|
|
|
|
|
animationTriggerNames: null,
|
2023-11-08 09:57:58 +00:00
|
|
|
ngContentSelectors: null,
|
|
|
|
|
preserveWhitespaces: false,
|
2023-07-25 00:06:59 +00:00
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-21 22:42:07 +00:00
|
|
|
return matcher;
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-19 06:56:27 +00:00
|
|
|
describe('findMatchingDirectivesAndPipes', () => {
|
|
|
|
|
it('should match directives and detect pipes in eager and deferrable parts of a template', () => {
|
|
|
|
|
const template = `
|
|
|
|
|
<div [title]="abc | uppercase"></div>
|
|
|
|
|
@defer {
|
|
|
|
|
<my-defer-cmp [label]="abc | lowercase" />
|
|
|
|
|
} @placeholder {}
|
|
|
|
|
`;
|
|
|
|
|
const directiveSelectors = ['[title]', 'my-defer-cmp', 'not-matching'];
|
|
|
|
|
const result = findMatchingDirectivesAndPipes(template, directiveSelectors);
|
|
|
|
|
expect(result).toEqual({
|
|
|
|
|
directives: {
|
|
|
|
|
regular: ['[title]'],
|
|
|
|
|
deferCandidates: ['my-defer-cmp'],
|
|
|
|
|
},
|
|
|
|
|
pipes: {
|
|
|
|
|
regular: ['uppercase'],
|
|
|
|
|
deferCandidates: ['lowercase'],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return empty directive list if no selectors are provided', () => {
|
|
|
|
|
const template = `
|
|
|
|
|
<div [title]="abc | uppercase"></div>
|
|
|
|
|
@defer {
|
|
|
|
|
<my-defer-cmp [label]="abc | lowercase" />
|
|
|
|
|
} @placeholder {}
|
|
|
|
|
`;
|
|
|
|
|
const directiveSelectors: string[] = [];
|
|
|
|
|
const result = findMatchingDirectivesAndPipes(template, directiveSelectors);
|
|
|
|
|
expect(result).toEqual({
|
|
|
|
|
directives: {
|
|
|
|
|
regular: [],
|
|
|
|
|
deferCandidates: [],
|
|
|
|
|
},
|
|
|
|
|
// Expect pipes to be present still.
|
|
|
|
|
pipes: {
|
|
|
|
|
regular: ['uppercase'],
|
|
|
|
|
deferCandidates: ['lowercase'],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return a directive and a pipe only once (either as a regular or deferrable)', () => {
|
|
|
|
|
const template = `
|
|
|
|
|
<my-defer-cmp [label]="abc | lowercase" [title]="abc | uppercase" />
|
|
|
|
|
@defer {
|
|
|
|
|
<my-defer-cmp [label]="abc | lowercase" [title]="abc | uppercase" />
|
|
|
|
|
} @placeholder {}
|
|
|
|
|
`;
|
|
|
|
|
const directiveSelectors = ['[title]', 'my-defer-cmp', 'not-matching'];
|
|
|
|
|
const result = findMatchingDirectivesAndPipes(template, directiveSelectors);
|
|
|
|
|
expect(result).toEqual({
|
|
|
|
|
directives: {
|
|
|
|
|
regular: ['my-defer-cmp', '[title]'],
|
|
|
|
|
// All directives/components are used eagerly.
|
|
|
|
|
deferCandidates: [],
|
|
|
|
|
},
|
|
|
|
|
pipes: {
|
|
|
|
|
regular: ['lowercase', 'uppercase'],
|
|
|
|
|
// All pipes are used eagerly.
|
|
|
|
|
deferCandidates: [],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
2024-08-26 22:32:37 +00:00
|
|
|
|
|
|
|
|
it('should handle directives on elements with local refs', () => {
|
|
|
|
|
const template = `
|
|
|
|
|
<input [(ngModel)]="name" #ctrl="ngModel" required />
|
|
|
|
|
@defer {
|
|
|
|
|
<my-defer-cmp [label]="abc | lowercase" [title]="abc | uppercase" />
|
|
|
|
|
<input [(ngModel)]="name" #ctrl="ngModel" required />
|
|
|
|
|
} @placeholder {}
|
|
|
|
|
`;
|
|
|
|
|
const directiveSelectors = [
|
|
|
|
|
'[ngModel]:not([formControlName]):not([formControl])',
|
|
|
|
|
'[title]',
|
|
|
|
|
'my-defer-cmp',
|
|
|
|
|
'not-matching',
|
|
|
|
|
];
|
|
|
|
|
const result = findMatchingDirectivesAndPipes(template, directiveSelectors);
|
|
|
|
|
expect(result).toEqual({
|
|
|
|
|
directives: {
|
|
|
|
|
// `ngModel` is used both eagerly and in a defer block, thus it's located
|
|
|
|
|
// in the "regular" (eager) bucket.
|
|
|
|
|
regular: ['[ngModel]:not([formControlName]):not([formControl])'],
|
|
|
|
|
deferCandidates: ['my-defer-cmp', '[title]'],
|
|
|
|
|
},
|
|
|
|
|
pipes: {
|
|
|
|
|
regular: [],
|
|
|
|
|
deferCandidates: ['lowercase', 'uppercase'],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
2024-08-19 06:56:27 +00:00
|
|
|
});
|
|
|
|
|
|
2018-09-21 22:42:07 +00:00
|
|
|
describe('t2 binding', () => {
|
|
|
|
|
it('should bind a simple template', () => {
|
2018-11-16 17:57:23 +00:00
|
|
|
const template = parseTemplate('<div *ngFor="let item of items">{{item.name}}</div>', '', {});
|
2022-07-22 11:33:09 +00:00
|
|
|
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
|
2018-09-21 22:42:07 +00:00
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
|
2020-04-08 17:14:18 +00:00
|
|
|
const itemBinding = (findExpression(template.nodes, '{{item.name}}')! as e.Interpolation)
|
|
|
|
|
.expressions[0] as e.PropertyRead;
|
2018-09-21 22:42:07 +00:00
|
|
|
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');
|
2023-09-18 11:43:31 +00:00
|
|
|
const itemTemplate = res.getDefinitionNodeOfSymbol(itemTarget);
|
2018-09-21 22:42:07 +00:00
|
|
|
expect(itemTemplate).not.toBeNull();
|
2020-04-08 17:14:18 +00:00
|
|
|
expect(res.getNestingLevel(itemTemplate!)).toBe(1);
|
2018-09-21 22:42:07 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should match directives when binding a simple template', () => {
|
2018-11-16 17:57:23 +00:00
|
|
|
const template = parseTemplate('<div *ngFor="let item of items">{{item.name}}</div>', '', {});
|
2018-09-21 22:42:07 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const tmpl = template.nodes[0] as a.Template;
|
2020-04-08 17:14:18 +00:00
|
|
|
const directives = res.getDirectivesOfNode(tmpl)!;
|
2018-09-21 22:42:07 +00:00
|
|
|
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 20:19:38 +00:00
|
|
|
|
2019-11-03 11:12:35 +00:00
|
|
|
it('should match directives on namespaced elements', () => {
|
|
|
|
|
const template = parseTemplate('<svg><text dir>SVG</text></svg>', '', {});
|
2022-07-22 11:33:09 +00:00
|
|
|
const matcher = new SelectorMatcher<DirectiveMeta[]>();
|
|
|
|
|
matcher.addSelectables(CssSelector.parse('text[dir]'), [
|
|
|
|
|
{
|
|
|
|
|
name: 'Dir',
|
|
|
|
|
exportAs: null,
|
|
|
|
|
inputs: new IdentityInputMapping([]),
|
|
|
|
|
outputs: new IdentityInputMapping([]),
|
|
|
|
|
isComponent: false,
|
|
|
|
|
isStructural: false,
|
|
|
|
|
selector: 'text[dir]',
|
|
|
|
|
animationTriggerNames: null,
|
2023-11-08 09:57:58 +00:00
|
|
|
ngContentSelectors: null,
|
|
|
|
|
preserveWhitespaces: false,
|
2022-07-22 11:33:09 +00:00
|
|
|
},
|
|
|
|
|
]);
|
2019-11-03 11:12:35 +00:00
|
|
|
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;
|
2020-04-08 17:14:18 +00:00
|
|
|
const directives = res.getDirectivesOfNode(textNode)!;
|
2019-11-03 11:12:35 +00:00
|
|
|
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 20:19:38 +00: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;
|
2020-04-08 17:14:18 +00:00
|
|
|
const tmplDirectives = res.getDirectivesOfNode(tmpl)!;
|
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 20:19:38 +00:00
|
|
|
expect(tmplDirectives).not.toBeNull();
|
|
|
|
|
expect(tmplDirectives.length).toBe(1);
|
|
|
|
|
expect(tmplDirectives[0].name).toBe('NgFor');
|
2020-04-08 17:14:18 +00:00
|
|
|
const elDirectives = res.getDirectivesOfNode(tmpl.children[0] as a.Element)!;
|
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 20:19:38 +00:00
|
|
|
expect(elDirectives).not.toBeNull();
|
|
|
|
|
expect(elDirectives.length).toBe(1);
|
|
|
|
|
expect(elDirectives[0].name).toBe('Dir');
|
|
|
|
|
});
|
2019-07-31 21:47:25 +00:00
|
|
|
|
2024-05-02 17:29:18 +00:00
|
|
|
it('should get @let declarations when resolving entities at the root', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
@let one = 1;
|
|
|
|
|
@let two = 2;
|
|
|
|
|
@let sum = one + two;
|
|
|
|
|
`,
|
|
|
|
|
'',
|
|
|
|
|
);
|
|
|
|
|
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const entities = Array.from(res.getEntitiesInScope(null));
|
|
|
|
|
|
|
|
|
|
expect(entities.map((entity) => entity.name)).toEqual(['one', 'two', 'sum']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should scope @let declarations to their current view', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
@let one = 1;
|
|
|
|
|
|
|
|
|
|
@if (true) {
|
|
|
|
|
@let two = 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@if (true) {
|
|
|
|
|
@let three = 3;
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
'',
|
|
|
|
|
);
|
|
|
|
|
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const rootEntities = Array.from(res.getEntitiesInScope(null));
|
|
|
|
|
const firstBranchEntities = Array.from(
|
|
|
|
|
res.getEntitiesInScope((template.nodes[1] as a.IfBlock).branches[0]),
|
|
|
|
|
);
|
|
|
|
|
const secondBranchEntities = Array.from(
|
|
|
|
|
res.getEntitiesInScope((template.nodes[2] as a.IfBlock).branches[0]),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(rootEntities.map((entity) => entity.name)).toEqual(['one']);
|
|
|
|
|
expect(firstBranchEntities.map((entity) => entity.name)).toEqual(['one', 'two']);
|
|
|
|
|
expect(secondBranchEntities.map((entity) => entity.name)).toEqual(['one', 'three']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should resolve expressions to an @let declaration', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
@let value = 1;
|
|
|
|
|
{{value}}
|
|
|
|
|
`,
|
|
|
|
|
'',
|
|
|
|
|
);
|
|
|
|
|
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const interpolationWrapper = (template.nodes[1] as a.BoundText).value as e.ASTWithSource;
|
|
|
|
|
const propertyRead = (interpolationWrapper.ast as e.Interpolation).expressions[0];
|
|
|
|
|
const target = res.getExpressionTarget(propertyRead);
|
|
|
|
|
|
|
|
|
|
expect(target instanceof a.LetDeclaration).toBe(true);
|
|
|
|
|
expect((target as a.LetDeclaration)?.name).toBe('value');
|
|
|
|
|
});
|
|
|
|
|
|
2024-04-02 22:01:11 +00:00
|
|
|
it('should not resolve a `this` access to a template reference', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
<input #value>
|
|
|
|
|
{{this.value}}
|
|
|
|
|
`,
|
|
|
|
|
'',
|
|
|
|
|
);
|
|
|
|
|
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const interpolationWrapper = (template.nodes[1] as a.BoundText).value as e.ASTWithSource;
|
|
|
|
|
const propertyRead = (interpolationWrapper.ast as e.Interpolation).expressions[0];
|
|
|
|
|
const target = res.getExpressionTarget(propertyRead);
|
|
|
|
|
|
|
|
|
|
expect(target).toBe(null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not resolve a `this` access to a template variable', () => {
|
|
|
|
|
const template = parseTemplate(`<ng-template let-value>{{this.value}}</ng-template>`, '');
|
|
|
|
|
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const templateNode = template.nodes[0] as a.Template;
|
|
|
|
|
const interpolationWrapper = (templateNode.children[0] as a.BoundText).value as e.ASTWithSource;
|
|
|
|
|
const propertyRead = (interpolationWrapper.ast as e.Interpolation).expressions[0];
|
|
|
|
|
const target = res.getExpressionTarget(propertyRead);
|
|
|
|
|
|
|
|
|
|
expect(target).toBe(null);
|
|
|
|
|
});
|
|
|
|
|
|
2024-05-02 17:29:18 +00:00
|
|
|
it('should not resolve a `this` access to a `@let` declaration', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
@let value = 1;
|
|
|
|
|
{{this.value}}
|
|
|
|
|
`,
|
|
|
|
|
'',
|
|
|
|
|
);
|
|
|
|
|
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const interpolationWrapper = (template.nodes[1] as a.BoundText).value as e.ASTWithSource;
|
|
|
|
|
const propertyRead = (interpolationWrapper.ast as e.Interpolation).expressions[0];
|
|
|
|
|
const target = res.getExpressionTarget(propertyRead);
|
|
|
|
|
|
|
|
|
|
expect(target).toBe(null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should resolve the definition node of let declarations', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
@if (true) {
|
|
|
|
|
@let one = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@if (true) {
|
|
|
|
|
@let two = 2;
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
'',
|
|
|
|
|
);
|
|
|
|
|
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const firstBranch = (template.nodes[0] as a.IfBlock).branches[0];
|
|
|
|
|
const firstLet = firstBranch.children[0] as a.LetDeclaration;
|
|
|
|
|
const secondBranch = (template.nodes[1] as a.IfBlock).branches[0];
|
|
|
|
|
const secondLet = secondBranch.children[0] as a.LetDeclaration;
|
|
|
|
|
|
|
|
|
|
expect(res.getDefinitionNodeOfSymbol(firstLet)).toBe(firstBranch);
|
|
|
|
|
expect(res.getDefinitionNodeOfSymbol(secondLet)).toBe(secondBranch);
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-02 11:39:02 +00:00
|
|
|
it('should resolve an element reference without a directive matcher', () => {
|
|
|
|
|
const template = parseTemplate('<div #foo></div>', '');
|
|
|
|
|
const binder = new R3TargetBinder(null);
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const node = template.nodes[0] as a.Component;
|
|
|
|
|
const reference = node.references[0];
|
|
|
|
|
const result = res.getReferenceTarget(reference) as a.Element;
|
|
|
|
|
expect(result instanceof a.Element).toBe(true);
|
|
|
|
|
expect(result.name).toBe('div');
|
|
|
|
|
});
|
|
|
|
|
|
2019-07-31 21:47:25 +00:00
|
|
|
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');
|
|
|
|
|
});
|
|
|
|
|
|
2023-04-11 08:08:48 +00:00
|
|
|
it('should not match directives on attribute bindings with the same name as an input', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
'<ng-template [attr.sameSelectorAsInput]="123"></ng-template>',
|
|
|
|
|
'',
|
|
|
|
|
{},
|
|
|
|
|
);
|
|
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const el = template.nodes[0] as a.Element;
|
|
|
|
|
const input = el.inputs[0];
|
|
|
|
|
const consumer = res.getConsumerOfBinding(input);
|
|
|
|
|
expect(consumer).toEqual(el);
|
|
|
|
|
});
|
|
|
|
|
|
2019-07-31 21:47:25 +00:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
});
|
2020-09-11 13:17:34 +00:00
|
|
|
|
2023-07-25 00:06:59 +00:00
|
|
|
describe('extracting defer blocks info', () => {
|
|
|
|
|
it('should extract top-level defer blocks', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer {<cmp-a />}
|
|
|
|
|
@defer {<cmp-b />}
|
2023-07-25 00:06:59 +00:00
|
|
|
<cmp-c />
|
|
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-07-25 00:06:59 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const deferBlocks = bound.getDeferBlocks();
|
|
|
|
|
expect(deferBlocks.length).toBe(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract nested defer blocks and associated pipes', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer {
|
2023-07-25 00:06:59 +00:00
|
|
|
{{ name | pipeA }}
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer {
|
|
|
|
|
{{ name | pipeB }}
|
|
|
|
|
}
|
|
|
|
|
} @loading {
|
|
|
|
|
@defer {
|
|
|
|
|
{{ name | pipeC }}
|
|
|
|
|
}
|
2023-07-25 00:06:59 +00:00
|
|
|
{{ name | loading }}
|
2023-09-26 07:52:47 +00:00
|
|
|
} @placeholder {
|
|
|
|
|
@defer {
|
|
|
|
|
{{ name | pipeD }}
|
|
|
|
|
}
|
2023-07-25 00:06:59 +00:00
|
|
|
{{ name | placeholder }}
|
2023-09-26 07:52:47 +00:00
|
|
|
} @error {
|
|
|
|
|
@defer {
|
|
|
|
|
{{ name | pipeE }}
|
|
|
|
|
}
|
2023-07-25 00:06:59 +00:00
|
|
|
{{ name | error }}
|
2023-09-26 07:52:47 +00:00
|
|
|
}
|
2023-07-25 00:06:59 +00:00
|
|
|
{{ name | pipeF }}
|
|
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-07-25 00:06:59 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const deferBlocks = bound.getDeferBlocks();
|
|
|
|
|
|
|
|
|
|
expect(deferBlocks.length).toBe(5);
|
|
|
|
|
|
|
|
|
|
// Record all pipes used within :placeholder, :loading and :error sub-blocks,
|
2023-09-26 07:52:47 +00:00
|
|
|
// also record pipes used outside of any defer blocks.
|
2023-07-25 00:06:59 +00:00
|
|
|
expect(bound.getEagerlyUsedPipes()).toEqual(['placeholder', 'loading', 'error', 'pipeF']);
|
|
|
|
|
|
2023-09-26 07:52:47 +00:00
|
|
|
// Record *all* pipes from the template, including the ones from defer blocks.
|
2023-07-25 00:06:59 +00:00
|
|
|
expect(bound.getUsedPipes()).toEqual([
|
|
|
|
|
'pipeA',
|
|
|
|
|
'pipeB',
|
|
|
|
|
'pipeD',
|
|
|
|
|
'placeholder',
|
|
|
|
|
'pipeC',
|
|
|
|
|
'loading',
|
|
|
|
|
'pipeE',
|
|
|
|
|
'error',
|
|
|
|
|
'pipeF',
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
2023-08-03 13:56:41 +00:00
|
|
|
it('should identify pipes used after a nested defer block as being lazy', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer {
|
2023-08-03 13:56:41 +00:00
|
|
|
{{ name | pipeA }}
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer {
|
|
|
|
|
{{ name | pipeB }}
|
|
|
|
|
}
|
2023-08-03 13:56:41 +00:00
|
|
|
{{ name | pipeC }}
|
2023-09-26 07:52:47 +00:00
|
|
|
}
|
2023-08-03 13:56:41 +00:00
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-08-03 13:56:41 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
|
|
|
|
|
expect(bound.getUsedPipes()).toEqual(['pipeA', 'pipeB', 'pipeC']);
|
|
|
|
|
expect(bound.getEagerlyUsedPipes()).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
2023-07-25 00:06:59 +00:00
|
|
|
it('should extract nested defer blocks and associated directives', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer {
|
2023-07-25 00:06:59 +00:00
|
|
|
<img *a />
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer {
|
|
|
|
|
<img *b />
|
|
|
|
|
}
|
|
|
|
|
} @loading {
|
|
|
|
|
@defer {
|
|
|
|
|
<img *c />
|
|
|
|
|
}
|
2023-07-25 00:06:59 +00:00
|
|
|
<img *loading />
|
2023-09-26 07:52:47 +00:00
|
|
|
} @placeholder {
|
|
|
|
|
@defer {
|
|
|
|
|
<img *d />
|
|
|
|
|
}
|
2023-07-25 00:06:59 +00:00
|
|
|
<img *placeholder />
|
2023-09-26 07:52:47 +00:00
|
|
|
} @error {
|
|
|
|
|
@defer {
|
|
|
|
|
<img *e />
|
|
|
|
|
}
|
2023-07-25 00:06:59 +00:00
|
|
|
<img *error />
|
2023-09-26 07:52:47 +00:00
|
|
|
}
|
2023-07-25 00:06:59 +00:00
|
|
|
<img *f />
|
|
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-07-25 00:06:59 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const deferBlocks = bound.getDeferBlocks();
|
|
|
|
|
|
|
|
|
|
expect(deferBlocks.length).toBe(5);
|
|
|
|
|
|
2023-09-26 07:52:47 +00:00
|
|
|
// Record all directives used within placeholder, loading and error sub-blocks,
|
|
|
|
|
// also record directives used outside of any defer blocks.
|
2023-07-25 00:06:59 +00:00
|
|
|
const eagerDirs = bound.getEagerlyUsedDirectives();
|
|
|
|
|
expect(eagerDirs.length).toBe(4);
|
|
|
|
|
expect(eagerDirs.map((dir) => dir.name)).toEqual([
|
|
|
|
|
'DirPlaceholder',
|
|
|
|
|
'DirLoading',
|
|
|
|
|
'DirError',
|
|
|
|
|
'DirF',
|
|
|
|
|
]);
|
|
|
|
|
|
2023-09-26 07:52:47 +00:00
|
|
|
// Record *all* directives from the template, including the ones from defer blocks.
|
2023-07-25 00:06:59 +00:00
|
|
|
const allDirs = bound.getUsedDirectives();
|
|
|
|
|
expect(allDirs.length).toBe(9);
|
|
|
|
|
expect(allDirs.map((dir) => dir.name)).toEqual([
|
|
|
|
|
'DirA',
|
|
|
|
|
'DirB',
|
|
|
|
|
'DirD',
|
|
|
|
|
'DirPlaceholder',
|
|
|
|
|
'DirC',
|
|
|
|
|
'DirLoading',
|
|
|
|
|
'DirE',
|
|
|
|
|
'DirError',
|
|
|
|
|
'DirF',
|
|
|
|
|
]);
|
|
|
|
|
});
|
2023-08-03 13:56:41 +00:00
|
|
|
|
|
|
|
|
it('should identify directives used after a nested defer block as being lazy', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer {
|
2023-08-03 13:56:41 +00:00
|
|
|
<img *a />
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer {<img *b />}
|
2023-08-03 13:56:41 +00:00
|
|
|
<img *c />
|
2023-09-26 07:52:47 +00:00
|
|
|
}
|
2023-08-03 13:56:41 +00:00
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-08-03 13:56:41 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const allDirs = bound.getUsedDirectives().map((dir) => dir.name);
|
|
|
|
|
const eagerDirs = bound.getEagerlyUsedDirectives().map((dir) => dir.name);
|
|
|
|
|
|
|
|
|
|
expect(allDirs).toEqual(['DirA', 'DirB', 'DirC']);
|
|
|
|
|
expect(eagerDirs).toEqual([]);
|
|
|
|
|
});
|
2023-09-18 12:05:51 +00:00
|
|
|
|
|
|
|
|
it('should identify a trigger element that is a parent of the deferred block', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
<div #trigger>
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer (on viewport(trigger)) {}
|
2023-09-18 12:05:51 +00:00
|
|
|
</div>
|
|
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-18 12:05:51 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl?.name).toBe('div');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should identify a trigger element outside of the deferred block', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
<div>
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer (on viewport(trigger)) {}
|
2023-09-18 12:05:51 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<div>
|
|
|
|
|
<button #trigger></button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-18 12:05:51 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl?.name).toBe('button');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should identify a trigger element in a parent embedded view', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
<div *ngFor="let item of items">
|
|
|
|
|
<button #trigger></button>
|
|
|
|
|
|
|
|
|
|
<div *ngFor="let child of item.children">
|
|
|
|
|
<div *ngFor="let grandchild of child.children">
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer (on viewport(trigger)) {}
|
2023-09-18 12:05:51 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-18 12:05:51 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl?.name).toBe('button');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should identify a trigger element inside the placeholder', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer (on viewport(trigger)) {
|
2023-09-18 12:05:51 +00:00
|
|
|
main
|
2023-09-26 07:52:47 +00:00
|
|
|
} @placeholder {
|
|
|
|
|
<button #trigger></button>
|
|
|
|
|
}
|
2023-09-18 12:05:51 +00:00
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-18 12:05:51 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl?.name).toBe('button');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not identify a trigger inside the main content block', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer (on viewport(trigger)) {<button #trigger></button>}
|
2023-09-18 12:05:51 +00:00
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-18 12:05:51 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should identify a trigger element on a component', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer (on viewport(trigger)) {}
|
2023-09-18 12:05:51 +00:00
|
|
|
|
|
|
|
|
<comp #trigger/>
|
|
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-18 12:05:51 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl?.name).toBe('comp');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should identify a trigger element on a directive', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer (on viewport(trigger)) {}
|
2023-09-18 12:05:51 +00:00
|
|
|
|
|
|
|
|
<button dir #trigger="dir"></button>
|
|
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-18 12:05:51 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl?.name).toBe('button');
|
|
|
|
|
});
|
|
|
|
|
|
2023-09-27 09:34:35 +00:00
|
|
|
it('should identify an implicit trigger inside the placeholder block', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
<div #trigger>
|
|
|
|
|
@defer (on viewport) {} @placeholder {<button></button>}
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-27 09:34:35 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
2023-10-30 09:15:50 +00:00
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl?.name).toBe('button');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should identify an implicit trigger inside the placeholder block with comments', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
@defer (on viewport) {
|
|
|
|
|
main
|
|
|
|
|
} @placeholder {
|
|
|
|
|
<!-- before -->
|
|
|
|
|
<button #trigger></button>
|
|
|
|
|
<!-- after -->
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
'',
|
|
|
|
|
);
|
|
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
2023-09-27 09:34:35 +00:00
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl?.name).toBe('button');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not identify an implicit trigger if the placeholder has multiple root nodes', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
<div #trigger>
|
|
|
|
|
@defer (on viewport) {} @placeholder {<button></button><div></div>}
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-27 09:34:35 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not identify an implicit trigger if there is no placeholder', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
<div #trigger>
|
|
|
|
|
@defer (on viewport) {}
|
|
|
|
|
<button></button>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-27 09:34:35 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not identify an implicit trigger if the placeholder has a single root text node', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
<div #trigger>
|
|
|
|
|
@defer (on viewport) {} @placeholder {hello}
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-27 09:34:35 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
2023-09-18 12:05:51 +00:00
|
|
|
it('should not identify a trigger inside a sibling embedded view', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
<div *ngIf="cond">
|
|
|
|
|
<button #trigger></button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer (on viewport(trigger)) {}
|
2023-09-18 12:05:51 +00:00
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-18 12:05:51 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not identify a trigger element in an embedded view inside the placeholder', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer (on viewport(trigger)) {
|
2023-09-18 12:05:51 +00:00
|
|
|
main
|
2023-09-26 07:52:47 +00:00
|
|
|
} @placeholder {
|
|
|
|
|
<div *ngIf="cond"><button #trigger></button></div>
|
|
|
|
|
}
|
2023-09-18 12:05:51 +00:00
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-18 12:05:51 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not identify a trigger element inside the a deferred block within the placeholder', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer (on viewport(trigger)) {
|
2023-09-18 12:05:51 +00:00
|
|
|
main
|
2023-09-26 07:52:47 +00:00
|
|
|
} @placeholder {
|
|
|
|
|
@defer {
|
|
|
|
|
<button #trigger></button>
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-09-18 12:05:51 +00:00
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-18 12:05:51 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not identify a trigger element on a template', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
2023-09-26 07:52:47 +00:00
|
|
|
@defer (on viewport(trigger)) {}
|
2023-09-18 12:05:51 +00:00
|
|
|
|
|
|
|
|
<ng-template #trigger></ng-template>
|
|
|
|
|
`,
|
2023-10-03 21:12:04 +00:00
|
|
|
'',
|
|
|
|
|
);
|
2023-09-18 12:05:51 +00:00
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const bound = binder.bind({template: template.nodes});
|
|
|
|
|
const block = Array.from(bound.getDeferBlocks())[0];
|
|
|
|
|
const triggerEl = bound.getDeferredTriggerTarget(block, block.triggers.viewport!);
|
|
|
|
|
expect(triggerEl).toBeNull();
|
|
|
|
|
});
|
2023-07-25 00:06:59 +00:00
|
|
|
});
|
|
|
|
|
|
2020-09-11 13:17:34 +00:00
|
|
|
describe('used pipes', () => {
|
|
|
|
|
it('should record pipes used in interpolations', () => {
|
|
|
|
|
const template = parseTemplate('{{value|date}}', '', {});
|
|
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
expect(res.getUsedPipes()).toEqual(['date']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should record pipes used in bound attributes', () => {
|
|
|
|
|
const template = parseTemplate('<person [age]="age|number"></person>', '', {});
|
|
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
expect(res.getUsedPipes()).toEqual(['number']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should record pipes used in bound template attributes', () => {
|
|
|
|
|
const template = parseTemplate('<ng-template [ngIf]="obs|async"></ng-template>', '', {});
|
|
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
expect(res.getUsedPipes()).toEqual(['async']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should record pipes used in ICUs', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`<span i18n>{count|number, plural,
|
|
|
|
|
=1 { {{value|date}} }
|
|
|
|
|
}</span>`,
|
|
|
|
|
'',
|
|
|
|
|
{},
|
|
|
|
|
);
|
|
|
|
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
expect(res.getUsedPipes()).toEqual(['number', 'date']);
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-04-22 19:52:30 +00:00
|
|
|
|
|
|
|
|
describe('selectorless', () => {
|
|
|
|
|
const options: ParseTemplateOptions = {enableSelectorless: true};
|
|
|
|
|
const baseMeta = {
|
|
|
|
|
selector: null,
|
|
|
|
|
inputs: new IdentityInputMapping([]),
|
|
|
|
|
outputs: new IdentityInputMapping([]),
|
|
|
|
|
exportAs: null,
|
|
|
|
|
isStructural: false,
|
|
|
|
|
ngContentSelectors: null,
|
|
|
|
|
preserveWhitespaces: false,
|
|
|
|
|
animationTriggerNames: null,
|
|
|
|
|
isComponent: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function makeSelectorlessMatcher(
|
2025-04-28 07:47:42 +00:00
|
|
|
directives: (DirectiveMeta | {root: DirectiveMeta; additionalDirectives: DirectiveMeta[]})[],
|
2025-04-23 12:18:21 +00:00
|
|
|
): SelectorlessMatcher<DirectiveMeta> {
|
2025-04-22 19:52:30 +00:00
|
|
|
const registry = new Map<string, DirectiveMeta[]>();
|
2025-04-28 07:47:42 +00:00
|
|
|
const isSingleDirective = (value: any): value is DirectiveMeta =>
|
|
|
|
|
!value.root && !value.additionalDirectives;
|
2025-04-22 19:52:30 +00:00
|
|
|
|
|
|
|
|
for (const dir of directives) {
|
2025-04-28 07:47:42 +00:00
|
|
|
if (isSingleDirective(dir)) {
|
|
|
|
|
registry.set(dir.name, [dir]);
|
|
|
|
|
} else {
|
|
|
|
|
registry.set(dir.root.name, [dir.root, ...dir.additionalDirectives]);
|
|
|
|
|
}
|
2025-04-22 19:52:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new SelectorlessMatcher(registry);
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-28 07:47:42 +00:00
|
|
|
it('should resolve directives applied on a component node', () => {
|
2025-04-22 19:52:30 +00:00
|
|
|
const template = parseTemplate('<MyComp @Dir @OtherDir/>', '', options);
|
|
|
|
|
const binder = new R3TargetBinder(
|
|
|
|
|
makeSelectorlessMatcher([
|
|
|
|
|
{
|
2025-04-28 07:47:42 +00:00
|
|
|
root: {
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'MyComp',
|
|
|
|
|
isComponent: true,
|
|
|
|
|
},
|
|
|
|
|
additionalDirectives: [
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'MyHostDir',
|
|
|
|
|
},
|
|
|
|
|
],
|
2025-04-22 19:52:30 +00:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'Dir',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'OtherDir',
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const node = template.nodes[0] as a.Component;
|
2025-04-28 07:47:42 +00:00
|
|
|
expect(res.getDirectivesOfNode(node)?.map((d) => d.name)).toEqual(['MyComp', 'MyHostDir']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should resolve directives applied on a directive node', () => {
|
|
|
|
|
const template = parseTemplate('<MyComp @Dir @OtherDir/>', '', options);
|
|
|
|
|
const binder = new R3TargetBinder(
|
|
|
|
|
makeSelectorlessMatcher([
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'MyComp',
|
|
|
|
|
isComponent: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
root: {
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'Dir',
|
|
|
|
|
},
|
|
|
|
|
additionalDirectives: [
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'HostDir',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'OtherDir',
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const dirs = (template.nodes[0] as a.Component).directives;
|
|
|
|
|
expect(res.getDirectivesOfNode(dirs[0])?.map((d) => d.name)).toEqual(['Dir', 'HostDir']);
|
|
|
|
|
expect(res.getDirectivesOfNode(dirs[1])?.map((d) => d.name)).toEqual(['OtherDir']);
|
2025-04-22 19:52:30 +00:00
|
|
|
});
|
|
|
|
|
|
2025-04-28 07:47:42 +00:00
|
|
|
it('should not apply selectorless directives on an element node', () => {
|
2025-04-22 19:52:30 +00:00
|
|
|
const template = parseTemplate('<div @Dir @OtherDir></div>', '', options);
|
|
|
|
|
const binder = new R3TargetBinder(
|
|
|
|
|
makeSelectorlessMatcher([
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'Dir',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'OtherDir',
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const node = template.nodes[0] as a.Element;
|
2025-04-28 07:47:42 +00:00
|
|
|
expect(res.getDirectivesOfNode(node)).toBe(null);
|
2025-04-22 19:52:30 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should resolve a reference on a component node to the component', () => {
|
|
|
|
|
const template = parseTemplate('<MyComp #foo/>', '', options);
|
|
|
|
|
const binder = new R3TargetBinder(
|
|
|
|
|
makeSelectorlessMatcher([
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'MyComp',
|
|
|
|
|
isComponent: true,
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const node = template.nodes[0] as a.Component;
|
|
|
|
|
const reference = node.references[0];
|
|
|
|
|
const result = res.getReferenceTarget(reference) as {node: a.Node; directive: DirectiveMeta};
|
|
|
|
|
expect(result.node).toBe(node);
|
|
|
|
|
expect(result.directive.name).toBe('MyComp');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should resolve a reference on a directive node to the component', () => {
|
|
|
|
|
const template = parseTemplate('<div @Dir(#foo)></div>', '', options);
|
|
|
|
|
const binder = new R3TargetBinder(
|
|
|
|
|
makeSelectorlessMatcher([
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'Dir',
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const node = template.nodes[0] as a.Element;
|
|
|
|
|
const directive = node.directives[0];
|
|
|
|
|
const reference = directive.references[0];
|
|
|
|
|
const result = res.getReferenceTarget(reference) as {node: a.Node; directive: DirectiveMeta};
|
|
|
|
|
expect(result.node).toBe(directive);
|
|
|
|
|
expect(result.directive.name).toBe('Dir');
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-02 11:39:02 +00:00
|
|
|
it('should resolve a reference on an element when using a selectorless matcher', () => {
|
|
|
|
|
const template = parseTemplate('<div #foo></div>', '', options);
|
|
|
|
|
const binder = new R3TargetBinder(makeSelectorlessMatcher([]));
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const node = template.nodes[0] as a.Component;
|
|
|
|
|
const reference = node.references[0];
|
|
|
|
|
const result = res.getReferenceTarget(reference) as a.Element;
|
|
|
|
|
expect(result instanceof a.Element).toBe(true);
|
|
|
|
|
expect(result.name).toBe('div');
|
|
|
|
|
});
|
|
|
|
|
|
2025-04-22 19:52:30 +00:00
|
|
|
it('should get consumer of component bindings', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
'<MyComp [input]="value" static="value" (output)="doStuff()" [doesNotExist]="value" [attr.input]="value"/>',
|
|
|
|
|
'',
|
|
|
|
|
options,
|
|
|
|
|
);
|
|
|
|
|
const binder = new R3TargetBinder(
|
|
|
|
|
makeSelectorlessMatcher([
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'MyComp',
|
|
|
|
|
isComponent: true,
|
|
|
|
|
inputs: new IdentityInputMapping(['input', 'static']),
|
|
|
|
|
outputs: new IdentityInputMapping(['output']),
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const node = template.nodes[0] as a.Component;
|
|
|
|
|
const input = node.inputs[0];
|
|
|
|
|
const staticAttr = node.attributes[0];
|
|
|
|
|
const output = node.outputs[0];
|
|
|
|
|
const doesNotExist = node.inputs[1];
|
|
|
|
|
const attrBinding = node.attributes[1];
|
|
|
|
|
|
|
|
|
|
expect((res.getConsumerOfBinding(input) as DirectiveMeta)?.name).toBe('MyComp');
|
|
|
|
|
expect((res.getConsumerOfBinding(staticAttr) as DirectiveMeta)?.name).toBe('MyComp');
|
|
|
|
|
expect((res.getConsumerOfBinding(output) as DirectiveMeta)?.name).toBe('MyComp');
|
|
|
|
|
expect(res.getConsumerOfBinding(doesNotExist)).toBe(null);
|
|
|
|
|
expect(res.getConsumerOfBinding(attrBinding)).toBe(null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should get consumer of directive bindings', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
'<div @Dir([input]="value" static="value" (output)="doStuff()" [doesNotExist]="value")></div>',
|
|
|
|
|
'',
|
|
|
|
|
options,
|
|
|
|
|
);
|
|
|
|
|
const binder = new R3TargetBinder(
|
|
|
|
|
makeSelectorlessMatcher([
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'Dir',
|
|
|
|
|
inputs: new IdentityInputMapping(['input', 'static']),
|
|
|
|
|
outputs: new IdentityInputMapping(['output']),
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
const node = template.nodes[0] as a.Element;
|
|
|
|
|
const directive = node.directives[0];
|
|
|
|
|
const input = directive.inputs[0];
|
|
|
|
|
const staticAttr = directive.attributes[0];
|
|
|
|
|
const output = directive.outputs[0];
|
|
|
|
|
const doesNotExist = directive.inputs[1];
|
|
|
|
|
|
|
|
|
|
expect((res.getConsumerOfBinding(input) as DirectiveMeta)?.name).toBe('Dir');
|
|
|
|
|
expect((res.getConsumerOfBinding(staticAttr) as DirectiveMeta)?.name).toBe('Dir');
|
|
|
|
|
expect((res.getConsumerOfBinding(output) as DirectiveMeta)?.name).toBe('Dir');
|
|
|
|
|
expect(res.getConsumerOfBinding(doesNotExist)).toBe(null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should get eagerly-used selectorless directives', () => {
|
|
|
|
|
const template = parseTemplate('<MyComp @Dir @OtherDir/>', '', options);
|
|
|
|
|
const binder = new R3TargetBinder(
|
|
|
|
|
makeSelectorlessMatcher([
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'MyComp',
|
|
|
|
|
isComponent: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'Dir',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'OtherDir',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'UnusedDir',
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
expect(res.getUsedDirectives().map((dir) => dir.name)).toEqual(['MyComp', 'Dir', 'OtherDir']);
|
|
|
|
|
expect(res.getEagerlyUsedDirectives().map((dir) => dir.name)).toEqual([
|
|
|
|
|
'MyComp',
|
|
|
|
|
'Dir',
|
|
|
|
|
'OtherDir',
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should get deferred selectorless directives', () => {
|
|
|
|
|
const template = parseTemplate('@defer {<MyComp @Dir @OtherDir/>}', '', options);
|
|
|
|
|
const binder = new R3TargetBinder(
|
|
|
|
|
makeSelectorlessMatcher([
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'MyComp',
|
|
|
|
|
isComponent: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'Dir',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'OtherDir',
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
expect(res.getUsedDirectives().map((dir) => dir.name)).toEqual(['MyComp', 'Dir', 'OtherDir']);
|
|
|
|
|
expect(res.getEagerlyUsedDirectives().map((dir) => dir.name)).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should get selectorless directives nested in other code', () => {
|
|
|
|
|
const template = parseTemplate(
|
|
|
|
|
`
|
|
|
|
|
<section>
|
|
|
|
|
@if (someCond) {
|
|
|
|
|
<MyComp>
|
|
|
|
|
<div>
|
|
|
|
|
<h1>
|
|
|
|
|
<span @Dir></span>
|
|
|
|
|
</h1>
|
|
|
|
|
</div>
|
|
|
|
|
</MyComp>
|
|
|
|
|
}
|
|
|
|
|
</section>
|
|
|
|
|
`,
|
|
|
|
|
'',
|
|
|
|
|
options,
|
|
|
|
|
);
|
|
|
|
|
const binder = new R3TargetBinder(
|
|
|
|
|
makeSelectorlessMatcher([
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'MyComp',
|
|
|
|
|
isComponent: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'Dir',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'UnusedDir',
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
expect(res.getUsedDirectives().map((dir) => dir.name)).toEqual(['MyComp', 'Dir']);
|
|
|
|
|
expect(res.getEagerlyUsedDirectives().map((dir) => dir.name)).toEqual(['MyComp', 'Dir']);
|
|
|
|
|
});
|
2025-04-23 12:18:21 +00:00
|
|
|
|
|
|
|
|
it('should check whether a referenced directive exists', () => {
|
|
|
|
|
const template = parseTemplate('<MyComp @MissingDir/><MissingComp @Dir/>', '', options);
|
|
|
|
|
const binder = new R3TargetBinder(
|
|
|
|
|
makeSelectorlessMatcher([
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'MyComp',
|
|
|
|
|
isComponent: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...baseMeta,
|
|
|
|
|
name: 'Dir',
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
const res = binder.bind({template: template.nodes});
|
|
|
|
|
expect(res.referencedDirectiveExists('MyComp')).toBe(true);
|
|
|
|
|
expect(res.referencedDirectiveExists('Dir')).toBe(true);
|
|
|
|
|
expect(res.referencedDirectiveExists('MissingDir')).toBe(false);
|
|
|
|
|
expect(res.referencedDirectiveExists('MissingComp')).toBe(false);
|
|
|
|
|
});
|
2025-04-22 19:52:30 +00:00
|
|
|
});
|
2019-04-27 22:26:13 +00:00
|
|
|
});
|