diff --git a/packages/compiler/src/render3/view/t2_binder.ts b/packages/compiler/src/render3/view/t2_binder.ts index 4c24168fedce7f..61dffb82f3fb3c 100644 --- a/packages/compiler/src/render3/view/t2_binder.ts +++ b/packages/compiler/src/render3/view/t2_binder.ts @@ -16,7 +16,7 @@ import { SafePropertyRead, ThisReceiver, } from '../../expression_parser/ast'; -import {SelectorMatcher} from '../../selector'; +import {CssSelector, SelectorMatcher} from '../../selector'; import { BoundAttribute, BoundEvent, @@ -59,8 +59,71 @@ import { TargetBinder, TemplateEntity, } from './t2_api'; +import {parseTemplate} from './template'; import {createCssSelectorFromNode} from './util'; +/** + * Computes a difference between full list (first argument) and + * list of items that should be excluded from the full list (second + * argument). + */ +function diff(fullList: string[], itemsToExclude: string[]): string[] { + const exclude = new Set(itemsToExclude); + return fullList.filter((item) => !exclude.has(item)); +} + +/** + * Given a template string and a set of available directive selectors, + * computes a list of matching selectors and splits them into 2 buckets: + * (1) eagerly used in a template and (2) directives used only in defer + * blocks. Similarly, returns 2 lists of pipes (eager and deferrable). + * + * Note: deferrable directives selectors and pipes names used in `@defer` + * blocks are **candidates** and API caller should make sure that: + * + * * A Component where a given template is defined is standalone + * * Underlying dependency classes are also standalone + * * Dependency class symbols are not eagerly used in a TS file + * where a host component (that owns the template) is located + */ +export function findMatchingDirectivesAndPipes(template: string, directiveSelectors: string[]) { + const matcher = new SelectorMatcher(); + for (const selector of directiveSelectors) { + // Create a fake directive instance to satisfy the `R3TargetBinder` contract. + const fakeDirective = { + selector, + inputs: { + hasBindingPropertyName() { + return false; + }, + }, + outputs: { + hasBindingPropertyName() { + return false; + }, + }, + }; + matcher.addSelectables(CssSelector.parse(selector), [fakeDirective]); + } + const parsedTemplate = parseTemplate(template, '' /* templateUrl */); + const binder = new R3TargetBinder(matcher as any); + const bound = binder.bind({template: parsedTemplate.nodes}); + + const eagerDirectiveSelectors = bound.getEagerlyUsedDirectives().map((dir) => dir.selector!); + const allMatchedDirectiveSelectors = bound.getUsedDirectives().map((dir) => dir.selector!); + const eagerPipes = bound.getEagerlyUsedPipes(); + return { + directives: { + regular: eagerDirectiveSelectors, + deferCandidates: diff(allMatchedDirectiveSelectors, eagerDirectiveSelectors), + }, + pipes: { + regular: eagerPipes, + deferCandidates: diff(bound.getUsedPipes(), eagerPipes), + }, + }; +} + /** * Processes `Target`s with a given set of directives and performs a binding operation, which * returns an object similar to TypeScript's `ts.TypeChecker` that contains knowledge about the diff --git a/packages/compiler/test/render3/view/binding_spec.ts b/packages/compiler/test/render3/view/binding_spec.ts index 837623292668ec..6dfef515078a3b 100644 --- a/packages/compiler/test/render3/view/binding_spec.ts +++ b/packages/compiler/test/render3/view/binding_spec.ts @@ -9,7 +9,7 @@ import * as e from '../../../src/expression_parser/ast'; import * as a from '../../../src/render3/r3_ast'; import {DirectiveMeta, InputOutputPropertySet} from '../../../src/render3/view/t2_api'; -import {R3TargetBinder} from '../../../src/render3/view/t2_binder'; +import {findMatchingDirectivesAndPipes, R3TargetBinder} from '../../../src/render3/view/t2_binder'; import {parseTemplate} from '../../../src/render3/view/template'; import {CssSelector, SelectorMatcher} from '../../../src/selector'; @@ -140,6 +140,32 @@ function makeSelectorMatcher(): SelectorMatcher { return matcher; } +describe('findMatchingDirectivesAndPipes', () => { + it( + 'should match directives and detect pipes in ' + 'eager and deferrable parts of a template', + () => { + const template = ` +
+ @defer { + + } @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'], + }, + }); + }, + ); +}); + describe('t2 binding', () => { it('should bind a simple template', () => { const template = parseTemplate('
{{item.name}}
', '', {});