From 73c203fda9281a233048d172cd502abcc1e87d1e Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Thu, 15 Feb 2018 16:43:16 -0800 Subject: [PATCH] feat(ivy): support host attribute and property bindings (#22334) PR Close #22334 --- packages/compiler/src/aot/compiler.ts | 12 +- packages/compiler/src/compile_metadata.ts | 6 +- .../compiler/src/render3/r3_identifiers.ts | 2 + .../compiler/src/render3/r3_view_compiler.ts | 169 +++++++++++++----- .../src/template_parser/binding_parser.ts | 15 +- .../src/template_parser/template_parser.ts | 5 +- .../compiler/test/render3/mock_compile.ts | 18 +- .../render3/r3_compiler_compliance_spec.ts | 36 +++- 8 files changed, 201 insertions(+), 62 deletions(-) diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index ad4dd14e2c..6ebb98a1d3 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -15,7 +15,7 @@ import {Identifiers, createTokenForExternalReference} from '../identifiers'; import {InjectableCompiler} from '../injectable_compiler'; import {CompileMetadataResolver} from '../metadata_resolver'; import {HtmlParser} from '../ml_parser/html_parser'; -import {InterpolationConfig} from '../ml_parser/interpolation_config'; +import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config'; import {NgModuleCompiler} from '../ng_module_compiler'; import {OutputEmitter} from '../output/abstract_emitter'; import * as o from '../output/output_ast'; @@ -24,6 +24,7 @@ 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'; +import {BindingParser} from '../template_parser/binding_parser'; import {TemplateAst} from '../template_parser/template_ast'; import {TemplateParser} from '../template_parser/template_parser'; import {OutputContext, ValueVisitor, error, syntaxError, visitValue} from '../util'; @@ -345,8 +346,12 @@ export class AotCompiler { directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: CompileNgModuleMetadata[], injectables: CompileInjectableMetadata[]): PartialModule[] { const classes: o.ClassStmt[] = []; + const errors: ParseError[] = []; const context = this._createOutputContext(fileName); + const hostBindingParser = new BindingParser( + this._templateParser.expressionParser, DEFAULT_INTERPOLATION_CONFIG, null !, [], errors); + // Process all components and directives directives.forEach(directiveType => { @@ -360,9 +365,10 @@ export class AotCompiler { const {template: parsedTemplate, pipes: parsedPipes} = this._parseTemplate(directiveMetadata, module, module.transitiveModule.directives); compileIvyComponent( - context, directiveMetadata, parsedPipes, parsedTemplate, this._reflector); + context, directiveMetadata, parsedPipes, parsedTemplate, this._reflector, + hostBindingParser); } else { - compileIvyDirective(context, directiveMetadata, this._reflector); + compileIvyDirective(context, directiveMetadata, this._reflector, hostBindingParser); } }); diff --git a/packages/compiler/src/compile_metadata.ts b/packages/compiler/src/compile_metadata.ts index 816494c8c7..2a8edf12e6 100644 --- a/packages/compiler/src/compile_metadata.ts +++ b/packages/compiler/src/compile_metadata.ts @@ -18,7 +18,7 @@ import {splitAtColon, stringify} from './util'; // group 3: "@trigger" from "@trigger" const HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))|(\@[-\w]+)$/; -function _sanitizeIdentifier(name: string): string { +export function sanitizeIdentifier(name: string): string { return name.replace(/\W/g, '_'); } @@ -42,7 +42,7 @@ export function identifierName(compileIdentifier: CompileIdentifierMetadata | nu identifier = `anonymous_${_anonymousTypeIndex++}`; ref['__anonymousType'] = identifier; } else { - identifier = _sanitizeIdentifier(identifier); + identifier = sanitizeIdentifier(identifier); } return identifier; } @@ -120,7 +120,7 @@ export interface CompileFactoryMetadata extends CompileIdentifierMetadata { } export function tokenName(token: CompileTokenMetadata) { - return token.value != null ? _sanitizeIdentifier(token.value) : identifierName(token.identifier); + return token.value != null ? sanitizeIdentifier(token.value) : identifierName(token.identifier); } export function tokenReference(token: CompileTokenMetadata) { diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 6d239df39b..b25cf60046 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -114,4 +114,6 @@ export class Identifiers { static queryRefresh: o.ExternalReference = {name: 'ɵqR', moduleName: CORE}; static NgOnChangesFeature: o.ExternalReference = {name: 'ɵNgOnChangesFeature', moduleName: CORE}; + + static listener: o.ExternalReference = {name: 'ɵL', moduleName: CORE}; } \ 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 9be0521d2c..4a3da8db0a 100644 --- a/packages/compiler/src/render3/r3_view_compiler.ts +++ b/packages/compiler/src/render3/r3_view_compiler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileDirectiveMetadata, CompilePipeSummary, CompileQueryMetadata, CompileTokenMetadata, CompileTypeMetadata, flatten, identifierName, rendererTypeName, tokenReference, viewClassName} from '../compile_metadata'; +import {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, CompileQueryMetadata, CompileTokenMetadata, CompileTypeMetadata, flatten, identifierName, rendererTypeName, sanitizeIdentifier, tokenReference, viewClassName} from '../compile_metadata'; import {CompileReflector} from '../compile_reflector'; import {BindingForm, BuiltinConverter, BuiltinFunctionCall, ConvertPropertyBindingResult, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter'; import {ConstantPool, DefinitionKind} from '../constant_pool'; @@ -14,8 +14,9 @@ import {AST, AstMemoryEfficientTransformer, AstTransformer, BindingPipe, Functio import {Identifiers} from '../identifiers'; import {LifecycleHooks} from '../lifecycle_reflector'; import * as o from '../output/output_ast'; -import {ParseSourceSpan} from '../parse_util'; +import {ParseSourceSpan, typeSourceSpan} from '../parse_util'; import {CssSelector} from '../selector'; +import {BindingParser} from '../template_parser/binding_parser'; import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAst, QueryMatch, RecursiveTemplateAstVisitor, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast'; import {OutputContext, error} from '../util'; @@ -39,22 +40,30 @@ const REFERENCE_PREFIX = '_r'; const IMPLICIT_REFERENCE = '$implicit'; export function compileDirective( - outputCtx: OutputContext, directive: CompileDirectiveMetadata, reflector: CompileReflector) { + outputCtx: OutputContext, directive: CompileDirectiveMetadata, reflector: CompileReflector, + bindingParser: BindingParser) { const definitionMapValues: {key: string, quoted: boolean, value: o.Expression}[] = []; + const field = (key: string, value: o.Expression | null) => { + if (value) { + definitionMapValues.push({key, value, quoted: false}); + } + }; + // e.g. 'type: MyDirective` - definitionMapValues.push( - {key: 'type', value: outputCtx.importExpr(directive.type.reference), quoted: false}); + field('type', outputCtx.importExpr(directive.type.reference)); // e.g. `factory: () => new MyApp(injectElementRef())` - const templateFactory = createFactory(directive.type, outputCtx, reflector, directive.queries); - definitionMapValues.push({key: 'factory', value: templateFactory, quoted: false}); + field('factory', createFactory(directive.type, outputCtx, reflector, directive.queries)); + + // e.g. `hostBindings: (dirIndex, elIndex) => { ... } + field('hostBindings', createHostBindingsFunction(directive, outputCtx, bindingParser)); + + // e.g. `attributes: ['role', 'listbox']` + field('attributes', createHostAttributesArray(directive, outputCtx)); // e.g 'inputs: {a: 'a'}` - if (Object.getOwnPropertyNames(directive.inputs).length > 0) { - definitionMapValues.push( - {key: 'inputs', quoted: false, value: mapToExpression(directive.inputs)}); - } + field('inputs', createInputsObject(directive, outputCtx)); const className = identifierName(directive.type) !; className || error(`Cannot resolver the name of ${directive.type}`); @@ -76,19 +85,24 @@ export function compileDirective( export function compileComponent( outputCtx: OutputContext, component: CompileDirectiveMetadata, pipes: CompilePipeSummary[], - template: TemplateAst[], reflector: CompileReflector) { + template: TemplateAst[], reflector: CompileReflector, bindingParser: BindingParser) { const definitionMapValues: {key: string, quoted: boolean, value: o.Expression}[] = []; + const field = (key: string, value: o.Expression | null) => { + if (value) { + definitionMapValues.push({key, value, quoted: false}); + } + }; + // e.g. `type: MyApp` - definitionMapValues.push( - {key: 'type', value: outputCtx.importExpr(component.type.reference), quoted: false}); + field('type', outputCtx.importExpr(component.type.reference)); // e.g. `tag: 'my-app'` // This is optional and only included if the first selector of a component has element. const selector = component.selector && CssSelector.parse(component.selector); const firstSelector = selector && selector[0]; if (firstSelector && firstSelector.hasElementSelector()) { - definitionMapValues.push({key: 'tag', value: o.literal(firstSelector.element), quoted: false}); + field('tag', o.literal(firstSelector.element)); } // e.g. `attr: ["class", ".my.app"] @@ -96,26 +110,19 @@ export function compileComponent( if (firstSelector) { const selectorAttributes = firstSelector.getAttrs(); if (selectorAttributes.length) { - definitionMapValues.push({ - key: 'attrs', - value: outputCtx.constantPool.getConstLiteral( - o.literalArr(selectorAttributes.map( - value => value != null ? o.literal(value) : o.literal(undefined))), - /* forceShared */ true), - quoted: false - }); + field( + 'attrs', outputCtx.constantPool.getConstLiteral( + o.literalArr(selectorAttributes.map( + value => value != null ? o.literal(value) : o.literal(undefined))), + /* forceShared */ true)); } } // e.g. `factory: function MyApp_Factory() { return new MyApp(injectElementRef()); }` - const templateFactory = createFactory(component.type, outputCtx, reflector, component.queries); - definitionMapValues.push({key: 'factory', value: templateFactory, quoted: false}); + field('factory', createFactory(component.type, outputCtx, reflector, component.queries)); // e.g `hostBindings: function MyApp_HostBindings { ... } - const hostBindings = createHostBindingsFunction(component.type, outputCtx, component.queries); - if (hostBindings) { - definitionMapValues.push({key: 'hostBindings', value: hostBindings, quoted: false}); - } + field('hostBindings', createHostBindingsFunction(component, outputCtx, bindingParser)); // e.g. `template: function MyComponent_Template(_ctx, _cm) {...}` const templateTypeName = component.type.reference.name; @@ -127,13 +134,11 @@ export function compileComponent( component.template !.ngContentSelectors, templateTypeName, templateName, pipeMap, component.viewQueries) .buildTemplateFunction(template, []); - definitionMapValues.push({key: 'template', value: templateFunctionExpression, quoted: false}); + + field('template', templateFunctionExpression); // e.g `inputs: {a: 'a'}` - if (Object.getOwnPropertyNames(component.inputs).length > 0) { - definitionMapValues.push( - {key: 'inputs', quoted: false, value: mapToExpression(component.inputs)}); - } + field('inputs', createInputsObject(component, outputCtx)); // e.g. `features: [NgOnChangesFeature(MyComponent)]` const features: o.Expression[] = []; @@ -142,7 +147,7 @@ export function compileComponent( component.type.reference)])); } if (features.length) { - definitionMapValues.push({key: 'features', quoted: false, value: o.literalArr(features)}); + field('features', o.literalArr(features)); } const className = identifierName(component.type) !; @@ -163,7 +168,6 @@ export function compileComponent( /* methods */[])); } - // TODO: Remove these when the things are fully supported function unknown(arg: o.Expression | o.Statement | TemplateAst): never { throw new Error( @@ -780,10 +784,28 @@ export function createFactory( type.reference.name ? `${type.reference.name}_Factory` : null); } +type HostBindings = { + [key: string]: string +}; + +function createHostAttributesArray( + directiveMetadata: CompileDirectiveMetadata, outputCtx: OutputContext): o.Expression|null { + const values: o.Expression[] = []; + const attributes = directiveMetadata.hostAttributes; + for (let key of Object.getOwnPropertyNames(attributes)) { + const value = attributes[key]; + values.push(o.literal(key), o.literal(value)); + } + if (values.length > 0) { + return outputCtx.constantPool.getConstLiteral(o.literalArr(values)); + } + return null; +} + // Return a host binding function or null if one is not necessary. -export function createHostBindingsFunction( - type: CompileTypeMetadata, outputCtx: OutputContext, - queries: CompileQueryMetadata[]): o.Expression|null { +function createHostBindingsFunction( + directiveMetadata: CompileDirectiveMetadata, outputCtx: OutputContext, + bindingParser: BindingParser): o.Expression|null { const statements: o.Statement[] = []; const temporary = function() { @@ -797,8 +819,12 @@ export function createHostBindingsFunction( }; }(); - for (let index = 0; index < queries.length; index++) { - const query = queries[index]; + const hostBindingSourceSpan = typeSourceSpan( + directiveMetadata.isComponent ? 'Component' : 'Directive', directiveMetadata.type); + + // Calculate the queries + for (let index = 0; index < directiveMetadata.queries.length; index++) { + const query = directiveMetadata.queries[index]; // e.g. r3.qR(tmp = r3.ld(dirIndex)[1]) && (r3.ld(dirIndex)[0].someDir = tmp); const getDirectiveMemory = o.importExpr(R3.load).callFn([o.variable('dirIndex')]); @@ -808,18 +834,67 @@ export function createHostBindingsFunction( const callQueryRefresh = o.importExpr(R3.queryRefresh).callFn([assignToTemporary]); const updateDirective = getDirectiveMemory.key(o.literal(0, o.INFERRED_TYPE)) .prop(query.propertyName) - .set(query.first ? temporary().key(o.literal(0)) : temporary()); + .set(query.first ? temporary().prop('first') : temporary()); const andExpression = callQueryRefresh.and(updateDirective); statements.push(andExpression.toStmt()); } - if (statements.length > 0) { - return o.fn( - [new o.FnParam('dirIndex', o.NUMBER_TYPE), new o.FnParam('elIndex', o.NUMBER_TYPE)], - statements, o.INFERRED_TYPE, null, - type.reference.name ? `${type.reference.name}_HostBindings` : null); + const directiveSummary = directiveMetadata.toSummary(); + + // Calculate the host property bindings + const bindings = bindingParser.createBoundHostProperties(directiveSummary, hostBindingSourceSpan); + const bindingContext = o.importExpr(R3.load).callFn([o.variable('dirIndex')]); + if (bindings) { + for (const binding of bindings) { + const bindingExpr = convertPropertyBinding( + null, bindingContext, binding.expression, 'b', BindingForm.TrySimple, + () => error('Unexpected interpolation')); + statements.push(...bindingExpr.stmts); + statements.push(o.importExpr(R3.elementProperty) + .callFn([ + o.variable('elIndex'), o.literal(binding.name), + o.importExpr(R3.bind).callFn([bindingExpr.currValExpr]) + ]) + .toStmt()); + } } + // Calculate host event bindings + const eventBindings = + bindingParser.createDirectiveHostEventAsts(directiveSummary, hostBindingSourceSpan); + if (eventBindings) { + for (const binding of eventBindings) { + const bindingExpr = convertActionBinding( + null, bindingContext, binding.handler, 'b', () => error('Unexpected interpolation')); + const bindingName = binding.name && sanitizeIdentifier(binding.name); + const typeName = identifierName(directiveMetadata.type); + const functionName = + typeName && bindingName ? `${typeName}_${bindingName}_HostBindingHandler` : null; + const handler = o.fn( + [new o.FnParam('event', o.DYNAMIC_TYPE)], + [...bindingExpr.stmts, new o.ReturnStatement(bindingExpr.allowDefault)], o.INFERRED_TYPE, + null, functionName); + statements.push( + o.importExpr(R3.listener).callFn([o.literal(binding.name), handler]).toStmt()); + } + } + + + if (statements.length > 0) { + const typeName = directiveMetadata.type.reference.name; + return o.fn( + [new o.FnParam('dirIndex', o.NUMBER_TYPE), new o.FnParam('elIndex', o.NUMBER_TYPE)], + statements, o.INFERRED_TYPE, null, typeName ? `${typeName}_HostBindings` : null); + } + + return null; +} + +function createInputsObject( + directive: CompileDirectiveMetadata, outputCtx: OutputContext): o.Expression|null { + if (Object.getOwnPropertyNames(directive.inputs).length > 0) { + return outputCtx.constantPool.getConstLiteral(mapToExpression(directive.inputs)); + } return null; } diff --git a/packages/compiler/src/template_parser/binding_parser.ts b/packages/compiler/src/template_parser/binding_parser.ts index 6a7ae7647d..3c30006de8 100644 --- a/packages/compiler/src/template_parser/binding_parser.ts +++ b/packages/compiler/src/template_parser/binding_parser.ts @@ -63,9 +63,8 @@ export class BindingParser { getUsedPipes(): CompilePipeSummary[] { return Array.from(this._usedPipes.values()); } - createDirectiveHostPropertyAsts( - dirMeta: CompileDirectiveSummary, elementSelector: string, - sourceSpan: ParseSourceSpan): BoundElementPropertyAst[]|null { + createBoundHostProperties(dirMeta: CompileDirectiveSummary, sourceSpan: ParseSourceSpan): + BoundProperty[]|null { if (dirMeta.hostProperties) { const boundProps: BoundProperty[] = []; Object.keys(dirMeta.hostProperties).forEach(propName => { @@ -78,11 +77,19 @@ export class BindingParser { sourceSpan); } }); - return boundProps.map((prop) => this.createElementPropertyAst(elementSelector, prop)); + return boundProps; } return null; } + createDirectiveHostPropertyAsts( + dirMeta: CompileDirectiveSummary, elementSelector: string, + sourceSpan: ParseSourceSpan): BoundElementPropertyAst[]|null { + const boundProps = this.createBoundHostProperties(dirMeta, sourceSpan); + return boundProps && + boundProps.map((prop) => this.createElementPropertyAst(elementSelector, prop)); + } + createDirectiveHostEventAsts(dirMeta: CompileDirectiveSummary, sourceSpan: ParseSourceSpan): BoundEventAst[]|null { if (dirMeta.hostListeners) { diff --git a/packages/compiler/src/template_parser/template_parser.ts b/packages/compiler/src/template_parser/template_parser.ts index 9827f0dd22..1b321f56d5 100644 --- a/packages/compiler/src/template_parser/template_parser.ts +++ b/packages/compiler/src/template_parser/template_parser.ts @@ -100,6 +100,8 @@ export class TemplateParser { private _htmlParser: I18NHtmlParser, private _console: Console, public transforms: TemplateAstVisitor[]) {} + public get expressionParser() { return this._exprParser; } + parse( component: CompileDirectiveMetadata, template: string|ParseTreeResult, directives: CompileDirectiveSummary[], pipes: CompilePipeSummary[], schemas: SchemaMetadata[], @@ -434,6 +436,7 @@ class TemplateParseVisitor implements html.Visitor { const bindParts = name.match(BIND_NAME_REGEXP); let hasBinding = false; + const boundEvents: BoundEventAst[] = []; if (bindParts !== null) { hasBinding = true; @@ -814,7 +817,7 @@ class ElementOrDirectiveRef { } } -/** Splits a raw, potentially comma-delimted `exportAs` value into an array of names. */ +/** Splits a raw, potentially comma-delimited `exportAs` value into an array of names. */ function splitExportAs(exportAs: string | null): string[] { return exportAs ? exportAs.split(',').map(e => e.trim()) : []; } diff --git a/packages/compiler/test/render3/mock_compile.ts b/packages/compiler/test/render3/mock_compile.ts index 638d26962f..bfbdf796ed 100644 --- a/packages/compiler/test/render3/mock_compile.ts +++ b/packages/compiler/test/render3/mock_compile.ts @@ -6,14 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -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 {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileDirectiveMetadata, CompileMetadataResolver, CompilePipeSummary, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, ParseError, 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 {ParserError} from '../../src/expression_parser/ast'; 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 {BindingParser} from '../../src/template_parser/binding_parser'; import {OutputContext} from '../../src/util'; import {MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, arrayToMockDir, expectNoDiagnostics, settings, setup, toMockFileArray} from '../aot/test_util'; @@ -194,6 +196,11 @@ export function compile( constantPool: new ConstantPool() }; + const errors: ParseError[] = []; + + const hostBindingParser = new BindingParser( + expressionParser, DEFAULT_INTERPOLATION_CONFIG, elementSchemaRegistry, [], errors); + // Load all directives and pipes for (const pipeOrDirective of pipesOrDirectives) { const module = analyzedModules.ngModuleByPipeOrDirective.get(pipeOrDirective) !; @@ -219,9 +226,10 @@ export function compile( const parsedTemplate = templateParser.parse( metadata, htmlAst, directives, pipes, module.schemas, fakeUrl, false); compileComponent( - fakeOutputContext, metadata, pipes, parsedTemplate.template, staticReflector); + fakeOutputContext, metadata, pipes, parsedTemplate.template, staticReflector, + hostBindingParser); } else { - compileDirective(fakeOutputContext, metadata, staticReflector); + compileDirective(fakeOutputContext, metadata, staticReflector, hostBindingParser); } } else if (resolver.isPipe(pipeOrDirective)) { const metadata = resolver.getPipeMetadata(pipeOrDirective); @@ -243,5 +251,9 @@ export function compile( /* referenceFilter */ undefined, /* importFilter */ e => e.moduleName != null && e.moduleName.startsWith('/app')); + if (errors.length) { + throw new Error('Unexpected errors:' + errors.map(e => e.toString()).join(', ')); + } + return {source: result.sourceText, outputContext: fakeOutputContext}; } diff --git a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts index c675688a2f..81c755b570 100644 --- a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts +++ b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts @@ -141,6 +141,40 @@ describe('compiler compliance', () => { expectEmit(source, MyComponentDefinition, 'Incorrect MyComponentDefinition.ngComponentDef'); }); + it('should support host bindings', () => { + const files = { + app: { + 'spec.ts': ` + import {Directive, HostBinding, NgModule} from '@angular/core'; + + @Directive({selector: '[hostBindingDir]'}) + export class HostBindingDir { + @HostBinding('id') dirId = 'some id'; + } + + @NgModule({declarations: [HostBindingDir]}) + export class MyModule {} + ` + } + }; + + const HostBindingDirDeclaration = ` + static ngDirectiveDef = $r3$.ɵdefineDirective({ + type: HostBindingDir, + factory: function HostBindingDir_Factory() { return new HostBindingDir(); }, + hostBindings: function HostBindingDir_HostBindings( + dirIndex: $number$, elIndex: $number$) { + $r3$.ɵp(elIndex, 'id', $r3$.ɵb($r3$.ɵld(dirIndex).dirId)); + } + }); + `; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, HostBindingDirDeclaration, 'Invalid host binding code'); + }); + it('should support structural directives', () => { const files = { app: { @@ -658,7 +692,7 @@ describe('compiler compliance', () => { hostBindings: function ContentQueryComponent_HostBindings( dirIndex: $number$, elIndex: $number$) { var $tmp$: $any$; - ($r3$.ɵqR(($tmp$ = $r3$.ɵld(dirIndex)[1])) && ($r3$.ɵld(dirIndex)[0].someDir = $tmp$[0])); + ($r3$.ɵqR(($tmp$ = $r3$.ɵld(dirIndex)[1])) && ($r3$.ɵld(dirIndex)[0].someDir = $tmp$.first)); }, template: function ContentQueryComponent_Template( ctx: $ContentQueryComponent$, cm: $boolean$) {