diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index 98814d35eb..ad4dd14e2c 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -20,6 +20,7 @@ import {NgModuleCompiler} from '../ng_module_compiler'; import {OutputEmitter} from '../output/abstract_emitter'; import * as o from '../output/output_ast'; import {ParseError} from '../parse_util'; +import {compilePipe as compileIvyPipe} from '../render3/r3_pipe_compiler'; import {compileComponent as compileIvyComponent, compileDirective as compileIvyDirective} from '../render3/r3_view_compiler'; import {CompiledStylesheet, StyleCompiler} from '../style_compiler'; import {SummaryResolver} from '../summary_resolver'; @@ -356,14 +357,22 @@ export class AotCompiler { error( `Cannot determine the module for component '${identifierName(directiveMetadata.type)}'`); - const {template: parsedTemplate} = + const {template: parsedTemplate, pipes: parsedPipes} = this._parseTemplate(directiveMetadata, module, module.transitiveModule.directives); - compileIvyComponent(context, directiveMetadata, parsedTemplate, this._reflector); + compileIvyComponent( + context, directiveMetadata, parsedPipes, parsedTemplate, this._reflector); } else { compileIvyDirective(context, directiveMetadata, this._reflector); } }); + pipes.forEach(pipeType => { + const pipeMetadata = this._metadataResolver.getPipeMetadata(pipeType); + if (pipeMetadata) { + compileIvyPipe(context, pipeMetadata, this._reflector); + } + }); + injectables.forEach(injectable => this._injectableCompiler.compile(injectable, context)); if (context.statements && context.statements.length > 0) { diff --git a/packages/compiler/src/constant_pool.ts b/packages/compiler/src/constant_pool.ts index 0775bacf97..00835f00b2 100644 --- a/packages/compiler/src/constant_pool.ts +++ b/packages/compiler/src/constant_pool.ts @@ -11,7 +11,7 @@ import {OutputContext, error} from './util'; const CONSTANT_PREFIX = '_c'; -export const enum DefinitionKind {Injector, Directive, Component} +export const enum DefinitionKind {Injector, Directive, Component, Pipe} /** * A node that is a place-holder that allows the node to be replaced when the actual @@ -51,6 +51,7 @@ export class ConstantPool { private injectorDefinitions = new Map(); private directiveDefinitions = new Map(); private componentDefinitions = new Map(); + private pipeDefinitions = new Map(); private nextNameIndex = 0; @@ -75,18 +76,19 @@ export class ConstantPool { return fixup; } - getDefinition(type: any, kind: DefinitionKind, ctx: OutputContext): o.Expression { - const declarations = kind == DefinitionKind.Component ? - this.componentDefinitions : - kind == DefinitionKind.Directive ? this.directiveDefinitions : this.injectorDefinitions; - let fixup = declarations.get(type); + getDefinition(type: any, kind: DefinitionKind, ctx: OutputContext, forceShared: boolean = false): + o.Expression { + const definitions = this.definitionsOf(kind); + let fixup = definitions.get(type); + let newValue = false; if (!fixup) { - const property = kind == DefinitionKind.Component ? - 'ngComponentDef' : - kind == DefinitionKind.Directive ? 'ngDirectiveDef' : 'ngInjectorDef'; + const property = this.propertyNameOf(kind); fixup = new FixupExpression(ctx.importExpr(type).prop(property)); - declarations.set(type, fixup); - } else if (!fixup.shared) { + definitions.set(type, fixup); + newValue = true; + } + + if ((!newValue && !fixup.shared) || (newValue && forceShared)) { const name = this.freshName(); this.statements.push( o.variable(name).set(fixup.resolved).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final])); @@ -104,6 +106,36 @@ export class ConstantPool { */ uniqueName(prefix: string): string { return `${prefix}${this.nextNameIndex++}`; } + private definitionsOf(kind: DefinitionKind): Map { + switch (kind) { + case DefinitionKind.Component: + return this.componentDefinitions; + case DefinitionKind.Directive: + return this.directiveDefinitions; + case DefinitionKind.Injector: + return this.injectorDefinitions; + case DefinitionKind.Pipe: + return this.pipeDefinitions; + } + error(`Unknown definition kind ${kind}`); + return this.componentDefinitions; + } + + public propertyNameOf(kind: DefinitionKind): string { + switch (kind) { + case DefinitionKind.Component: + return 'ngComponentDef'; + case DefinitionKind.Directive: + return 'ngDirectiveDef'; + case DefinitionKind.Injector: + return 'ngInjectorDef'; + case DefinitionKind.Pipe: + return 'ngPipeDef'; + } + error(`Unknown definition kind ${kind}`); + return ''; + } + private freshName(): string { return this.uniqueName(CONSTANT_PREFIX); } private keyOf(expression: o.Expression) { diff --git a/packages/compiler/src/expression_parser/ast.ts b/packages/compiler/src/expression_parser/ast.ts index b6b1b03f61..2a579cdd3a 100644 --- a/packages/compiler/src/expression_parser/ast.ts +++ b/packages/compiler/src/expression_parser/ast.ts @@ -435,6 +435,173 @@ export class AstTransformer implements AstVisitor { } } +// A transformer that only creates new nodes if the transformer makes a change or +// a change is made a child node. +export class AstMemoryEfficientTransformer implements AstVisitor { + visitImplicitReceiver(ast: ImplicitReceiver, context: any): AST { return ast; } + + visitInterpolation(ast: Interpolation, context: any): Interpolation { + const expressions = this.visitAll(ast.expressions); + if (expressions !== ast.expressions) + return new Interpolation(ast.span, ast.strings, expressions); + return ast; + } + + visitLiteralPrimitive(ast: LiteralPrimitive, context: any): AST { return ast; } + + visitPropertyRead(ast: PropertyRead, context: any): AST { + const receiver = ast.receiver.visit(this); + if (receiver !== ast.receiver) { + return new PropertyRead(ast.span, receiver, ast.name); + } + return ast; + } + + visitPropertyWrite(ast: PropertyWrite, context: any): AST { + const receiver = ast.receiver.visit(this); + const value = ast.value.visit(this); + if (receiver !== ast.receiver || value !== ast.value) { + return new PropertyWrite(ast.span, receiver, ast.name, value); + } + return ast; + } + + visitSafePropertyRead(ast: SafePropertyRead, context: any): AST { + const receiver = ast.receiver.visit(this); + if (receiver !== ast.receiver) { + return new SafePropertyRead(ast.span, receiver, ast.name); + } + return ast; + } + + visitMethodCall(ast: MethodCall, context: any): AST { + const receiver = ast.receiver.visit(this); + if (receiver !== ast.receiver) { + return new MethodCall(ast.span, receiver, ast.name, this.visitAll(ast.args)); + } + return ast; + } + + visitSafeMethodCall(ast: SafeMethodCall, context: any): AST { + const receiver = ast.receiver.visit(this); + const args = this.visitAll(ast.args); + if (receiver !== ast.receiver || args !== ast.args) { + return new SafeMethodCall(ast.span, receiver, ast.name, args); + } + return ast; + } + + visitFunctionCall(ast: FunctionCall, context: any): AST { + const target = ast.target && ast.target.visit(this); + const args = this.visitAll(ast.args); + if (target !== ast.target || args !== ast.args) { + return new FunctionCall(ast.span, target, args); + } + return ast; + } + + visitLiteralArray(ast: LiteralArray, context: any): AST { + const expressions = this.visitAll(ast.expressions); + if (expressions !== ast.expressions) { + return new LiteralArray(ast.span, expressions); + } + return ast; + } + + visitLiteralMap(ast: LiteralMap, context: any): AST { + const values = this.visitAll(ast.values); + if (values !== ast.values) { + return new LiteralMap(ast.span, ast.keys, values); + } + return ast; + } + + visitBinary(ast: Binary, context: any): AST { + const left = ast.left.visit(this); + const right = ast.right.visit(this); + if (left !== ast.left || right !== ast.right) { + return new Binary(ast.span, ast.operation, left, right); + } + return ast; + } + + visitPrefixNot(ast: PrefixNot, context: any): AST { + const expression = ast.expression.visit(this); + if (expression !== ast.expression) { + return new PrefixNot(ast.span, expression); + } + return ast; + } + + visitNonNullAssert(ast: NonNullAssert, context: any): AST { + const expression = ast.expression.visit(this); + if (expression !== ast.expression) { + return new NonNullAssert(ast.span, expression); + } + return ast; + } + + visitConditional(ast: Conditional, context: any): AST { + const condition = ast.condition.visit(this); + const trueExp = ast.trueExp.visit(this); + const falseExp = ast.falseExp.visit(this); + if (condition !== ast.condition || trueExp !== ast.trueExp || falseExp !== falseExp) { + return new Conditional(ast.span, condition, trueExp, falseExp); + } + return ast; + } + + visitPipe(ast: BindingPipe, context: any): AST { + const exp = ast.exp.visit(this); + const args = this.visitAll(ast.args); + if (exp !== ast.exp || args !== ast.args) { + return new BindingPipe(ast.span, exp, ast.name, args); + } + return ast; + } + + visitKeyedRead(ast: KeyedRead, context: any): AST { + const obj = ast.obj.visit(this); + const key = ast.key.visit(this); + if (obj !== ast.obj || key !== ast.key) { + return new KeyedRead(ast.span, obj, key); + } + return ast; + } + + visitKeyedWrite(ast: KeyedWrite, context: any): AST { + const obj = ast.obj.visit(this); + const key = ast.key.visit(this); + const value = ast.value.visit(this); + if (obj !== ast.obj || key !== ast.key || value !== ast.value) { + return new KeyedWrite(ast.span, obj, key, value); + } + return ast; + } + + visitAll(asts: any[]): any[] { + const res = new Array(asts.length); + let modified = false; + for (let i = 0; i < asts.length; ++i) { + const original = asts[i]; + const value = original.visit(this); + res[i] = value; + modified = modified || value !== original; + } + return modified ? res : asts; + } + + visitChain(ast: Chain, context: any): AST { + const expressions = this.visitAll(ast.expressions); + if (expressions !== ast.expressions) { + return new Chain(ast.span, expressions); + } + return ast; + } + + visitQuote(ast: Quote, context: any): AST { return ast; } +} + export function visitAstChildren(ast: AST, visitor: AstVisitor, context?: any) { function visit(ast: AST) { visitor.visit && visitor.visit(ast, context) || ast.visit(visitor, context); diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 09a4a4a0fe..4fbe0e5954 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -21,6 +21,7 @@ export class Identifiers { /* Methods */ static NEW_METHOD = 'n'; static HOST_BINDING_METHOD = 'h'; + static TRANSFORM_METHOD = 'transform'; /* Instructions */ static createElement: o.ExternalReference = {name: 'ɵE', moduleName: CORE}; @@ -63,8 +64,16 @@ export class Identifiers { static interpolation8: o.ExternalReference = {name: 'ɵi8', moduleName: CORE}; static interpolationV: o.ExternalReference = {name: 'ɵiV', moduleName: CORE}; + static pipeBind1: o.ExternalReference = {name: 'ɵpb1', moduleName: CORE}; + static pipeBind2: o.ExternalReference = {name: 'ɵpb2', moduleName: CORE}; + static pipeBind3: o.ExternalReference = {name: 'ɵpb3', moduleName: CORE}; + static pipeBind4: o.ExternalReference = {name: 'ɵpb4', moduleName: CORE}; + static pipeBindV: o.ExternalReference = {name: 'ɵpbV', moduleName: CORE}; + static load: o.ExternalReference = {name: 'ɵld', moduleName: CORE}; + static pipe: o.ExternalReference = {name: 'ɵPp', moduleName: CORE}; + static projection: o.ExternalReference = {name: 'ɵP', moduleName: CORE}; static projectionDef: o.ExternalReference = {name: 'ɵpD', moduleName: CORE}; @@ -88,5 +97,7 @@ export class Identifiers { moduleName: CORE, }; + static definePipe: o.ExternalReference = {name: 'ɵdefinePipe', moduleName: CORE}; + static NgOnChangesFeature: o.ExternalReference = {name: 'ɵNgOnChangesFeature', moduleName: CORE}; } \ No newline at end of file diff --git a/packages/compiler/src/render3/r3_pipe_compiler.ts b/packages/compiler/src/render3/r3_pipe_compiler.ts new file mode 100644 index 0000000000..3593418efd --- /dev/null +++ b/packages/compiler/src/render3/r3_pipe_compiler.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * 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 {CompileDirectiveMetadata, CompilePipeMetadata, identifierName} from '../compile_metadata'; +import {CompileReflector} from '../compile_reflector'; +import {DefinitionKind} from '../constant_pool'; +import * as o from '../output/output_ast'; +import {OutputContext, error} from '../util'; + +import {Identifiers as R3} from './r3_identifiers'; +import {createFactory} from './r3_view_compiler'; + +export function compilePipe( + outputCtx: OutputContext, pipe: CompilePipeMetadata, reflector: CompileReflector) { + const definitionMapValues: {key: string, quoted: boolean, value: o.Expression}[] = []; + + // e.g. 'type: MyPipe` + definitionMapValues.push( + {key: 'type', value: outputCtx.importExpr(pipe.type.reference), quoted: false}); + + // e.g. factory: function MyPipe_Factory() { return new MyPipe(); }, + const templateFactory = createFactory(pipe.type, outputCtx, reflector); + definitionMapValues.push({key: 'factory', value: templateFactory, quoted: false}); + + // e.g. pure: true + if (pipe.pure) { + definitionMapValues.push({key: 'pure', value: o.literal(true), quoted: false}); + } + + const className = identifierName(pipe.type) !; + className || error(`Cannot resolve the name of ${pipe.type}`); + + outputCtx.statements.push(new o.ClassStmt( + /* name */ className, + /* parent */ null, + /* fields */[new o.ClassField( + /* name */ outputCtx.constantPool.propertyNameOf(DefinitionKind.Pipe), + /* type */ o.INFERRED_TYPE, + /* modifiers */[o.StmtModifier.Static], + /* initializer */ o.importExpr(R3.definePipe).callFn([o.literalMap( + definitionMapValues)]))], + /* getters */[], + /* constructorMethod */ new o.ClassMethod(null, [], []), + /* methods */[])); +} \ No newline at end of file diff --git a/packages/compiler/src/render3/r3_view_compiler.ts b/packages/compiler/src/render3/r3_view_compiler.ts index 0b4cc16284..f127880733 100644 --- a/packages/compiler/src/render3/r3_view_compiler.ts +++ b/packages/compiler/src/render3/r3_view_compiler.ts @@ -8,9 +8,9 @@ import {CompileDirectiveMetadata, CompilePipeSummary, CompileTokenMetadata, CompileTypeMetadata, flatten, identifierName, rendererTypeName, tokenReference, viewClassName} from '../compile_metadata'; import {CompileReflector} from '../compile_reflector'; -import {BindingForm, BuiltinConverter, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter'; +import {BindingForm, BuiltinConverter, ConvertPropertyBindingResult, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter'; import {ConstantPool, DefinitionKind} from '../constant_pool'; -import {AST} from '../expression_parser/ast'; +import {AST, AstMemoryEfficientTransformer, AstTransformer, BindingPipe, FunctionCall, ImplicitReceiver, LiteralPrimitive, MethodCall, ParseSpan, PropertyRead} from '../expression_parser/ast'; import {Identifiers} from '../identifiers'; import {LifecycleHooks} from '../lifecycle_reflector'; import * as o from '../output/output_ast'; @@ -21,6 +21,7 @@ import {OutputContext, error} from '../util'; import {Identifiers as R3} from './r3_identifiers'; + /** Name of the context parameter passed into a template function */ const CONTEXT_NAME = 'ctx'; @@ -62,7 +63,7 @@ export function compileDirective( /* name */ className, /* parent */ null, /* fields */[new o.ClassField( - /* name */ 'ngDirectiveDef', + /* name */ outputCtx.constantPool.propertyNameOf(DefinitionKind.Directive), /* type */ o.INFERRED_TYPE, /* modifiers */[o.StmtModifier.Static], /* initializer */ o.importExpr(R3.defineDirective).callFn([o.literalMap( @@ -73,8 +74,8 @@ export function compileDirective( } export function compileComponent( - outputCtx: OutputContext, component: CompileDirectiveMetadata, template: TemplateAst[], - reflector: CompileReflector) { + outputCtx: OutputContext, component: CompileDirectiveMetadata, pipes: CompilePipeSummary[], + template: TemplateAst[], reflector: CompileReflector) { const definitionMapValues: {key: string, quoted: boolean, value: o.Expression}[] = []; // e.g. `type: MyApp` @@ -112,10 +113,11 @@ export function compileComponent( // e.g. `template: function MyComponent_Template(_ctx, _cm) {...}` const templateTypeName = component.type.reference.name; const templateName = templateTypeName ? `${templateTypeName}_Template` : null; + const pipeMap = new Map(pipes.map<[string, CompilePipeSummary]>(pipe => [pipe.name, pipe])); const templateFunctionExpression = new TemplateDefinitionBuilder( outputCtx, outputCtx.constantPool, reflector, CONTEXT_NAME, ROOT_SCOPE.nestedScope(), 0, - component.template !.ngContentSelectors, templateTypeName, templateName) + component.template !.ngContentSelectors, templateTypeName, templateName, pipeMap) .buildTemplateFunction(template, []); definitionMapValues.push({key: 'template', value: templateFunctionExpression, quoted: false}); @@ -143,7 +145,7 @@ export function compileComponent( /* name */ className, /* parent */ null, /* fields */[new o.ClassField( - /* name */ 'ngComponentDef', + /* name */ outputCtx.constantPool.propertyNameOf(DefinitionKind.Component), /* type */ o.INFERRED_TYPE, /* modifiers */[o.StmtModifier.Static], /* initializer */ o.importExpr(R3.defineComponent).callFn([o.literalMap( @@ -199,6 +201,22 @@ function interpolate(args: o.Expression[]): o.Expression { return o.importExpr(R3.interpolationV).callFn([o.literalArr(args)]); } +function pipeBinding(args: o.Expression[]): o.ExternalReference { + switch (args.length) { + case 0: + // The first parameter to pipeBind is always the value to be transformed followed + // by arg.length arguments so the total number of arguments to pipeBind are + // arg.length + 1. + return R3.pipeBind1; + case 1: + return R3.pipeBind2; + case 2: + return R3.pipeBind3; + default: + return R3.pipeBindV; + } +} + class BindingScope { private map = new Map(); private referenceNameIndex = 0; @@ -219,10 +237,10 @@ class BindingScope { return null; } - set(name: string, variableName: string): BindingScope { + set(name: string, value: o.Expression): BindingScope { !this.map.has(name) || error(`The name ${name} is already defined in scope to be ${this.map.get(name)}`); - this.map.set(name, o.variable(variableName)); + this.map.set(name, value); return this; } @@ -236,7 +254,7 @@ class BindingScope { } } -const ROOT_SCOPE = new BindingScope(null).set('$event', '$event'); +const ROOT_SCOPE = new BindingScope(null).set('$event', o.variable('$event')); class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { private _dataIndex = 0; @@ -251,6 +269,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { private _postfix: o.Statement[] = []; private _contentProjections: Map; private _projectionDefinitionIndex = 0; + private _pipeConverter: PipeConverter; private unsupported = unsupported; private invalid = invalid; @@ -258,7 +277,23 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { private outputCtx: OutputContext, private constantPool: ConstantPool, private reflector: CompileReflector, private contextParameter: string, private bindingScope: BindingScope, private level = 0, private ngContentSelectors: string[], - private contextName: string|null, private templateName: string|null) {} + private contextName: string|null, private templateName: string|null, + private pipes: Map) { + this._pipeConverter = + new PipeConverter(() => this.allocateDataSlot(), (name, localName, slot, value) => { + bindingScope.set(localName, value); + const pipe = pipes.get(name) !; + pipe || error(`Could not find pipe ${name}`); + const pipeDefinition = constantPool.getDefinition( + pipe.type.reference, DefinitionKind.Pipe, outputCtx, /* forceShared */ true); + this._creationMode.push( + o.importExpr(R3.pipe) + .callFn([ + o.literal(slot), pipeDefinition, pipeDefinition.callMethod(R3.NEW_METHOD, []) + ]) + .toStmt()); + }); + } buildTemplateFunction(asts: TemplateAst[], variables: VariableAst[]): o.FunctionExpr { // Create variable bindings @@ -272,7 +307,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { ]); // Add the reference to the local scope. - this.bindingScope.set(variableName, scopedName); + this.bindingScope.set(variableName, o.variable(scopedName)); // Declare the local variable in binding mode this._bindingMode.push(declaration); @@ -332,8 +367,10 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { o.INFERRED_TYPE, null, this.templateName); } + // LocalResolver getLocal(name: string): o.Expression|null { return this.bindingScope.get(name); } + // TemplateAstVisitor visitNgContent(ast: NgContentAst) { const info = this._contentProjections.get(ast) !; info || error(`Expected ${ast.sourceSpan} to be included in content projection collection`); @@ -361,6 +398,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { }; } + // TemplateAstVisitor visitElement(ast: ElementAst) { let bindingCount = 0; const elementIndex = this.allocateDataSlot(); @@ -410,7 +448,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { this._bindingMode.push(o.variable(variableName, o.INFERRED_TYPE) .set(o.importExpr(R3.load).callFn([o.literal(slot)])) .toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final])); - this.bindingScope.set(reference.name, variableName); + this.bindingScope.set(reference.name, o.variable(variableName)); return [reference.name, reference.originalValue]; })).map(value => o.literal(value)); parameters.push( @@ -434,18 +472,14 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { if (input.isAnimation) { this.unsupported('animations'); } - // TODO(chuckj): Built-in transform? - const convertedBinding = convertPropertyBinding( - this, implicit, input.value, this.bindingContext(), BindingForm.TrySimple, interpolate); - this._bindingMode.push(...convertedBinding.stmts); - const parameters = - [o.literal(elementIndex), o.literal(input.name), convertedBinding.currValExpr]; + const convertedBinding = this.convertPropertyBinding(implicit, input.value); + const parameters = [o.literal(elementIndex), o.literal(input.name), convertedBinding]; const instruction = BINDING_INSTRUCTION_MAP[input.type]; if (instruction) { // TODO(chuckj): runtime: security context? this.instruction( this._bindingMode, input.sourceSpan, instruction, o.literal(elementIndex), - o.literal(input.name), convertedBinding.currValExpr); + o.literal(input.name), convertedBinding); } else { this.unsupported(`binding ${PropertyBindingType[input.type]}`); } @@ -479,13 +513,10 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { // Bindings for (const input of directive.inputs) { - const convertedBinding = convertPropertyBinding( - this, implicit, input.value, this.bindingContext(), BindingForm.TrySimple, interpolate); - this._bindingMode.push(...convertedBinding.stmts); + const convertedBinding = this.convertPropertyBinding(implicit, input.value); this.instruction( this._bindingMode, directive.sourceSpan, R3.elementProperty, o.literal(nodeIndex), - o.literal(input.templateName), - o.importExpr(R3.bind).callFn([convertedBinding.currValExpr])); + o.literal(input.templateName), o.importExpr(R3.bind).callFn([convertedBinding])); } // e.g. MyDirective.ngDirectiveDef.h(0, 0); @@ -501,6 +532,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { } } + // TemplateAstVisitor visitEmbeddedTemplate(ast: EmbeddedTemplateAst) { const templateIndex = this.allocateDataSlot(); @@ -539,7 +571,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { const templateVisitor = new TemplateDefinitionBuilder( this.outputCtx, this.constantPool, this.reflector, templateContext, this.bindingScope.nestedScope(), this.level + 1, this.ngContentSelectors, contextName, - templateName); + templateName, this.pipes); const templateFunctionExpr = templateVisitor.buildTemplateFunction(ast.children, ast.variables); this._postfix.push(templateFunctionExpr.toDeclStmt(templateName, null)); } @@ -551,6 +583,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { readonly visitElementProperty = invalid; readonly visitAttr = invalid; + // TemplateAstVisitor visitBoundText(ast: BoundTextAst) { const nodeIndex = this.allocateDataSlot(); @@ -563,6 +596,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { this.bind(o.variable(CONTEXT_NAME), ast.value, ast.sourceSpan)); } + // TemplateAstVisitor visitText(ast: TextAst) { // Text is defined in creation mode only. this.instruction( @@ -600,8 +634,10 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { } private convertPropertyBinding(implicit: o.Expression, value: AST): o.Expression { + const pipesConvertedValue = value.visit(this._pipeConverter); const convertedPropertyBinding = convertPropertyBinding( - this, implicit, value, this.bindingContext(), BindingForm.TrySimple, interpolate); + this, implicit, pipesConvertedValue, this.bindingContext(), BindingForm.TrySimple, + interpolate); this._refreshMode.push(...convertedPropertyBinding.stmts); return convertedPropertyBinding.currValExpr; } @@ -611,7 +647,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { } } -function createFactory( +export function createFactory( type: CompileTypeMetadata, outputCtx: OutputContext, reflector: CompileReflector): o.FunctionExpr { let args: o.Expression[] = []; @@ -652,6 +688,35 @@ function createFactory( o.INFERRED_TYPE, null, type.reference.name ? `${type.reference.name}_Factory` : null); } +class PipeConverter extends AstMemoryEfficientTransformer { + private pipeSlots = new Map(); + constructor( + private allocateSlot: () => number, + private definePipe: + (name: string, localName: string, slot: number, value: o.Expression) => void) { + super(); + } + + // AstMemoryEfficientTransformer + visitPipe(ast: BindingPipe, context: any): AST { + // Allocate a slot to create the pipe + let slot = this.pipeSlots.get(ast.name); + if (slot == null) { + slot = this.allocateSlot(); + this.pipeSlots.set(ast.name, slot); + } + const slotPseudoLocal = `PIPE:${slot}`; + const target = new PropertyRead(ast.span, new ImplicitReceiver(ast.span), slotPseudoLocal); + const bindingId = pipeBinding(ast.args); + this.definePipe(ast.name, slotPseudoLocal, slot, o.importExpr(bindingId)); + const value = ast.exp.visit(this); + const args = this.visitAll(ast.args); + + return new FunctionCall( + ast.span, target, [new LiteralPrimitive(ast.span, slot), value, ...args]); + } +} + function invalid(arg: o.Expression | o.Statement | TemplateAst): never { throw new Error( `Invalid state: Visitor ${this.constructor.name} doesn't handle ${o.constructor.name}`); diff --git a/packages/compiler/test/render3/mock_compile.ts b/packages/compiler/test/render3/mock_compile.ts index 28cb8f1f5d..638d26962f 100644 --- a/packages/compiler/test/render3/mock_compile.ts +++ b/packages/compiler/test/render3/mock_compile.ts @@ -6,12 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileDirectiveMetadata, CompileMetadataResolver, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver} from '@angular/compiler'; +import {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileDirectiveMetadata, CompileMetadataResolver, CompilePipeSummary, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver} from '@angular/compiler'; import {ViewEncapsulation} from '@angular/core'; import * as ts from 'typescript'; import {ConstantPool} from '../../src/constant_pool'; import * as o from '../../src/output/output_ast'; +import {compilePipe} from '../../src/render3/r3_pipe_compiler'; import {compileComponent, compileDirective} from '../../src/render3/r3_view_compiler'; import {OutputContext} from '../../src/util'; import {MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, arrayToMockDir, expectNoDiagnostics, settings, setup, toMockFileArray} from '../aot/test_util'; @@ -173,7 +174,7 @@ export function compile( // to generate a template definition. const analyzedModules = analyzeNgModules(sourceFiles, compilerHost, symbolResolver, resolver); - const directives = Array.from(analyzedModules.ngModuleByPipeOrDirective.keys()); + const pipesOrDirectives = Array.from(analyzedModules.ngModuleByPipeOrDirective.keys()); const fakeOutputContext: OutputContext = { genFilePath: 'fakeFactory.ts', @@ -193,20 +194,20 @@ export function compile( constantPool: new ConstantPool() }; - // Load All directives - for (const directive of directives) { - const module = analyzedModules.ngModuleByPipeOrDirective.get(directive) !; + // Load all directives and pipes + for (const pipeOrDirective of pipesOrDirectives) { + const module = analyzedModules.ngModuleByPipeOrDirective.get(pipeOrDirective) !; resolver.loadNgModuleDirectiveAndPipeMetadata(module.type.reference, true); } // Compile the directives. - for (const directive of directives) { - const module = analyzedModules.ngModuleByPipeOrDirective.get(directive); + for (const pipeOrDirective of pipesOrDirectives) { + const module = analyzedModules.ngModuleByPipeOrDirective.get(pipeOrDirective); if (!module || !module.type.reference.filePath.startsWith('/app')) { continue; } - if (resolver.isDirective(directive)) { - const metadata = resolver.getDirectiveMetadata(directive); + if (resolver.isDirective(pipeOrDirective)) { + const metadata = resolver.getDirectiveMetadata(pipeOrDirective); if (metadata.isComponent) { const fakeUrl = 'ng://fake-template-url.html'; const htmlAst = htmlParser.parse(metadata.template !.template !, fakeUrl); @@ -217,11 +218,16 @@ export function compile( module.transitiveModule.pipes.map(pipe => resolver.getPipeSummary(pipe.reference)); const parsedTemplate = templateParser.parse( metadata, htmlAst, directives, pipes, module.schemas, fakeUrl, false); - - compileComponent(fakeOutputContext, metadata, parsedTemplate.template, staticReflector); + compileComponent( + fakeOutputContext, metadata, pipes, parsedTemplate.template, staticReflector); } else { compileDirective(fakeOutputContext, metadata, staticReflector); } + } else if (resolver.isPipe(pipeOrDirective)) { + const metadata = resolver.getPipeMetadata(pipeOrDirective); + if (metadata) { + compilePipe(fakeOutputContext, metadata, staticReflector); + } } } diff --git a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts new file mode 100644 index 0000000000..5935766ddf --- /dev/null +++ b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts @@ -0,0 +1,716 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * 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 {MockDirectory, setup} from '../aot/test_util'; +import {compile, expectEmit} from './mock_compile'; + +/* These tests are codified version of the tests in compiler_canonical_spec.ts. Every + * test in compiler_canonical_spec.ts should have a corresponding test here. + */ +describe('compiler compliance', () => { + const angularFiles = setup({ + compileAngular: true, + compileAnimations: false, + compileCommon: true, + }); + + describe('elements', () => { + it('should translate DOM structure', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-component', + template: \`
Hello World!
\` + }) + export class MyComponent {} + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + // The factory should look like this: + const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }'; + + // The template should look like this (where IDENT is a wild card for an identifier): + const template = ` + const $c1$ = ['class', 'my-app', 'title', 'Hello']; + … + template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵE(0, 'div', $c1$); + $r3$.ɵT(1, 'Hello '); + $r3$.ɵE(2, 'b'); + $r3$.ɵT(3, 'World'); + $r3$.ɵe(); + $r3$.ɵT(4, '!'); + $r3$.ɵe(); + } + } + `; + + + const result = compile(files, angularFiles); + + expectEmit(result.source, factory, 'Incorrect factory'); + expectEmit(result.source, template, 'Incorrect template'); + }); + }); + + describe('components & directives', () => { + it('should instantiate directives', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, Directive, NgModule} from '@angular/core'; + + @Component({selector: 'child', template: 'child-view'}) + export class ChildComponent {} + + @Directive({selector: '[some-directive]'}) + export class SomeDirective {} + + @Component({selector: 'my-component', template: '!'}) + export class MyComponent {} + + @NgModule({declarations: [ChildComponent, SomeDirective, MyComponent]}) + export class MyModule{} + ` + } + }; + + // ChildComponent definition should be: + const ChildComponentDefinition = ` + static ngComponentDef = $r3$.ɵdefineComponent({ + type: ChildComponent, + tag: 'child', + factory: function ChildComponent_Factory() { return new ChildComponent(); }, + template: function ChildComponent_Template(ctx: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵT(0, 'child-view'); + } + } + });`; + + // SomeDirective definition should be: + const SomeDirectiveDefinition = ` + static ngDirectiveDef = $r3$.ɵdefineDirective({ + type: SomeDirective, + factory: function SomeDirective_Factory() {return new SomeDirective(); } + }); + `; + + // MyComponent definition should be: + const MyComponentDefinition = ` + const $c1$ = ['some-directive', '']; + const $c2$ = [SomeDirective]; + … + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyComponent, + tag: 'my-component', + factory: function MyComponent_Factory() { return new MyComponent(); }, + template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵE(0, ChildComponent, IDENT, IDENT); + $r3$.ɵe(); + $r3$.ɵT(3, '!'); + } + ChildComponent.ngComponentDef.h(1, 0); + SomeDirective.ngDirectiveDef.h(2, 0); + $r3$.ɵr(1, 0); + $r3$.ɵr(2, 0); + } + }); + `; + + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, ChildComponentDefinition, 'Incorrect ChildComponent.ngComponentDef'); + expectEmit(source, SomeDirectiveDefinition, 'Incorrect SomeDirective.ngDirectiveDef'); + expectEmit(source, MyComponentDefinition, 'Incorrect MyComponentDefinition.ngComponentDef'); + }); + + it('should support structural directives', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, Directive, NgModule, TemplateRef} from '@angular/core'; + + @Directive({selector: '[if]'}) + export class IfDirective { + constructor(template: TemplateRef) { } + } + + @Component({ + selector: 'my-component', + template: '
  • {{salutation}} {{foo}}
' + }) + export class MyComponent { + salutation = 'Hello'; + } + + @NgModule({declarations: [IfDirective, MyComponent]}) + export class MyModule {} + ` + } + }; + + const IfDirectiveDefinition = ` + static ngDirectiveDef = $r3$.ɵdefineDirective({ + type: IfDirective, + factory: function IfDirective_Factory() { return new IfDirective($r3$.ɵinjectTemplateRef()); } + });`; + const MyComponentDefinition = ` + const $c1$ = ['foo', '']; + const $c2$ = [IfDirective]; + … + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyComponent, + tag: 'my-component', + factory: function MyComponent_Factory() { return new MyComponent(); }, + template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵE(0, 'ul', null, null, $c1$); + $r3$.ɵC(2, $c2$, MyComponent_IfDirective_Template_2); + $r3$.ɵe(); + } + const $foo$ = $r3$.ɵld(1); + IfDirective.ngDirectiveDef.h(3,2); + $r3$.ɵcR(2); + $r3$.ɵr(3,2); + $r3$.ɵcr(); + + function MyComponent_IfDirective_Template_2(ctx0: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵE(0, 'li'); + $r3$.ɵT(1); + $r3$.ɵe(); + } + $r3$.ɵt(1, $r3$.ɵi2('', ctx.salutation, ' ', $foo$, '')); + } + } + });`; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, IfDirectiveDefinition, 'Incorrect IfDirective.ngDirectiveDef'); + expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef'); + }); + + it('should support content projection', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, Directive, NgModule, TemplateRef} from '@angular/core'; + + @Component({selector: 'simple', template: '
'}) + export class SimpleComponent {} + + @Component({ + selector: 'complex', + template: \` +
+
\` + }) + export class ComplexComponent { } + + @NgModule({declarations: [SimpleComponent, ComplexComponent]}) + export class MyModule {} + + @Component({ + selector: 'my-app', + template: 'content ' + }) + export class MyApp {} + ` + } + }; + + const SimpleComponentDefinition = ` + static ngComponentDef = $r3$.ɵdefineComponent({ + type: SimpleComponent, + tag: 'simple', + factory: function SimpleComponent_Factory() { return new SimpleComponent(); }, + template: function SimpleComponent_Template(ctx: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵpD(0); + $r3$.ɵE(1, 'div'); + $r3$.ɵP(2, 0); + $r3$.ɵe(); + } + } + });`; + + const ComplexComponentDefinition = ` + const $c1$ = [[[['span', 'title', 'tofirst'], null]], [[['span', 'title', 'tosecond'], null]]]; + const $c2$ = ['id','first']; + const $c3$ = ['id','second']; + … + static ngComponentDef = $r3$.ɵdefineComponent({ + type: ComplexComponent, + tag: 'complex', + factory: function ComplexComponent_Factory() { return new ComplexComponent(); }, + template: function ComplexComponent_Template(ctx: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵpD(0, $c1$); + $r3$.ɵE(1, 'div', $c2$); + $r3$.ɵP(2, 0, 1); + $r3$.ɵe(); + $r3$.ɵE(3, 'div', $c3$); + $r3$.ɵP(4, 0, 2); + $r3$.ɵe(); + } + } + }); + `; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(result.source, SimpleComponentDefinition, 'Incorrect SimpleComponent definition'); + expectEmit( + result.source, ComplexComponentDefinition, 'Incorrect ComplexComponent definition'); + }); + + describe('pipes', () => { + + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule, Pipe, PipeTransform, OnDestroy} from '@angular/core'; + + @Pipe({ + name: 'myPipe', + pure: false + }) + export class MyPipe implements PipeTransform, + OnDestroy { + transform(value: any, ...args: any[]) { return value; } + ngOnDestroy(): void { } + } + + @Pipe({ + name: 'myPurePipe', + pure: true, + }) + export class MyPurePipe implements PipeTransform { + transform(value: any, ...args: any[]) { return value; } + } + + @Component({selector: 'my-app', template: '{{name | myPipe:size | myPurePipe:size }}'}) + export class MyApp { + name = 'World'; + size = 0; + } + + @NgModule({declarations:[MyPipe, MyPurePipe, MyApp]}) + export class MyModule {} + ` + } + }; + + it('should render pipes', () => { + const MyPipeDefinition = ` + static ngPipeDef = $r3$.ɵdefinePipe( + {type: MyPipe, factory: function MyPipe_Factory() { return new MyPipe(); }}); + `; + + const MyPurePipeDefinition = ` + static ngPipeDef = $r3$.ɵdefinePipe({ + type: MyPurePipe, + factory: function MyPurePipe_Factory() { return new MyPurePipe(); }, + pure: true + });`; + + const MyAppDefinition = ` + const $MyPurePipe_ngPipeDef$ = MyPurePipe.ngPipeDef; + const $MyPipe_ngPipeDef$ = MyPipe.ngPipeDef; + … + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyApp, + tag: 'my-app', + factory: function MyApp_Factory() { return new MyApp(); }, + template: function MyApp_Template(ctx: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵT(0); + $r3$.ɵPp(1, $MyPurePipe_ngPipeDef$, $MyPurePipe_ngPipeDef$.n()); + $r3$.ɵPp(2, $MyPipe_ngPipeDef$, $MyPipe_ngPipeDef$.n()); + } + $r3$.ɵt(0, $r3$.ɵi1('', $r3$.ɵpb2(1, $r3$.ɵpb2(2,ctx.name, ctx.size), ctx.size), '')); + } + });`; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, MyPipeDefinition, 'Invalid pipe definition'); + expectEmit(source, MyPurePipeDefinition, 'Invalid pure pipe definition'); + expectEmit(source, MyAppDefinition, 'Invalid MyApp definition'); + }); + }); + + it('local reference', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({selector: 'my-component', template: 'Hello {{user.value}}!'}) + export class MyComponent {} + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + const MyComponentDefinition = ` + const $c1$ = ['user', '']; + … + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyComponent, + tag: 'my-component', + factory: function MyComponent_Factory() { return new MyComponent(); }, + template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵE(0, 'input', null, null, $c1$); + $r3$.ɵe(); + $r3$.ɵT(2); + } + const $user$ = $r3$.ɵld(1); + $r3$.ɵt(2, $r3$.ɵi1('Hello ', $user$.value, '!')); + } + }); + `; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef'); + }); + + describe('lifecycle hooks', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, Input, NgModule} from '@angular/core'; + + let events: string[] = []; + + @Component({selector: 'lifecycle-comp', template: ''}) + export class LifecycleComp { + @Input('name') nameMin: string; + + ngOnChanges() { events.push('changes' + this.nameMin); } + + ngOnInit() { events.push('init' + this.nameMin); } + ngDoCheck() { events.push('check' + this.nameMin); } + + ngAfterContentInit() { events.push('content init' + this.nameMin); } + ngAfterContentChecked() { events.push('content check' + this.nameMin); } + + ngAfterViewInit() { events.push('view init' + this.nameMin); } + ngAfterViewChecked() { events.push('view check' + this.nameMin); } + + ngOnDestroy() { events.push(this.nameMin); } + } + + @Component({ + selector: 'simple-layout', + template: \` + + + \` + }) + export class SimpleLayout { + name1 = '1'; + name2 = '2'; + } + + @NgModule({declarations: [LifecycleComp, SimpleLayout]}) + export class LifecycleModule {} + ` + } + }; + + it('should gen hooks with a few simple components', () => { + const LifecycleCompDefinition = ` + static ngComponentDef = $r3$.ɵdefineComponent({ + type: LifecycleComp, + tag: 'lifecycle-comp', + factory: function LifecycleComp_Factory() { return new LifecycleComp(); }, + template: function LifecycleComp_Template(ctx: IDENT, cm: IDENT) {}, + inputs: {nameMin: 'name'}, + features: [$r3$.ɵNgOnChangesFeature(LifecycleComp)] + });`; + + const SimpleLayoutDefinition = ` + const $c1$ = LifecycleComp.ngComponentDef; + … + static ngComponentDef = $r3$.ɵdefineComponent({ + type: SimpleLayout, + tag: 'simple-layout', + factory: function SimpleLayout_Factory() { return new SimpleLayout(); }, + template: function SimpleLayout_Template(ctx: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵE(0, LifecycleComp); + $r3$.ɵe(); + $r3$.ɵE(2, LifecycleComp); + $r3$.ɵe(); + } + $r3$.ɵp(0, 'name', $r3$.ɵb(ctx.name1)); + $r3$.ɵp(2, 'name', $r3$.ɵb(ctx.name2)); + $c1$.h(1, 0); + $c1$.h(3, 2); + $r3$.ɵr(1, 0); + $r3$.ɵr(3, 2); + } + });`; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, LifecycleCompDefinition, 'Invalid LifecycleComp definition'); + expectEmit(source, SimpleLayoutDefinition, 'Invalid SimpleLayout 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: \`
  • {{item.name}}
\` + }) + 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 = $r3$.ɵdefineDirective({ + type: ForOfDirective, + factory: function ForOfDirective_Factory() { + return new ForOfDirective($r3$.ɵinjectViewContainerRef(), $r3$.ɵinjectTemplateRef()); + }, + features: [$r3$.ɵNgOnChangesFeature(NgForOf)], + inputs: {forOf: 'forOf'} + }); + `; + + const MyComponentDefinition = ` + const $c1$ = [ForOfDirective]; + … + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyComponent, + tag: 'my-component', + factory: function MyComponent_Factory() { return new MyComponent(); }, + template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵE(0, 'ul'); + $r3$.ɵC(1, $c1$, MyComponent_ForOfDirective_Template_1); + $r3$.ɵe(); + } + $r3$.ɵp(1, 'forOf', $r3$.ɵb(ctx.items)); + ForOfDirective.ngDirectiveDef.h(2, 1); + $r3$.ɵcR(1); + $r3$.ɵr(2, 1); + $r3$.ɵcr(); + + function MyComponent_ForOfDirective_Template_1(ctx0: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵE(0, 'li'); + $r3$.ɵT(1); + $r3$.ɵe(); + } + const $item$ = ctx0.$implicit; + $r3$.ɵt(1, $r3$.ɵi1('', $item$.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: \` +
    +
  • +
    {{item.name}}
    +
      +
    • + {{item.name}}: {{info.description}} +
    • +
    +
  • +
\` + }) + export class MyComponent { + items = [ + {name: 'one', infos: [{description: '11'}, {description: '12'}]}, + {name: 'two', infos: [{description: '21'}, {description: '22'}]} + ]; + } + + @NgModule({ + declarations: [MyComponent, ForOfDirective] + }) + export class MyModule {} + ` + } + }; + + const MyComponentDefinition = ` + const $c1$ = [ForOfDirective]; + const $c2$ = ForOfDirective.ngDirectiveDef; + … + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyComponent, + tag: 'my-component', + factory: function MyComponent_Factory() { return new MyComponent(); }, + template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵE(0, 'ul'); + $r3$.ɵC(1, $c1$, MyComponent_ForOfDirective_Template_1); + $r3$.ɵe(); + } + $r3$.ɵp(1, 'forOf', $r3$.ɵb(ctx.items)); + $c2$.h(2,1); + $r3$.ɵcR(1); + $r3$.ɵr(2, 1); + $r3$.ɵcr(); + + function MyComponent_ForOfDirective_Template_1(ctx0: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵE(0, 'li'); + $r3$.ɵE(1, 'div'); + $r3$.ɵT(2); + $r3$.ɵe(); + $r3$.ɵE(3, 'ul'); + $r3$.ɵC(4, $c1$, MyComponent_ForOfDirective_ForOfDirective_Template_4); + $r3$.ɵe(); + $r3$.ɵe(); + } + const $item$ = ctx0.$implicit; + $r3$.ɵp(4, 'forOf', $r3$.ɵb(IDENT.infos)); + $c2$.h(5,4); + $r3$.ɵt(2, $r3$.ɵi1('', IDENT.name, '')); + $r3$.ɵcR(4); + $r3$.ɵr(5, 4); + $r3$.ɵcr(); + + function MyComponent_ForOfDirective_ForOfDirective_Template_4( + ctx1: IDENT, cm: IDENT) { + if (cm) { + $r3$.ɵE(0, 'li'); + $r3$.ɵT(1); + $r3$.ɵe(); + } + const $info$ = ctx1.$implicit; + $r3$.ɵt(1, $r3$.ɵi2(' ', $item$.name, ': ', $info$.description, ' ')); + } + } + } + });`; + + const result = compile(files, angularFiles); + const source = result.source; + expectEmit(source, MyComponentDefinition, 'Invalid component definition'); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/compiler/test/render3/r3_view_compiler_spec.ts b/packages/compiler/test/render3/r3_view_compiler_spec.ts index 619b0c58e3..658944ac17 100644 --- a/packages/compiler/test/render3/r3_view_compiler_spec.ts +++ b/packages/compiler/test/render3/r3_view_compiler_spec.ts @@ -122,626 +122,4 @@ describe('r3_view_compiler', () => { }); }); - /* These tests are codified version of the tests in compiler_canonical_spec.ts. Every - * test in compiler_canonical_spec.ts should have a corresponding test here. - */ - describe('compiler conformance', () => { - describe('elements', () => { - it('should translate DOM structure', () => { - const files = { - app: { - 'spec.ts': ` - import {Component, NgModule} from '@angular/core'; - - @Component({ - selector: 'my-component', - template: \`
Hello World!
\` - }) - export class MyComponent {} - - @NgModule({declarations: [MyComponent]}) - export class MyModule {} - ` - } - }; - - // The factory should look like this: - const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }'; - - // The template should look like this (where IDENT is a wild card for an identifier): - const template = ` - const $c1$ = ['class', 'my-app', 'title', 'Hello']; - … - template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { - if (cm) { - $r3$.ɵE(0, 'div', $c1$); - $r3$.ɵT(1, 'Hello '); - $r3$.ɵE(2, 'b'); - $r3$.ɵT(3, 'World'); - $r3$.ɵe(); - $r3$.ɵT(4, '!'); - $r3$.ɵe(); - } - } - `; - - - const result = compile(files, angularFiles); - - expectEmit(result.source, factory, 'Incorrect factory'); - expectEmit(result.source, template, 'Incorrect template'); - }); - }); - }); - - describe('components & directives', () => { - it('should instantiate directives', () => { - const files = { - app: { - 'spec.ts': ` - import {Component, Directive, NgModule} from '@angular/core'; - - @Component({selector: 'child', template: 'child-view'}) - export class ChildComponent {} - - @Directive({selector: '[some-directive]'}) - export class SomeDirective {} - - @Component({selector: 'my-component', template: '!'}) - export class MyComponent {} - - @NgModule({declarations: [ChildComponent, SomeDirective, MyComponent]}) - export class MyModule{} - ` - } - }; - - // ChildComponent definition should be: - const ChildComponentDefinition = ` - static ngComponentDef = $r3$.ɵdefineComponent({ - type: ChildComponent, - tag: 'child', - factory: function ChildComponent_Factory() { return new ChildComponent(); }, - template: function ChildComponent_Template(ctx: IDENT, cm: IDENT) { - if (cm) { - $r3$.ɵT(0, 'child-view'); - } - } - });`; - - // SomeDirective definition should be: - const SomeDirectiveDefinition = ` - static ngDirectiveDef = $r3$.ɵdefineDirective({ - type: SomeDirective, - factory: function SomeDirective_Factory() {return new SomeDirective(); } - }); - `; - - // MyComponent definition should be: - const MyComponentDefinition = ` - const $c1$ = ['some-directive', '']; - const $c2$ = [SomeDirective]; - … - static ngComponentDef = $r3$.ɵdefineComponent({ - type: MyComponent, - tag: 'my-component', - factory: function MyComponent_Factory() { return new MyComponent(); }, - template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { - if (cm) { - $r3$.ɵE(0, ChildComponent, IDENT, IDENT); - $r3$.ɵe(); - $r3$.ɵT(3, '!'); - } - ChildComponent.ngComponentDef.h(1, 0); - SomeDirective.ngDirectiveDef.h(2, 0); - $r3$.ɵr(1, 0); - $r3$.ɵr(2, 0); - } - }); - `; - - - const result = compile(files, angularFiles); - const source = result.source; - - expectEmit(source, ChildComponentDefinition, 'Incorrect ChildComponent.ngComponentDef'); - expectEmit(source, SomeDirectiveDefinition, 'Incorrect SomeDirective.ngDirectiveDef'); - expectEmit(source, MyComponentDefinition, 'Incorrect MyComponentDefinition.ngComponentDef'); - }); - - it('should support structural directives', () => { - const files = { - app: { - 'spec.ts': ` - import {Component, Directive, NgModule, TemplateRef} from '@angular/core'; - - @Directive({selector: '[if]'}) - export class IfDirective { - constructor(template: TemplateRef) { } - } - - @Component({ - selector: 'my-component', - template: '
  • {{salutation}} {{foo}}
' - }) - export class MyComponent { - salutation = 'Hello'; - } - - @NgModule({declarations: [IfDirective, MyComponent]}) - export class MyModule {} - ` - } - }; - - const IfDirectiveDefinition = ` - static ngDirectiveDef = $r3$.ɵdefineDirective({ - type: IfDirective, - factory: function IfDirective_Factory() { return new IfDirective($r3$.ɵinjectTemplateRef()); } - });`; - const MyComponentDefinition = ` - const $c1$ = ['foo', '']; - const $c2$ = [IfDirective]; - … - static ngComponentDef = $r3$.ɵdefineComponent({ - type: MyComponent, - tag: 'my-component', - factory: function MyComponent_Factory() { return new MyComponent(); }, - template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { - if (cm) { - $r3$.ɵE(0, 'ul', null, null, $c1$); - $r3$.ɵC(2, $c2$, MyComponent_IfDirective_Template_2); - $r3$.ɵe(); - } - const $foo$ = $r3$.ɵld(1); - IfDirective.ngDirectiveDef.h(3,2); - $r3$.ɵcR(2); - $r3$.ɵr(3,2); - $r3$.ɵcr(); - - function MyComponent_IfDirective_Template_2(ctx0: IDENT, cm: IDENT) { - if (cm) { - $r3$.ɵE(0, 'li'); - $r3$.ɵT(1); - $r3$.ɵe(); - } - $r3$.ɵt(1, $r3$.ɵi2('', ctx.salutation, ' ', $foo$, '')); - } - } - });`; - - const result = compile(files, angularFiles); - const source = result.source; - - expectEmit(source, IfDirectiveDefinition, 'Incorrect IfDirective.ngDirectiveDef'); - expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef'); - }); - - it('should support content projection', () => { - const files = { - app: { - 'spec.ts': ` - import {Component, Directive, NgModule, TemplateRef} from '@angular/core'; - - @Component({selector: 'simple', template: '
'}) - export class SimpleComponent {} - - @Component({ - selector: 'complex', - template: \` -
-
\` - }) - export class ComplexComponent { } - - @NgModule({declarations: [SimpleComponent, ComplexComponent]}) - export class MyModule {} - - @Component({ - selector: 'my-app', - template: 'content ' - }) - export class MyApp {} - ` - } - }; - - const SimpleComponentDefinition = ` - static ngComponentDef = $r3$.ɵdefineComponent({ - type: SimpleComponent, - tag: 'simple', - factory: function SimpleComponent_Factory() { return new SimpleComponent(); }, - template: function SimpleComponent_Template(ctx: IDENT, cm: IDENT) { - if (cm) { - $r3$.ɵpD(0); - $r3$.ɵE(1, 'div'); - $r3$.ɵP(2, 0); - $r3$.ɵe(); - } - } - });`; - - const ComplexComponentDefinition = ` - const $c1$ = [[[['span', 'title', 'tofirst'], null]], [[['span', 'title', 'tosecond'], null]]]; - const $c2$ = ['id','first']; - const $c3$ = ['id','second']; - … - static ngComponentDef = $r3$.ɵdefineComponent({ - type: ComplexComponent, - tag: 'complex', - factory: function ComplexComponent_Factory() { return new ComplexComponent(); }, - template: function ComplexComponent_Template(ctx: IDENT, cm: IDENT) { - if (cm) { - $r3$.ɵpD(0, $c1$); - $r3$.ɵE(1, 'div', $c2$); - $r3$.ɵP(2, 0, 1); - $r3$.ɵe(); - $r3$.ɵE(3, 'div', $c3$); - $r3$.ɵP(4, 0, 2); - $r3$.ɵe(); - } - } - }); - `; - - const result = compile(files, angularFiles); - const source = result.source; - - expectEmit(result.source, SimpleComponentDefinition, 'Incorrect SimpleComponent definition'); - expectEmit( - result.source, ComplexComponentDefinition, 'Incorrect ComplexComponent definition'); - }); - - it('local reference', () => { - const files = { - app: { - 'spec.ts': ` - import {Component, NgModule} from '@angular/core'; - - @Component({selector: 'my-component', template: 'Hello {{user.value}}!'}) - export class MyComponent {} - - @NgModule({declarations: [MyComponent]}) - export class MyModule {} - ` - } - }; - - const MyComponentDefinition = ` - const $c1$ = ['user', '']; - … - static ngComponentDef = $r3$.ɵdefineComponent({ - type: MyComponent, - tag: 'my-component', - factory: function MyComponent_Factory() { return new MyComponent(); }, - template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { - if (cm) { - $r3$.ɵE(0, 'input', null, null, $c1$); - $r3$.ɵe(); - $r3$.ɵT(2); - } - const $user$ = $r3$.ɵld(1); - $r3$.ɵt(2, $r3$.ɵi1('Hello ', $user$.value, '!')); - } - }); - `; - - const result = compile(files, angularFiles); - const source = result.source; - - expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef'); - }); - - describe('lifecycle hooks', () => { - const files = { - app: { - 'spec.ts': ` - import {Component, Input, NgModule} from '@angular/core'; - - let events: string[] = []; - - @Component({selector: 'lifecycle-comp', template: ''}) - export class LifecycleComp { - @Input('name') nameMin: string; - - ngOnChanges() { events.push('changes' + this.nameMin); } - - ngOnInit() { events.push('init' + this.nameMin); } - ngDoCheck() { events.push('check' + this.nameMin); } - - ngAfterContentInit() { events.push('content init' + this.nameMin); } - ngAfterContentChecked() { events.push('content check' + this.nameMin); } - - ngAfterViewInit() { events.push('view init' + this.nameMin); } - ngAfterViewChecked() { events.push('view check' + this.nameMin); } - - ngOnDestroy() { events.push(this.nameMin); } - } - - @Component({ - selector: 'simple-layout', - template: \` - - - \` - }) - export class SimpleLayout { - name1 = '1'; - name2 = '2'; - } - - @NgModule({declarations: [LifecycleComp, SimpleLayout]}) - export class LifecycleModule {} - ` - } - }; - - it('should gen hooks with a few simple components', () => { - const LifecycleCompDefinition = ` - static ngComponentDef = $r3$.ɵdefineComponent({ - type: LifecycleComp, - tag: 'lifecycle-comp', - factory: function LifecycleComp_Factory() { return new LifecycleComp(); }, - template: function LifecycleComp_Template(ctx: IDENT, cm: IDENT) {}, - inputs: {nameMin: 'name'}, - features: [$r3$.ɵNgOnChangesFeature(LifecycleComp)] - });`; - - const SimpleLayoutDefinition = ` - const $c1$ = LifecycleComp.ngComponentDef; - … - static ngComponentDef = $r3$.ɵdefineComponent({ - type: SimpleLayout, - tag: 'simple-layout', - factory: function SimpleLayout_Factory() { return new SimpleLayout(); }, - template: function SimpleLayout_Template(ctx: IDENT, cm: IDENT) { - if (cm) { - $r3$.ɵE(0, LifecycleComp); - $r3$.ɵe(); - $r3$.ɵE(2, LifecycleComp); - $r3$.ɵe(); - } - $r3$.ɵp(0, 'name', $r3$.ɵb(ctx.name1)); - $r3$.ɵp(2, 'name', $r3$.ɵb(ctx.name2)); - $c1$.h(1, 0); - $c1$.h(3, 2); - $r3$.ɵr(1, 0); - $r3$.ɵr(3, 2); - } - });`; - - const result = compile(files, angularFiles); - const source = result.source; - - expectEmit(source, LifecycleCompDefinition, 'Invalid LifecycleComp definition'); - expectEmit(source, SimpleLayoutDefinition, 'Invalid SimpleLayout 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: \`
  • {{item.name}}
\` - }) - 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 = $r3$.ɵdefineDirective({ - type: ForOfDirective, - factory: function ForOfDirective_Factory() { - return new ForOfDirective($r3$.ɵinjectViewContainerRef(), $r3$.ɵinjectTemplateRef()); - }, - features: [$r3$.ɵNgOnChangesFeature(NgForOf)], - inputs: {forOf: 'forOf'} - }); - `; - - const MyComponentDefinition = ` - const $c1$ = [ForOfDirective]; - … - static ngComponentDef = $r3$.ɵdefineComponent({ - type: MyComponent, - tag: 'my-component', - factory: function MyComponent_Factory() { return new MyComponent(); }, - template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { - if (cm) { - $r3$.ɵE(0, 'ul'); - $r3$.ɵC(1, $c1$, MyComponent_ForOfDirective_Template_1); - $r3$.ɵe(); - } - $r3$.ɵp(1, 'forOf', $r3$.ɵb(ctx.items)); - ForOfDirective.ngDirectiveDef.h(2, 1); - $r3$.ɵcR(1); - $r3$.ɵr(2, 1); - $r3$.ɵcr(); - - function MyComponent_ForOfDirective_Template_1(ctx0: IDENT, cm: IDENT) { - if (cm) { - $r3$.ɵE(0, 'li'); - $r3$.ɵT(1); - $r3$.ɵe(); - } - const $item$ = ctx0.$implicit; - $r3$.ɵt(1, $r3$.ɵi1('', $item$.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: \` -
    -
  • -
    {{item.name}}
    -
      -
    • - {{item.name}}: {{info.description}} -
    • -
    -
  • -
\` - }) - export class MyComponent { - items = [ - {name: 'one', infos: [{description: '11'}, {description: '12'}]}, - {name: 'two', infos: [{description: '21'}, {description: '22'}]} - ]; - } - - @NgModule({ - declarations: [MyComponent, ForOfDirective] - }) - export class MyModule {} - ` - } - }; - - const MyComponentDefinition = ` - const $c1$ = [ForOfDirective]; - const $c2$ = ForOfDirective.ngDirectiveDef; - … - static ngComponentDef = $r3$.ɵdefineComponent({ - type: MyComponent, - tag: 'my-component', - factory: function MyComponent_Factory() { return new MyComponent(); }, - template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { - if (cm) { - $r3$.ɵE(0, 'ul'); - $r3$.ɵC(1, $c1$, MyComponent_ForOfDirective_Template_1); - $r3$.ɵe(); - } - $r3$.ɵp(1, 'forOf', $r3$.ɵb(ctx.items)); - $c2$.h(2,1); - $r3$.ɵcR(1); - $r3$.ɵr(2, 1); - $r3$.ɵcr(); - - function MyComponent_ForOfDirective_Template_1(ctx0: IDENT, cm: IDENT) { - if (cm) { - $r3$.ɵE(0, 'li'); - $r3$.ɵE(1, 'div'); - $r3$.ɵT(2); - $r3$.ɵe(); - $r3$.ɵE(3, 'ul'); - $r3$.ɵC(4, $c1$, MyComponent_ForOfDirective_ForOfDirective_Template_4); - $r3$.ɵe(); - $r3$.ɵe(); - } - const $item$ = ctx0.$implicit; - $r3$.ɵp(4, 'forOf', $r3$.ɵb(IDENT.infos)); - $c2$.h(5,4); - $r3$.ɵt(2, $r3$.ɵi1('', IDENT.name, '')); - $r3$.ɵcR(4); - $r3$.ɵr(5, 4); - $r3$.ɵcr(); - - function MyComponent_ForOfDirective_ForOfDirective_Template_4( - ctx1: IDENT, cm: IDENT) { - if (cm) { - $r3$.ɵE(0, 'li'); - $r3$.ɵT(1); - $r3$.ɵe(); - } - const $info$ = ctx1.$implicit; - $r3$.ɵt(1, $r3$.ɵi2(' ', $item$.name, ': ', $info$.description, ' ')); - } - } - } - });`; - - const result = compile(files, angularFiles); - const source = result.source; - expectEmit(source, MyComponentDefinition, 'Invalid component definition'); - }); - }); - }); });