Skip to content
Permalink
Browse files

fix(ivy): teach template type checker about template attributes

For the template type checking to work correctly, it needs to know
what attributes are bound to expressions or directives, which may
require expressions in the template to be evaluated in a different
scope.

In inline templates, there are attributes that are now marked as
"Template" attributes. We need to ensure that the template
type checking code looks at these "bound" attributes as well as the
"input" attributes.
  • Loading branch information...
petebacondarwin committed Mar 1, 2019
1 parent deeb385 commit 2d159caa6adee2a0b007cd211334fd2a64014cee
@@ -7,6 +7,7 @@
*/

import {AST, BindingType, BoundTarget, ImplicitReceiver, PropertyRead, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
import {BoundAttribute, TextAttribute} from '@angular/compiler/src/render3/r3_ast';
import * as ts from 'typescript';

import {Reference, ReferenceEmitter} from '../../imports';
@@ -15,7 +16,6 @@ import {ImportManager, translateExpression} from '../../translator';
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta} from './api';
import {astToTypescript} from './expression';


/**
* Given a `ts.ClassDeclaration` for a component, and metadata regarding that component, compose a
* "type check block" function.
@@ -430,7 +430,7 @@ function tcbProcessTemplateDeclaration(tmpl: TmplAstTemplate, tcb: Context, scop
// any template guards, and generate them if needed.
dir.ngTemplateGuards.forEach(inputName => {
// For each template guard function on the directive, look for a binding to that input.
const boundInput = tmpl.inputs.find(i => i.name === inputName);
const boundInput = tmpl.boundAttributes.find(i => i.name === inputName);
if (boundInput !== undefined) {
// If there is such a binding, generate an expression for it.
const expr = tcbExpression(boundInput.value, tcb, scope);
@@ -523,19 +523,19 @@ function tcbGetInputBindingExpressions(
Array.isArray(inputs[key]) ? propMatch.set(inputs[key][0], key) :
propMatch.set(inputs[key] as string, key);
});

// Add a binding expression to the map for each input of the directive that has a
// matching binding.
el.inputs.filter(input => propMatch.has(input.name)).forEach(input => {
// Produce an expression representing the value of the binding.
const expr = tcbExpression(input.value, tcb, scope);

// Call the callback.
bindings.push({
property: input.name,
field: propMatch.get(input.name) !,
expression: expr,
});
(el instanceof TmplAstElement ? el.inputs : el.boundAttributes).forEach(input => {
if (propMatch.has(input.name)) {
// Produce an expression representing the value of the binding.
const expr = tcbExpression(input.value, tcb, scope);
// Call the callback.
bindings.push({
property: input.name,
field: propMatch.get(input.name) !,
expression: expr,
});
}
});
return bindings;
}
@@ -633,6 +633,7 @@ function tcbResolve(ast: AST, tcb: Context, scope: Scope): ts.Expression|null {
} else if (ast instanceof ImplicitReceiver) {
// AST instances representing variables and references look very similar to property reads from
// the component context: both have the shape PropertyRead(ImplicitReceiver, 'propertyName').
//
// `tcbExpression` will first try to `tcbResolve` the outer PropertyRead. If this works, it's
// because the `BoundTarget` found an expression target for the whole expression, and therefore
// `tcbExpression` will never attempt to `tcbResolve` the ImplicitReceiver of that PropertyRead.
@@ -662,8 +663,8 @@ function tcbResolveVariable(binding: TmplAstVariable, tcb: Context, scope: Scope
if (tmpl === null) {
throw new Error(`Expected TmplAstVariable to be mapped to a TmplAstTemplate`);
}
// Look for a context variable for the template. This should've been declared before anything
// that could reference the template's variables.
// Look for a context variable for the template. This should've been declared before anything that
// could reference the template's variables.
const ctx = scope.getTemplateCtx(tmpl);
if (ctx === null) {
throw new Error('Expected template context to exist.');
@@ -79,6 +79,12 @@ export class Element implements Node {
}

export class Template implements Node {
/**
* Bound attributes are a combination of inputs and bound template attributes.
* These are used by the template TypeChecker for working out the scope of expressions
* in templates. See `TemplateBinder`.
*/
public boundAttributes = this.inputs.concat(this.templateAttrs.filter(isBoundAttribute));
constructor(
public tagName: string, public attributes: TextAttribute[], public inputs: BoundAttribute[],
public outputs: BoundEvent[], public templateAttrs: (BoundAttribute|TextAttribute)[],
@@ -246,4 +252,8 @@ export function transformAll<Result extends Node>(
changed = changed || newNode != node;
}
return changed ? result : nodes;
}

function isBoundAttribute(attr: any): attr is BoundAttribute {
return attr instanceof BoundAttribute;
}
@@ -285,14 +285,15 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
// Associate bindings on the node with directives or with the node itself.

// Inputs:
[...node.attributes, ...node.inputs].forEach(binding => {
let dir = directives.find(dir => dir.inputs.hasOwnProperty(binding.name));
if (dir !== undefined) {
this.bindings.set(binding, dir);
} else {
this.bindings.set(binding, node);
}
});
[...node.attributes, ...(node instanceof Template ? node.boundAttributes : node.inputs)]
.forEach(binding => {
let dir = directives.find(dir => dir.inputs.hasOwnProperty(binding.name));
if (dir !== undefined) {
this.bindings.set(binding, dir);
} else {
this.bindings.set(binding, node);
}
});

// Outputs:
node.outputs.forEach(binding => {
@@ -380,7 +381,7 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {
if (template instanceof Template) {
// For <ng-template>s, process inputs, outputs, variables, and child nodes. References were
// processed in the scope of the containing template.
template.inputs.forEach(this.visitNode);
template.boundAttributes.forEach(this.visitNode);
template.outputs.forEach(this.visitNode);
template.variables.forEach(this.visitNode);
template.children.forEach(this.visitNode);
@@ -402,7 +403,7 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {

visitTemplate(template: Template) {
// First, visit the inputs, outputs of the template node.
template.inputs.forEach(this.visitNode);
template.boundAttributes.forEach(this.visitNode);
template.outputs.forEach(this.visitNode);

// References are also evaluated in the outer context.

0 comments on commit 2d159ca

Please sign in to comment.
You can’t perform that action at this time.