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
|
|
|
|
|
* 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';
|
2020-09-02 19:18:29 +00:00
|
|
|
import {DirectiveMeta, InputOutputPropertySet} from '../../../src/render3/view/t2_api';
|
2018-09-21 22:42:07 +00:00
|
|
|
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';
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
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']);
|
|
|
|
|
});
|
|
|
|
|
});
|
2019-04-27 22:26:13 +00:00
|
|
|
});
|