diff --git a/packages/compiler/src/render3/view/t2_api.ts b/packages/compiler/src/render3/view/t2_api.ts index b8463d8f33..e87ee6cb94 100644 --- a/packages/compiler/src/render3/view/t2_api.ts +++ b/packages/compiler/src/render3/view/t2_api.ts @@ -149,6 +149,12 @@ export interface BoundTarget { */ getNestingLevel(template: Template): number; + /** + * Get all `Reference`s and `Variables` visible within the given `Template` (or at the top level, + * if `null` is passed). + */ + getEntitiesInTemplateScope(template: Template|null): ReadonlySet; + /** * Get a list of all the directives used by the target. */ diff --git a/packages/compiler/src/render3/view/t2_binder.ts b/packages/compiler/src/render3/view/t2_binder.ts index f4abcb6a1c..5226823a53 100644 --- a/packages/compiler/src/render3/view/t2_binder.ts +++ b/packages/compiler/src/render3/view/t2_binder.ts @@ -37,6 +37,10 @@ export class R3TargetBinder implements TargetB // scopes in the template and makes them available for later use. const scope = Scope.apply(target.template); + + // Use the `Scope` to extract the entities present at every level of the template. + const templateEntities = extractTemplateEntities(scope); + // Next, perform directive matching on the template using the `DirectiveBinder`. This returns: // - directives: Map of nodes (elements & ng-templates) to the directives on them. // - bindings: Map of inputs, outputs, and attributes to the directive/element that claims @@ -49,7 +53,8 @@ export class R3TargetBinder implements TargetB const {expressions, symbols, nestingLevel, usedPipes} = TemplateBinder.apply(target.template, scope); return new R3BoundTarget( - target, directives, bindings, references, expressions, symbols, nestingLevel, usedPipes); + target, directives, bindings, references, expressions, symbols, nestingLevel, + templateEntities, usedPipes); } } @@ -71,14 +76,18 @@ class Scope implements Visitor { */ readonly childScopes = new Map(); - private constructor(readonly parentScope?: Scope) {} + private constructor(readonly parentScope: Scope|null, readonly template: Template|null) {} + + static newRootScope(): Scope { + return new Scope(null, null); + } /** * Process a template (either as a `Template` sub-template with variables, or a plain array of * template `Node`s) and construct its `Scope`. */ - static apply(template: Template|Node[]): Scope { - const scope = new Scope(); + static apply(template: Node[]): Scope { + const scope = Scope.newRootScope(); scope.ingest(template); return scope; } @@ -113,7 +122,7 @@ class Scope implements Visitor { template.references.forEach(node => this.visitReference(node)); // Next, create an inner scope and process the template within it. - const scope = new Scope(this); + const scope = new Scope(this, template); scope.ingest(template); this.childScopes.set(template, scope); } @@ -153,7 +162,7 @@ class Scope implements Visitor { if (this.namedEntities.has(name)) { // Found in the local scope. return this.namedEntities.get(name)!; - } else if (this.parentScope !== undefined) { + } else if (this.parentScope !== null) { // Not in the local scope, but there's a parent scope so check there. return this.parentScope.lookup(name); } else { @@ -517,7 +526,13 @@ export class R3BoundTarget implements BoundTar {directive: DirectiveT, node: Element|Template}|Element|Template>, private exprTargets: Map, private symbols: Map, - private nestingLevel: Map, private usedPipes: Set) {} + private nestingLevel: Map, + private templateEntities: Map>, + private usedPipes: Set) {} + + getEntitiesInTemplateScope(template: Template|null): ReadonlySet { + return this.templateEntities.get(template) ?? new Set(); + } getDirectivesOfNode(node: Element|Template): DirectiveT[]|null { return this.directives.get(node) || null; @@ -555,3 +570,40 @@ export class R3BoundTarget implements BoundTar return Array.from(this.usedPipes); } } + +function extractTemplateEntities(rootScope: Scope): Map> { + const entityMap = new Map>(); + + function extractScopeEntities(scope: Scope): Map { + if (entityMap.has(scope.template)) { + return entityMap.get(scope.template)!; + } + + const currentEntities = scope.namedEntities; + + let templateEntities: Map; + if (scope.parentScope !== null) { + templateEntities = new Map([...extractScopeEntities(scope.parentScope), ...currentEntities]); + } else { + templateEntities = new Map(currentEntities); + } + + entityMap.set(scope.template, templateEntities); + return templateEntities; + } + + const scopesToProcess: Scope[] = [rootScope]; + while (scopesToProcess.length > 0) { + const scope = scopesToProcess.pop()!; + for (const childScope of scope.childScopes.values()) { + scopesToProcess.push(childScope); + } + extractScopeEntities(scope); + } + + const templateEntities = new Map>(); + for (const [template, entities] of entityMap) { + templateEntities.set(template, new Set(entities.values())); + } + return templateEntities; +}