diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/base_def.ts b/packages/compiler-cli/src/ngtsc/annotations/src/base_def.ts index a93031661f..ce23c170a8 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/base_def.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/base_def.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {ConstantPool, R3BaseRefMetaData, compileBaseDefFromMetadata} from '@angular/compiler'; +import {ConstantPool, R3BaseRefMetaData, compileBaseDefFromMetadata, makeBindingParser} from '@angular/compiler'; import {PartialEvaluator} from '../../partial_evaluator'; import {ClassDeclaration, ClassMember, Decorator, ReflectionHost} from '../../reflection'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; -import {queriesFromFields} from './directive'; +import {extractHostBindings, queriesFromFields} from './directive'; import {isAngularDecorator} from './util'; function containsNgTopLevelDecorator(decorators: Decorator[] | null, isCore: boolean): boolean { @@ -69,6 +69,12 @@ export class BaseDefDecoratorHandler implements result = result || {}; const queries = result.queries = result.queries || []; queries.push({member: property, decorators}); + } else if ( + isAngularDecorator(decorator, 'HostBinding', this.isCore) || + isAngularDecorator(decorator, 'HostListener', this.isCore)) { + result = result || {}; + const host = result.host = result.host || []; + host.push(property); } } }); @@ -85,7 +91,8 @@ export class BaseDefDecoratorHandler implements analyze(node: ClassDeclaration, metadata: R3BaseRefDecoratorDetection): AnalysisOutput { - const analysis: R3BaseRefMetaData = {}; + const analysis: R3BaseRefMetaData = {name: node.name.text, typeSourceSpan: null !}; + if (metadata.inputs) { const inputs = analysis.inputs = {} as{[key: string]: string | [string, string]}; metadata.inputs.forEach(({decorator, property}) => { @@ -133,12 +140,17 @@ export class BaseDefDecoratorHandler implements analysis.queries = queriesFromFields(metadata.queries, this.reflector, this.evaluator); } + if (metadata.host) { + analysis.host = extractHostBindings( + metadata.host, this.evaluator, this.isCore ? undefined : '@angular/core'); + } + return {analysis}; } compile(node: ClassDeclaration, analysis: R3BaseRefMetaData, pool: ConstantPool): CompileResult[]|CompileResult { - const {expression, type} = compileBaseDefFromMetadata(analysis, pool); + const {expression, type} = compileBaseDefFromMetadata(analysis, pool, makeBindingParser()); return { name: 'ngBaseDef', @@ -149,8 +161,9 @@ export class BaseDefDecoratorHandler implements } export interface R3BaseRefDecoratorDetection { - inputs?: Array<{property: ClassMember, decorator: Decorator}>; - outputs?: Array<{property: ClassMember, decorator: Decorator}>; + inputs?: {property: ClassMember, decorator: Decorator}[]; + outputs?: {property: ClassMember, decorator: Decorator}[]; viewQueries?: {member: ClassMember, decorators: Decorator[]}[]; queries?: {member: ClassMember, decorators: Decorator[]}[]; + host?: ClassMember[]; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 4a937b2eab..9fb745c1cd 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -194,7 +194,7 @@ export function extractDirectiveMetadata( throw new Error(`Directive ${clazz.name.text} has no selector, please add it!`); } - const host = extractHostBindings(directive, decoratedElements, evaluator, coreModule); + const host = extractHostBindings(decoratedElements, evaluator, coreModule, directive); const providers: Expression|null = directive.has('providers') ? new WrappedNodeExpr(directive.get('providers') !) : null; @@ -460,11 +460,11 @@ type StringMap = { [key: string]: T; }; -function extractHostBindings( - metadata: Map, members: ClassMember[], evaluator: PartialEvaluator, - coreModule: string | undefined): ParsedHostBindings { +export function extractHostBindings( + members: ClassMember[], evaluator: PartialEvaluator, coreModule: string | undefined, + metadata?: Map): ParsedHostBindings { let hostMetadata: StringMap = {}; - if (metadata.has('host')) { + if (metadata && metadata.has('host')) { const expr = metadata.get('host') !; const hostMetaMap = evaluator.evaluate(expr); if (!(hostMetaMap instanceof Map)) { @@ -501,7 +501,7 @@ function extractHostBindings( throw new FatalDiagnosticError( // TODO: provide more granular diagnostic and output specific host expression that triggered // an error instead of the whole host object - ErrorCode.HOST_BINDING_PARSE_ERROR, metadata.get('host') !, + ErrorCode.HOST_BINDING_PARSE_ERROR, metadata !.get('host') !, errors.map((error: ParseError) => error.msg).join('\n')); } diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index 705ab0e5a3..e6eaa960ed 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -3220,6 +3220,89 @@ describe('compiler compliance', () => { expectEmit(result.source, expectedOutput, 'Invalid base definition'); }); + it('should add ngBaseDef if a host binding is present', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule, HostBinding} from '@angular/core'; + export class BaseClass { + @HostBinding('attr.tabindex') + tabindex = -1; + } + + @Component({ + selector: 'my-component', + template: '' + }) + export class MyComponent extends BaseClass { + } + + @NgModule({ + declarations: [MyComponent] + }) + export class MyModule {} + ` + } + }; + const expectedOutput = ` + // ... + BaseClass.ngBaseDef = $r3$.ɵɵdefineBase({ + hostBindings: function (rf, ctx, elIndex) { + if (rf & 1) { + $r3$.ɵɵallocHostVars(1); + } + if (rf & 2) { + $r3$.ɵɵelementAttribute(elIndex, "tabindex", $r3$.ɵɵbind(ctx.tabindex)); + } + } + }); + // ... + `; + const result = compile(files, angularFiles); + expectEmit(result.source, expectedOutput, 'Invalid base definition'); + }); + + it('should add ngBaseDef if a host listener is present', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule, HostListener} from '@angular/core'; + export class BaseClass { + @HostListener('mousedown', ['$event']) + handleMousedown(event: any) {} + } + + @Component({ + selector: 'my-component', + template: '' + }) + export class MyComponent extends BaseClass { + } + + @NgModule({ + declarations: [MyComponent] + }) + export class MyModule {} + ` + } + }; + const expectedOutput = ` + // ... + BaseClass.ngBaseDef = $r3$.ɵɵdefineBase({ + hostBindings: function (rf, ctx, elIndex) { + if (rf & 1) { + $r3$.ɵɵlistener("mousedown", function ($event) { + return ctx.handleMousedown($event); + }); + } + } + }); + // ... + `; + const result = compile(files, angularFiles); + expectEmit(result.source, expectedOutput, 'Invalid base definition'); + }); + it('should NOT add ngBaseDef if @Component is present', () => { const files = { app: { diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 68d0a983b0..f445dc845e 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -1723,7 +1723,7 @@ describe('ngtsc behavioral tests', () => { .toContain('Cannot have a pipe in an action expression'); }); - it('should throw in case pipes are used in host listeners', () => { + it('should throw in case pipes are used in host bindings', () => { env.tsconfig(); env.write(`test.ts`, ` import {Component} from '@angular/core'; diff --git a/packages/compiler/src/compiler_facade_interface.ts b/packages/compiler/src/compiler_facade_interface.ts index 97e8c07d54..7214522ddd 100644 --- a/packages/compiler/src/compiler_facade_interface.ts +++ b/packages/compiler/src/compiler_facade_interface.ts @@ -149,6 +149,8 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade { } export interface R3BaseMetadataFacade { + name: string; + propMetadata: {[key: string]: any[]}; inputs?: {[key: string]: string | [string, string]}; outputs?: {[key: string]: string}; queries?: R3QueryMetadataFacade[]; diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index b2d9396a24..4e135ad90a 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -154,13 +154,17 @@ export class CompilerFacadeImpl implements CompilerFacade { compileBase(angularCoreEnv: CoreEnvironment, sourceMapUrl: string, facade: R3BaseMetadataFacade): any { const constantPool = new ConstantPool(); + const typeSourceSpan = + this.createParseSourceSpan('Base', facade.name, `ng:///${facade.name}.js`); const meta = { ...facade, + typeSourceSpan, viewQueries: facade.viewQueries ? facade.viewQueries.map(convertToR3QueryMetadata) : facade.viewQueries, - queries: facade.queries ? facade.queries.map(convertToR3QueryMetadata) : facade.queries + queries: facade.queries ? facade.queries.map(convertToR3QueryMetadata) : facade.queries, + host: extractHostBindings(facade.propMetadata, typeSourceSpan) }; - const res = compileBaseDefFromMetadata(meta, constantPool); + const res = compileBaseDefFromMetadata(meta, constantPool, makeBindingParser()); return this.jitExpression( res.expression, angularCoreEnv, sourceMapUrl, constantPool.statements); } @@ -244,7 +248,7 @@ function convertDirectiveFacadeToMetadata(facade: R3DirectiveMetadataFacade): R3 typeSourceSpan: facade.typeSourceSpan, type: new WrappedNodeExpr(facade.type), deps: convertR3DependencyMetadataArray(facade.deps), - host: extractHostBindings(facade.host, facade.propMetadata, facade.typeSourceSpan), + host: extractHostBindings(facade.propMetadata, facade.typeSourceSpan, facade.host), inputs: {...inputsFromMetadata, ...inputsFromType}, outputs: {...outputsFromMetadata, ...outputsFromType}, queries: facade.queries.map(convertToR3QueryMetadata), @@ -298,8 +302,8 @@ function convertR3DependencyMetadataArray(facades: R3DependencyMetadataFacade[] } function extractHostBindings( - host: {[key: string]: string}, propMetadata: {[key: string]: any[]}, - sourceSpan: ParseSourceSpan): ParsedHostBindings { + propMetadata: {[key: string]: any[]}, sourceSpan: ParseSourceSpan, + host?: {[key: string]: string}): ParsedHostBindings { // First parse the declarations from the metadata. const bindings = parseHostBindings(host || {}); diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index c6deb2b288..dd3f89fab7 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -62,24 +62,7 @@ export interface R3DirectiveMetadata { * Mappings indicating how the directive interacts with its host element (host bindings, * listeners, etc). */ - host: { - /** - * A mapping of attribute binding keys to `o.Expression`s. - */ - attributes: {[key: string]: o.Expression}; - - /** - * A mapping of event binding keys to unparsed expressions. - */ - listeners: {[key: string]: string}; - - /** - * A mapping of property binding keys to unparsed expressions. - */ - properties: {[key: string]: string}; - - specialAttributes: {styleAttr?: string; classAttr?: string;} - }; + host: R3HostMetadata; /** * Information about usage of specific lifecycle events which require special treatment in the @@ -265,3 +248,26 @@ export interface R3ComponentDef { type: o.Type; statements: o.Statement[]; } + +/** + * Mappings indicating how the class interacts with its + * host element (host bindings, listeners, etc). + */ +export interface R3HostMetadata { + /** + * A mapping of attribute binding keys to `o.Expression`s. + */ + attributes: {[key: string]: o.Expression}; + + /** + * A mapping of event binding keys to unparsed expressions. + */ + listeners: {[key: string]: string}; + + /** + * A mapping of property binding keys to unparsed expressions. + */ + properties: {[key: string]: string}; + + specialAttributes: {styleAttr?: string; classAttr?: string;}; +} diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index a26424f0ed..e844062ca9 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -28,7 +28,7 @@ import {Identifiers as R3} from '../r3_identifiers'; import {Render3ParseResult} from '../r3_template_transform'; import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, typeWithParameters} from '../util'; -import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api'; +import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from './api'; import {Instruction, StylingBuilder} from './styling_builder'; import {BindingScope, TemplateDefinitionBuilder, ValueConverter, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn} from './template'; import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util'; @@ -75,32 +75,11 @@ function baseDirectiveFields( 'viewQuery', createViewQueriesFunction(meta.viewQueries, constantPool, meta.name)); } - // Initialize hostVarsCount to number of bound host properties (interpolations illegal), - // except 'style' and 'class' properties, since they should *not* allocate host var slots - const hostVarsCount = Object.keys(meta.host.properties) - .filter(name => { - const prefix = getStylingPrefix(name); - return prefix !== 'style' && prefix !== 'class'; - }) - .length; - - const elVarExp = o.variable('elIndex'); - const contextVarExp = o.variable(CONTEXT_NAME); - const styleBuilder = new StylingBuilder(elVarExp, contextVarExp); - - const {styleAttr, classAttr} = meta.host.specialAttributes; - if (styleAttr !== undefined) { - styleBuilder.registerStyleAttr(styleAttr); - } - if (classAttr !== undefined) { - styleBuilder.registerClassAttr(classAttr); - } - // e.g. `hostBindings: (rf, ctx, elIndex) => { ... } definitionMap.set( 'hostBindings', createHostBindingsFunction( - meta, elVarExp, contextVarExp, meta.host.attributes, styleBuilder, - bindingParser, constantPool, hostVarsCount)); + meta.host, meta.typeSourceSpan, bindingParser, constantPool, + meta.selector || '', meta.name)); // e.g 'inputs: {a: 'a'}` definitionMap.set('inputs', conditionallyCreateMapObjectLiteral(meta.inputs, true)); @@ -163,17 +142,21 @@ export function compileDirectiveFromMetadata( } export interface R3BaseRefMetaData { + name: string; + typeSourceSpan: ParseSourceSpan; inputs?: {[key: string]: string | [string, string]}; outputs?: {[key: string]: string}; viewQueries?: R3QueryMetadata[]; queries?: R3QueryMetadata[]; + host?: R3HostMetadata; } /** * Compile a base definition for the render3 runtime as defined by {@link R3BaseRefMetadata} * @param meta the metadata used for compilation. */ -export function compileBaseDefFromMetadata(meta: R3BaseRefMetaData, constantPool: ConstantPool) { +export function compileBaseDefFromMetadata( + meta: R3BaseRefMetaData, constantPool: ConstantPool, bindingParser: BindingParser) { const definitionMap = new DefinitionMap(); if (meta.inputs) { const inputs = meta.inputs; @@ -198,6 +181,12 @@ export function compileBaseDefFromMetadata(meta: R3BaseRefMetaData, constantPool if (meta.queries && meta.queries.length > 0) { definitionMap.set('contentQueries', createContentQueriesFunction(meta.queries, constantPool)); } + if (meta.host) { + definitionMap.set( + 'hostBindings', + createHostBindingsFunction( + meta.host, meta.typeSourceSpan, bindingParser, constantPool, meta.name)); + } const expression = o.importExpr(R3.defineBase).callFn([definitionMap.toLiteralMap()]); const type = new o.ExpressionType(o.importExpr(R3.BaseDef)); @@ -593,16 +582,35 @@ function createViewQueriesFunction( // Return a host binding function or null if one is not necessary. function createHostBindingsFunction( - meta: R3DirectiveMetadata, elVarExp: o.ReadVarExpr, bindingContext: o.ReadVarExpr, - staticAttributesAndValues: {[name: string]: o.Expression}, styleBuilder: StylingBuilder, - bindingParser: BindingParser, constantPool: ConstantPool, hostVarsCount: number): o.Expression| - null { + hostBindingsMetadata: R3HostMetadata, typeSourceSpan: ParseSourceSpan, + bindingParser: BindingParser, constantPool: ConstantPool, selector: string, + name?: string): o.Expression|null { + // Initialize hostVarsCount to number of bound host properties (interpolations illegal), + // except 'style' and 'class' properties, since they should *not* allocate host var slots + const hostVarsCount = Object.keys(hostBindingsMetadata.properties) + .filter(name => { + const prefix = getStylingPrefix(name); + return prefix !== 'style' && prefix !== 'class'; + }) + .length; + const elVarExp = o.variable('elIndex'); + const bindingContext = o.variable(CONTEXT_NAME); + const styleBuilder = new StylingBuilder(elVarExp, bindingContext); + + const {styleAttr, classAttr} = hostBindingsMetadata.specialAttributes; + if (styleAttr !== undefined) { + styleBuilder.registerStyleAttr(styleAttr); + } + if (classAttr !== undefined) { + styleBuilder.registerClassAttr(classAttr); + } + const createStatements: o.Statement[] = []; const updateStatements: o.Statement[] = []; let totalHostVarsCount = hostVarsCount; - const hostBindingSourceSpan = meta.typeSourceSpan; - const directiveSummary = metadataAsSummary(meta); + const hostBindingSourceSpan = typeSourceSpan; + const directiveSummary = metadataAsSummary(hostBindingsMetadata); let valueConverter: ValueConverter; const getValueConverter = () => { @@ -625,7 +633,7 @@ function createHostBindingsFunction( const eventBindings = bindingParser.createDirectiveHostEventAsts(directiveSummary, hostBindingSourceSpan); if (eventBindings && eventBindings.length) { - const listeners = createHostListeners(bindingContext, eventBindings, meta); + const listeners = createHostListeners(bindingContext, eventBindings, name); createStatements.push(...listeners); } @@ -643,7 +651,7 @@ function createHostBindingsFunction( const {bindingName, instruction, isAttribute} = getBindingNameAndInstruction(binding); const securityContexts = - bindingParser.calcPossibleSecurityContexts(meta.selector || '', bindingName, isAttribute) + bindingParser.calcPossibleSecurityContexts(selector, bindingName, isAttribute) .filter(context => context !== core.SecurityContext.NONE); let sanitizerFn: o.ExternalExpr|null = null; @@ -696,7 +704,7 @@ function createHostBindingsFunction( // that is inside of a host binding within a directive/component) to be attached // to the host element alongside any of the provided host attributes that were // collected earlier. - const hostAttrs = convertAttributesToExpressions(staticAttributesAndValues); + const hostAttrs = convertAttributesToExpressions(hostBindingsMetadata.attributes); const hostInstruction = styleBuilder.buildHostAttrsInstruction(null, hostAttrs, constantPool); if (hostInstruction) { createStatements.push(createStylingStmt(hostInstruction, bindingContext, bindingFn)); @@ -729,7 +737,7 @@ function createHostBindingsFunction( } if (createStatements.length > 0 || updateStatements.length > 0) { - const hostBindingsFnName = meta.name ? `${meta.name}_HostBindings` : null; + const hostBindingsFnName = name ? `${name}_HostBindings` : null; const statements: o.Statement[] = []; if (createStatements.length > 0) { statements.push(renderFlagCheckIfStmt(core.RenderFlags.Create, createStatements)); @@ -787,15 +795,13 @@ function getBindingNameAndInstruction(binding: ParsedProperty): } function createHostListeners( - bindingContext: o.Expression, eventBindings: ParsedEvent[], - meta: R3DirectiveMetadata): o.Statement[] { + bindingContext: o.Expression, eventBindings: ParsedEvent[], name?: string): o.Statement[] { return eventBindings.map(binding => { let bindingName = binding.name && sanitizeIdentifier(binding.name); const bindingFnName = binding.type === ParsedEventType.Animation ? prepareSyntheticListenerFunctionName(bindingName, binding.targetOrPhase) : bindingName; - const handlerName = - meta.name && bindingName ? `${meta.name}_${bindingFnName}_HostBindingHandler` : null; + const handlerName = name && bindingName ? `${name}_${bindingFnName}_HostBindingHandler` : null; const params = prepareEventListenerParameters( BoundEvent.fromParsedEvent(binding), bindingContext, handlerName); const instruction = @@ -804,14 +810,14 @@ function createHostListeners( }); } -function metadataAsSummary(meta: R3DirectiveMetadata): CompileDirectiveSummary { +function metadataAsSummary(meta: R3HostMetadata): CompileDirectiveSummary { // clang-format off return { // This is used by the BindingParser, which only deals with listeners and properties. There's no // need to pass attributes to it. hostAttributes: {}, - hostListeners: meta.host.listeners, - hostProperties: meta.host.properties, + hostListeners: meta.listeners, + hostProperties: meta.properties, } as CompileDirectiveSummary; // clang-format on } @@ -909,7 +915,7 @@ export function parseHostBindings(host: {[key: string]: string | o.Expression}): */ export function verifyHostBindings( bindings: ParsedHostBindings, sourceSpan: ParseSourceSpan): ParseError[] { - const summary = metadataAsSummary({ host: bindings } as any); + const summary = metadataAsSummary(bindings); // TODO: abstract out host bindings verification logic and use it instead of // creating events and properties ASTs to detect errors (FW-996) const bindingParser = makeBindingParser(); diff --git a/packages/core/src/compiler/compiler_facade_interface.ts b/packages/core/src/compiler/compiler_facade_interface.ts index 97e8c07d54..7214522ddd 100644 --- a/packages/core/src/compiler/compiler_facade_interface.ts +++ b/packages/core/src/compiler/compiler_facade_interface.ts @@ -149,6 +149,8 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade { } export interface R3BaseMetadataFacade { + name: string; + propMetadata: {[key: string]: any[]}; inputs?: {[key: string]: string | [string, string]}; outputs?: {[key: string]: string}; queries?: R3QueryMetadataFacade[]; diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index 4715e117fc..ecefde30c9 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -571,6 +571,11 @@ export function ɵɵdefineBase(baseDefinition: { * set of instructions to be inserted into the template function. */ viewQuery?: ViewQueriesFunction| null; + + /** + * Function executed by the parent template to allow children to apply host bindings. + */ + hostBindings?: HostBindingsFunction; }): ɵɵBaseDef { const declaredInputs: {[P in keyof T]: string} = {} as any; return { @@ -579,6 +584,7 @@ export function ɵɵdefineBase(baseDefinition: { outputs: invertObject(baseDefinition.outputs as any), viewQuery: baseDefinition.viewQuery || null, contentQueries: baseDefinition.contentQueries || null, + hostBindings: baseDefinition.hostBindings || null }; } diff --git a/packages/core/src/render3/features/inherit_definition_feature.ts b/packages/core/src/render3/features/inherit_definition_feature.ts index 76529d4390..f3d0508cfd 100644 --- a/packages/core/src/render3/features/inherit_definition_feature.ts +++ b/packages/core/src/render3/features/inherit_definition_feature.ts @@ -9,7 +9,7 @@ import {Type} from '../../interface/type'; import {fillProperties} from '../../util/property'; import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; -import {ComponentDef, ContentQueriesFunction, DirectiveDef, DirectiveDefFeature, RenderFlags, ViewQueriesFunction} from '../interfaces/definition'; +import {ComponentDef, ContentQueriesFunction, DirectiveDef, DirectiveDefFeature, HostBindingsFunction, RenderFlags, ViewQueriesFunction} from '../interfaces/definition'; import {adjustActiveDirectiveSuperClassDepthPosition} from '../state'; import {isComponentDef} from '../util/view_utils'; @@ -56,6 +56,8 @@ export function ɵɵInheritDefinitionFeature(definition: DirectiveDef| Comp if (baseDef) { const baseViewQuery = baseDef.viewQuery; const baseContentQueries = baseDef.contentQueries; + const baseHostBindings = baseDef.hostBindings; + baseHostBindings && inheritHostBindings(definition, baseHostBindings); baseViewQuery && inheritViewQuery(definition, baseViewQuery); baseContentQueries && inheritContentQueries(definition, baseContentQueries); fillProperties(definition.inputs, baseDef.inputs); @@ -65,34 +67,8 @@ export function ɵɵInheritDefinitionFeature(definition: DirectiveDef| Comp if (superDef) { // Merge hostBindings - const prevHostBindings = definition.hostBindings; const superHostBindings = superDef.hostBindings; - if (superHostBindings) { - if (prevHostBindings) { - // because inheritance is unknown during compile time, the runtime code - // needs to be informed of the super-class depth so that instruction code - // can distinguish one host bindings function from another. The reason why - // relying on the directive uniqueId exclusively is not enough is because the - // uniqueId value and the directive instance stay the same between hostBindings - // calls throughout the directive inheritance chain. This means that without - // a super-class depth value, there is no way to know whether a parent or - // sub-class host bindings function is currently being executed. - definition.hostBindings = (rf: RenderFlags, ctx: any, elementIndex: number) => { - // The reason why we increment first and then decrement is so that parent - // hostBindings calls have a higher id value compared to sub-class hostBindings - // calls (this way the leaf directive is always at a super-class depth of 0). - adjustActiveDirectiveSuperClassDepthPosition(1); - try { - superHostBindings(rf, ctx, elementIndex); - } finally { - adjustActiveDirectiveSuperClassDepthPosition(-1); - } - prevHostBindings(rf, ctx, elementIndex); - }; - } else { - definition.hostBindings = superHostBindings; - } - } + superHostBindings && inheritHostBindings(definition, superHostBindings); // Merge queries const superViewQuery = superDef.viewQuery; @@ -190,3 +166,33 @@ function inheritContentQueries( definition.contentQueries = superContentQueries; } } + +function inheritHostBindings( + definition: DirectiveDef| ComponentDef, + superHostBindings: HostBindingsFunction) { + const prevHostBindings = definition.hostBindings; + if (prevHostBindings) { + // because inheritance is unknown during compile time, the runtime code + // needs to be informed of the super-class depth so that instruction code + // can distinguish one host bindings function from another. The reason why + // relying on the directive uniqueId exclusively is not enough is because the + // uniqueId value and the directive instance stay the same between hostBindings + // calls throughout the directive inheritance chain. This means that without + // a super-class depth value, there is no way to know whether a parent or + // sub-class host bindings function is currently being executed. + definition.hostBindings = (rf: RenderFlags, ctx: any, elementIndex: number) => { + // The reason why we increment first and then decrement is so that parent + // hostBindings calls have a higher id value compared to sub-class hostBindings + // calls (this way the leaf directive is always at a super-class depth of 0). + adjustActiveDirectiveSuperClassDepthPosition(1); + try { + superHostBindings(rf, ctx, elementIndex); + } finally { + adjustActiveDirectiveSuperClassDepthPosition(-1); + } + prevHostBindings(rf, ctx, elementIndex); + }; + } else { + definition.hostBindings = superHostBindings; + } +} diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index f0476d3989..55effb84bb 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -135,6 +135,11 @@ export interface ɵɵBaseDef { * components that extend the directive. */ viewQuery: ViewQueriesFunction|null; + + /** + * Refreshes host bindings on the associated directive. + */ + hostBindings: HostBindingsFunction|null; } /** @@ -173,11 +178,6 @@ export interface DirectiveDef extends ɵɵBaseDef { */ factory: FactoryFn; - /** - * Refreshes host bindings on the associated directive. - */ - hostBindings: HostBindingsFunction|null; - /* The following are lifecycle hooks for this component */ onChanges: (() => void)|null; onInit: (() => void)|null; diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 9f29a54176..42e00d89f6 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -220,22 +220,28 @@ function extractBaseDefMetadata(type: Type): R3BaseMetadataFacade|null { const queries = extractQueriesMetadata(type, propMetadata, isContentQuery); let inputs: {[key: string]: string | [string, string]}|undefined; let outputs: {[key: string]: string}|undefined; + // We only need to know whether there are any HostListener or HostBinding + // decorators present, the parsing logic is in the compiler already. + let hasHostDecorators = false; for (const field in propMetadata) { propMetadata[field].forEach(ann => { - if (ann.ngMetadataName === 'Input') { + const metadataName = ann.ngMetadataName; + if (metadataName === 'Input') { inputs = inputs || {}; inputs[field] = ann.bindingPropertyName ? [ann.bindingPropertyName, field] : field; - } else if (ann.ngMetadataName === 'Output') { + } else if (metadataName === 'Output') { outputs = outputs || {}; outputs[field] = ann.bindingPropertyName || field; + } else if (metadataName === 'HostBinding' || metadataName === 'HostListener') { + hasHostDecorators = true; } }); } // Only generate the base def if there's any info inside it. - if (inputs || outputs || viewQueries.length || queries.length) { - return {inputs, outputs, viewQueries, queries}; + if (inputs || outputs || viewQueries.length || queries.length || hasHostDecorators) { + return {name: type.name, inputs, outputs, viewQueries, queries, propMetadata}; } return null; diff --git a/packages/core/test/acceptance/integration_spec.ts b/packages/core/test/acceptance/integration_spec.ts index 0bae8e89a8..64191a69a7 100644 --- a/packages/core/test/acceptance/integration_spec.ts +++ b/packages/core/test/acceptance/integration_spec.ts @@ -5,7 +5,7 @@ * 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 {Component, ContentChild, Directive, EventEmitter, HostListener, Input, Output, QueryList, TemplateRef, ViewChildren} from '@angular/core'; +import {Component, ContentChild, Directive, EventEmitter, HostBinding, HostListener, Input, Output, QueryList, TemplateRef, ViewChildren} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -171,4 +171,116 @@ describe('acceptance integration tests', () => { expect(clicks).toBe(1); }); + it('should inherit host bindings from undecorated superclasses', () => { + class BaseButton { + @HostBinding('attr.tabindex') + tabindex = -1; + } + + @Component({selector: '[sub-button]', template: ''}) + class SubButton extends BaseButton { + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [SubButton, App]}); + const fixture = TestBed.createComponent(App); + const button = fixture.debugElement.query(By.directive(SubButton)); + fixture.detectChanges(); + + expect(button.nativeElement.getAttribute('tabindex')).toBe('-1'); + + button.componentInstance.tabindex = 2; + fixture.detectChanges(); + + expect(button.nativeElement.getAttribute('tabindex')).toBe('2'); + }); + + it('should inherit host bindings from undecorated grand superclasses', () => { + class SuperBaseButton { + @HostBinding('attr.tabindex') + tabindex = -1; + } + + class BaseButton extends SuperBaseButton {} + + @Component({selector: '[sub-button]', template: ''}) + class SubButton extends BaseButton { + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [SubButton, App]}); + const fixture = TestBed.createComponent(App); + const button = fixture.debugElement.query(By.directive(SubButton)); + fixture.detectChanges(); + + expect(button.nativeElement.getAttribute('tabindex')).toBe('-1'); + + button.componentInstance.tabindex = 2; + fixture.detectChanges(); + + expect(button.nativeElement.getAttribute('tabindex')).toBe('2'); + }); + + it('should inherit host listeners from undecorated superclasses', () => { + let clicks = 0; + + class BaseButton { + @HostListener('click') + handleClick() { clicks++; } + } + + @Component({selector: '[sub-button]', template: ''}) + class SubButton extends BaseButton { + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [SubButton, App]}); + const fixture = TestBed.createComponent(App); + const button = fixture.debugElement.query(By.directive(SubButton)).nativeElement; + + button.click(); + fixture.detectChanges(); + + expect(clicks).toBe(1); + }); + + // TODO(crisbeto): this fails even with decorated classes + // in master. To be enabled as a part of FW-1294. + xit('should inherit host listeners from undecorated grand superclasses', () => { + let clicks = 0; + + class SuperBaseButton { + @HostListener('click') + handleClick() { clicks++; } + } + + class BaseButton extends SuperBaseButton {} + + @Component({selector: '[sub-button]', template: ''}) + class SubButton extends BaseButton { + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [SubButton, App]}); + const fixture = TestBed.createComponent(App); + const button = fixture.debugElement.query(By.directive(SubButton)).nativeElement; + + button.click(); + fixture.detectChanges(); + + expect(clicks).toBe(1); + }); + }); diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 26b76420a6..31e4dedadf 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -666,6 +666,7 @@ export interface ɵɵBaseDef { /** @deprecated */ readonly declaredInputs: { [P in keyof T]: string; }; + hostBindings: HostBindingsFunction | null; readonly inputs: { [P in keyof T]: string; }; @@ -706,6 +707,7 @@ export declare function ɵɵdefineBase(baseDefinition: { }; contentQueries?: ContentQueriesFunction | null; viewQuery?: ViewQueriesFunction | null; + hostBindings?: HostBindingsFunction; }): ɵɵBaseDef; export declare function ɵɵdefineComponent(componentDefinition: {