diff --git a/packages/compiler/src/render3/r3_view_compiler.ts b/packages/compiler/src/render3/r3_view_compiler.ts index c66f6354dc..5f02d280ac 100644 --- a/packages/compiler/src/render3/r3_view_compiler.ts +++ b/packages/compiler/src/render3/r3_view_compiler.ts @@ -32,6 +32,9 @@ const TEMPORARY_NAME = '_t'; /** The prefix reference variables */ const REFERENCE_PREFIX = '_r'; +/** The name of the implicit context reference */ +const IMPLICIT_REFERENCE = '$implicit'; + export function compileDirective( outputCtx: OutputContext, directive: CompileDirectiveMetadata, reflector: CompileReflector) { const definitionMapValues: {key: string, quoted: boolean, value: o.Expression}[] = []; @@ -98,7 +101,7 @@ export function compileComponent( new TemplateDefinitionBuilder( outputCtx, outputCtx.constantPool, reflector, CONTEXT_NAME, ROOT_SCOPE.nestedScope(), 0, templateTypeName, templateName) - .buildTemplateFunction(template); + .buildTemplateFunction(template, []); definitionMapValues.push({key: 'template', value: templateFunctionExpression, quoted: false}); @@ -223,7 +226,24 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { private bindingScope: BindingScope, private level = 0, private contextName: string|null, private templateName: string|null) {} - buildTemplateFunction(asts: TemplateAst[]): o.FunctionExpr { + buildTemplateFunction(asts: TemplateAst[], variables: VariableAst[]): o.FunctionExpr { + // Create variable bindings + for (const variable of variables) { + const variableName = variable.name; + const expression = + o.variable(this.contextParameter).prop(variable.value || IMPLICIT_REFERENCE); + const scopedName = this.bindingScope.freshReferenceName(); + const declaration = o.variable(scopedName).set(expression).toDeclStmt(o.INFERRED_TYPE, [ + o.StmtModifier.Final + ]); + + // Add the reference to the local scope. + this.bindingScope.set(variableName, scopedName); + + // Declare the local variable in binding mode + this._bindingMode.push(declaration); + } + templateVisitAll(this, asts); return o.fn( @@ -395,8 +415,9 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { this, implicit, input.value, this.bindingContext(), BindingForm.TrySimple, interpolate); this._bindingMode.push(...convertedBinding.stmts); this.instruction( - this._bindingMode, directive.sourceSpan, R3.elementProperty, - o.literal(input.templateName), o.literal(nodeIndex), convertedBinding.currValExpr); + this._bindingMode, directive.sourceSpan, R3.elementProperty, o.literal(nodeIndex), + o.literal(input.templateName), + o.importExpr(R3.bind).callFn([convertedBinding.currValExpr])); } // e.g. TodoComponentDef.r(0, 0); @@ -445,7 +466,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { const templateVisitor = new TemplateDefinitionBuilder( this.outputCtx, this.constantPool, this.reflector, templateContext, this.bindingScope.nestedScope(), this.level + 1, contextName, templateName); - const templateFunctionExpr = templateVisitor.buildTemplateFunction(ast.children); + const templateFunctionExpr = templateVisitor.buildTemplateFunction(ast.children, ast.variables); this._postfix.push(templateFunctionExpr.toDeclStmt(templateName, null)); } diff --git a/packages/compiler/test/render3/r3_view_compiler_spec.ts b/packages/compiler/test/render3/r3_view_compiler_spec.ts index 57b4342953..d4febbd8f9 100644 --- a/packages/compiler/test/render3/r3_view_compiler_spec.ts +++ b/packages/compiler/test/render3/r3_view_compiler_spec.ts @@ -331,6 +331,224 @@ describe('r3_view_compiler', () => { expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef'); expectEmit(source, locals, 'Incorrect locals constant definition'); }); + + describe('template variables', () => { + const shared = { + shared: { + 'for_of.ts': ` + import {Directive, Input, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core'; + + export interface ForOfContext { + $implicit: any; + index: number; + even: boolean; + odd: boolean; + } + + @Directive({selector: '[forOf]'}) + export class ForOfDirective { + private previous: any[]; + + constructor(private view: ViewContainerRef, private template: TemplateRef) {} + + @Input() forOf: any[]; + + ngOnChanges(simpleChanges: SimpleChanges) { + if ('forOf' in simpleChanges) { + this.update(); + } + } + + ngDoCheck(): void { + const previous = this.previous; + const current = this.forOf; + if (!previous || previous.length != current.length || + previous.some((value: any, index: number) => current[index] !== previous[index])) { + this.update(); + } + } + + private update() { + // TODO(chuckj): Not implemented yet + // this.view.clear(); + if (this.forOf) { + const current = this.forOf; + for (let i = 0; i < current.length; i++) { + const context = {$implicit: current[i], index: i, even: i % 2 == 0, odd: i % 2 == 1}; + // TODO(chuckj): Not implemented yet + // this.view.createEmbeddedView(this.template, context); + } + this.previous = [...this.forOf]; + } + } + } + ` + } + }; + + it('should support a let variable and reference', () => { + const files = { + app: { + ...shared, + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + import {ForOfDirective} from './shared/for_of'; + + @Component({ + selector: 'my-component', + template: \`\` + }) + export class MyComponent { + items = [{name: 'one'}, {name: 'two'}]; + } + + @NgModule({ + declarations: [MyComponent, ForOfDirective] + }) + export class MyModule {} + ` + } + }; + + // TODO(chuckj): Enforce this when the directives are specified + const ForDirectiveDefinition = ` + static ngDirectiveDef = IDENT.ɵdefineDirective({ + factory: function ForOfDirective_Factory() { + return new ForOfDirective(IDENT.ɵinjectViewContainerRef(), IDENT.ɵinjectTemplateRef()); + }, + features: [IDENT.ɵNgOnChangesFeature(NgForOf)], + refresh: function ForOfDirective_Refresh(directiveIndex: IDENT, elementIndex: IDENT) { + IDENT.ɵm(directiveIndex).ngDoCheck(); + }, + inputs: {forOf: 'forOf'} + }); + `; + + const MyComponentDefinition = ` + static ngComponentDef = IDENT.ɵdefineComponent({ + tag: 'my-component', + factory: function MyComponent_Factory() { return new MyComponent(); }, + template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { + if (cm) { + IDENT.ɵE(0, 'ul'); + IDENT.ɵC(1, IDENT, MyComponent_ForOfDirective_Template_1); + IDENT.ɵe(); + } + IDENT.ɵp(1, 'forOf', IDENT.ɵb(ctx.items)); + IDENT.ɵcR(1); + ForOfDirective.ngDirectiveDef.r(2, 1); + IDENT.ɵcr(); + + function MyComponent_ForOfDirective_Template_1(ctx0: IDENT, cm: IDENT) { + if (cm) { + IDENT.ɵE(0, 'li'); + IDENT.ɵT(1); + IDENT.ɵe(); + } + const IDENT = ctx0.$implicit; + IDENT.ɵt(1, IDENT.ɵb1('', IDENT.name, '')); + } + } + }); + `; + + const result = compile(files, angularFiles); + const source = result.source; + + // TODO(chuckj): Enforce this when the directives are specified + // expectEmit(source, ForDirectiveDefinition, 'Invalid directive definition'); + expectEmit(source, MyComponentDefinition, 'Invalid component definition'); + }); + + it('should support accessing parent template variables', () => { + const files = { + app: { + ...shared, + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + import {ForOfDirective} from './shared/for_of'; + + @Component({ + selector: 'my-component', + template: \` + \` + }) + export class MyComponent { + items: Item[] = [ + {name: 'one', infos: [{description: '11'}, {description: '12'}]}, + {name: 'two', infos: [{description: '21'}, {description: '22'}]} + ]; + } + + @NgModule({ + declarations: [MyComponent, ForOfDirective] + }) + export class MyModule {} + ` + } + }; + + const MyComponentDefinition = ` + static ngComponentDef = IDENT.ɵdefineComponent({ + tag: 'my-component', + factory: function MyComponent_Factory() { return new MyComponent(); }, + template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { + if (cm) { + IDENT.ɵE(0, 'ul'); + IDENT.ɵC(1, IDENT, MyComponent_ForOfDirective_Template_1); + IDENT.ɵe(); + } + IDENT.ɵp(1, 'forOf', IDENT.ɵb(ctx.items)); + IDENT.ɵcR(1); + IDENT.r(2, 1); + IDENT.ɵcr(); + + function MyComponent_ForOfDirective_Template_1(ctx0: IDENT, cm: IDENT) { + if (cm) { + IDENT.ɵE(0, 'li'); + IDENT.ɵE(1, 'div'); + IDENT.ɵT(2); + IDENT.ɵe(); + IDENT.ɵE(3, 'ul'); + IDENT.ɵC(4, IDENT, MyComponent_ForOfDirective_ForOfDirective_Template_4); + IDENT.ɵe(); + IDENT.ɵe(); + } + const IDENT = ctx0.$implicit; + IDENT.ɵp(4, 'forOf', IDENT.ɵb(IDENT.infos)); + IDENT.ɵt(2, IDENT.ɵb1('', IDENT.name, '')); + IDENT.ɵcR(4); + IDENT.r(5, 4); + IDENT.ɵcr(); + + function MyComponent_ForOfDirective_ForOfDirective_Template_4( + ctx1: IDENT, cm: IDENT) { + if (cm) { + IDENT.ɵE(0, 'li'); + IDENT.ɵT(1); + IDENT.ɵe(); + } + const IDENT = ctx1.$implicit; + IDENT.ɵt(1, IDENT.ɵb2(' ', IDENT.name, ': ', IDENT.description, ' ')); + } + } + } + });`; + + const result = compile(files, angularFiles); + const source = result.source; + expectEmit(source, MyComponentDefinition, 'Invalid component definition'); + }); + }); }); }); @@ -389,7 +607,7 @@ function expectEmit(source: string, emitted: string, description: string) { if (!m) { const contextPieceWidth = contextWidth / 2; fail( - `${description}: Expected to find ${expected} '${source.substr(0,last)}[<---HERE]${source.substr(last)}'`); + `${description}: Expected to find ${expected} '${source.substr(0,last)}[<---HERE expected "${expected}"]${source.substr(last)}'`); return; } else { last = (m.index || 0) + m[0].length; @@ -401,7 +619,7 @@ function expectEmit(source: string, emitted: string, description: string) { } const IDENT_LIKE = /^[a-z][A-Z]/; -const SPECIAL_RE_CHAR = /\/|\(|\)|\||\*|\+|\[|\]|\{|\}/g; +const SPECIAL_RE_CHAR = /\/|\(|\)|\||\*|\+|\[|\]|\{|\}|\$/g; function r(...pieces: (string | RegExp)[]): RegExp { let results: string[] = []; let first = true;