fix(ivy): host bindings and listeners not being inherited from undecorated classes (#30158)

Fixes `HostBinding` and `HostListener` declarations not being inherited from base classes that don't have an Angular decorator.

This PR resolves FW-1275.

PR Close #30158
This commit is contained in:
Kristiyan Kostadinov 2019-04-27 09:33:10 +02:00 committed by Andrew Kushnir
parent 164d160b22
commit 68ff2cc323
15 changed files with 365 additions and 117 deletions

View File

@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license * 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 {PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, ClassMember, Decorator, ReflectionHost} from '../../reflection'; import {ClassDeclaration, ClassMember, Decorator, ReflectionHost} from '../../reflection';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
import {queriesFromFields} from './directive'; import {extractHostBindings, queriesFromFields} from './directive';
import {isAngularDecorator} from './util'; import {isAngularDecorator} from './util';
function containsNgTopLevelDecorator(decorators: Decorator[] | null, isCore: boolean): boolean { function containsNgTopLevelDecorator(decorators: Decorator[] | null, isCore: boolean): boolean {
@ -69,6 +69,12 @@ export class BaseDefDecoratorHandler implements
result = result || {}; result = result || {};
const queries = result.queries = result.queries || []; const queries = result.queries = result.queries || [];
queries.push({member: property, decorators}); 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): analyze(node: ClassDeclaration, metadata: R3BaseRefDecoratorDetection):
AnalysisOutput<R3BaseRefMetaData> { AnalysisOutput<R3BaseRefMetaData> {
const analysis: R3BaseRefMetaData = {}; const analysis: R3BaseRefMetaData = {name: node.name.text, typeSourceSpan: null !};
if (metadata.inputs) { if (metadata.inputs) {
const inputs = analysis.inputs = {} as{[key: string]: string | [string, string]}; const inputs = analysis.inputs = {} as{[key: string]: string | [string, string]};
metadata.inputs.forEach(({decorator, property}) => { metadata.inputs.forEach(({decorator, property}) => {
@ -133,12 +140,17 @@ export class BaseDefDecoratorHandler implements
analysis.queries = queriesFromFields(metadata.queries, this.reflector, this.evaluator); 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}; return {analysis};
} }
compile(node: ClassDeclaration, analysis: R3BaseRefMetaData, pool: ConstantPool): compile(node: ClassDeclaration, analysis: R3BaseRefMetaData, pool: ConstantPool):
CompileResult[]|CompileResult { CompileResult[]|CompileResult {
const {expression, type} = compileBaseDefFromMetadata(analysis, pool); const {expression, type} = compileBaseDefFromMetadata(analysis, pool, makeBindingParser());
return { return {
name: 'ngBaseDef', name: 'ngBaseDef',
@ -149,8 +161,9 @@ export class BaseDefDecoratorHandler implements
} }
export interface R3BaseRefDecoratorDetection { export interface R3BaseRefDecoratorDetection {
inputs?: Array<{property: ClassMember, decorator: Decorator}>; inputs?: {property: ClassMember, decorator: Decorator}[];
outputs?: Array<{property: ClassMember, decorator: Decorator}>; outputs?: {property: ClassMember, decorator: Decorator}[];
viewQueries?: {member: ClassMember, decorators: Decorator[]}[]; viewQueries?: {member: ClassMember, decorators: Decorator[]}[];
queries?: {member: ClassMember, decorators: Decorator[]}[]; queries?: {member: ClassMember, decorators: Decorator[]}[];
host?: ClassMember[];
} }

View File

@ -194,7 +194,7 @@ export function extractDirectiveMetadata(
throw new Error(`Directive ${clazz.name.text} has no selector, please add it!`); 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 = const providers: Expression|null =
directive.has('providers') ? new WrappedNodeExpr(directive.get('providers') !) : null; directive.has('providers') ? new WrappedNodeExpr(directive.get('providers') !) : null;
@ -460,11 +460,11 @@ type StringMap<T> = {
[key: string]: T; [key: string]: T;
}; };
function extractHostBindings( export function extractHostBindings(
metadata: Map<string, ts.Expression>, members: ClassMember[], evaluator: PartialEvaluator, members: ClassMember[], evaluator: PartialEvaluator, coreModule: string | undefined,
coreModule: string | undefined): ParsedHostBindings { metadata?: Map<string, ts.Expression>): ParsedHostBindings {
let hostMetadata: StringMap<string|Expression> = {}; let hostMetadata: StringMap<string|Expression> = {};
if (metadata.has('host')) { if (metadata && metadata.has('host')) {
const expr = metadata.get('host') !; const expr = metadata.get('host') !;
const hostMetaMap = evaluator.evaluate(expr); const hostMetaMap = evaluator.evaluate(expr);
if (!(hostMetaMap instanceof Map)) { if (!(hostMetaMap instanceof Map)) {
@ -501,7 +501,7 @@ function extractHostBindings(
throw new FatalDiagnosticError( throw new FatalDiagnosticError(
// TODO: provide more granular diagnostic and output specific host expression that triggered // TODO: provide more granular diagnostic and output specific host expression that triggered
// an error instead of the whole host object // 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')); errors.map((error: ParseError) => error.msg).join('\n'));
} }

View File

@ -3220,6 +3220,89 @@ describe('compiler compliance', () => {
expectEmit(result.source, expectedOutput, 'Invalid base definition'); 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', () => { it('should NOT add ngBaseDef if @Component is present', () => {
const files = { const files = {
app: { app: {

View File

@ -1723,7 +1723,7 @@ describe('ngtsc behavioral tests', () => {
.toContain('Cannot have a pipe in an action expression'); .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.tsconfig();
env.write(`test.ts`, ` env.write(`test.ts`, `
import {Component} from '@angular/core'; import {Component} from '@angular/core';

View File

@ -149,6 +149,8 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade {
} }
export interface R3BaseMetadataFacade { export interface R3BaseMetadataFacade {
name: string;
propMetadata: {[key: string]: any[]};
inputs?: {[key: string]: string | [string, string]}; inputs?: {[key: string]: string | [string, string]};
outputs?: {[key: string]: string}; outputs?: {[key: string]: string};
queries?: R3QueryMetadataFacade[]; queries?: R3QueryMetadataFacade[];

View File

@ -154,13 +154,17 @@ export class CompilerFacadeImpl implements CompilerFacade {
compileBase(angularCoreEnv: CoreEnvironment, sourceMapUrl: string, facade: R3BaseMetadataFacade): compileBase(angularCoreEnv: CoreEnvironment, sourceMapUrl: string, facade: R3BaseMetadataFacade):
any { any {
const constantPool = new ConstantPool(); const constantPool = new ConstantPool();
const typeSourceSpan =
this.createParseSourceSpan('Base', facade.name, `ng:///${facade.name}.js`);
const meta = { const meta = {
...facade, ...facade,
typeSourceSpan,
viewQueries: facade.viewQueries ? facade.viewQueries.map(convertToR3QueryMetadata) : viewQueries: facade.viewQueries ? facade.viewQueries.map(convertToR3QueryMetadata) :
facade.viewQueries, 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( return this.jitExpression(
res.expression, angularCoreEnv, sourceMapUrl, constantPool.statements); res.expression, angularCoreEnv, sourceMapUrl, constantPool.statements);
} }
@ -244,7 +248,7 @@ function convertDirectiveFacadeToMetadata(facade: R3DirectiveMetadataFacade): R3
typeSourceSpan: facade.typeSourceSpan, typeSourceSpan: facade.typeSourceSpan,
type: new WrappedNodeExpr(facade.type), type: new WrappedNodeExpr(facade.type),
deps: convertR3DependencyMetadataArray(facade.deps), deps: convertR3DependencyMetadataArray(facade.deps),
host: extractHostBindings(facade.host, facade.propMetadata, facade.typeSourceSpan), host: extractHostBindings(facade.propMetadata, facade.typeSourceSpan, facade.host),
inputs: {...inputsFromMetadata, ...inputsFromType}, inputs: {...inputsFromMetadata, ...inputsFromType},
outputs: {...outputsFromMetadata, ...outputsFromType}, outputs: {...outputsFromMetadata, ...outputsFromType},
queries: facade.queries.map(convertToR3QueryMetadata), queries: facade.queries.map(convertToR3QueryMetadata),
@ -298,8 +302,8 @@ function convertR3DependencyMetadataArray(facades: R3DependencyMetadataFacade[]
} }
function extractHostBindings( function extractHostBindings(
host: {[key: string]: string}, propMetadata: {[key: string]: any[]}, propMetadata: {[key: string]: any[]}, sourceSpan: ParseSourceSpan,
sourceSpan: ParseSourceSpan): ParsedHostBindings { host?: {[key: string]: string}): ParsedHostBindings {
// First parse the declarations from the metadata. // First parse the declarations from the metadata.
const bindings = parseHostBindings(host || {}); const bindings = parseHostBindings(host || {});

View File

@ -62,24 +62,7 @@ export interface R3DirectiveMetadata {
* Mappings indicating how the directive interacts with its host element (host bindings, * Mappings indicating how the directive interacts with its host element (host bindings,
* listeners, etc). * listeners, etc).
*/ */
host: { host: 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;}
};
/** /**
* Information about usage of specific lifecycle events which require special treatment in the * Information about usage of specific lifecycle events which require special treatment in the
@ -265,3 +248,26 @@ export interface R3ComponentDef {
type: o.Type; type: o.Type;
statements: o.Statement[]; 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;};
}

View File

@ -28,7 +28,7 @@ import {Identifiers as R3} from '../r3_identifiers';
import {Render3ParseResult} from '../r3_template_transform'; import {Render3ParseResult} from '../r3_template_transform';
import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, typeWithParameters} from '../util'; 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 {Instruction, StylingBuilder} from './styling_builder';
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn} from './template'; import {BindingScope, TemplateDefinitionBuilder, ValueConverter, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn} from './template';
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util'; 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)); '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) => { ... } // e.g. `hostBindings: (rf, ctx, elIndex) => { ... }
definitionMap.set( definitionMap.set(
'hostBindings', createHostBindingsFunction( 'hostBindings', createHostBindingsFunction(
meta, elVarExp, contextVarExp, meta.host.attributes, styleBuilder, meta.host, meta.typeSourceSpan, bindingParser, constantPool,
bindingParser, constantPool, hostVarsCount)); meta.selector || '', meta.name));
// e.g 'inputs: {a: 'a'}` // e.g 'inputs: {a: 'a'}`
definitionMap.set('inputs', conditionallyCreateMapObjectLiteral(meta.inputs, true)); definitionMap.set('inputs', conditionallyCreateMapObjectLiteral(meta.inputs, true));
@ -163,17 +142,21 @@ export function compileDirectiveFromMetadata(
} }
export interface R3BaseRefMetaData { export interface R3BaseRefMetaData {
name: string;
typeSourceSpan: ParseSourceSpan;
inputs?: {[key: string]: string | [string, string]}; inputs?: {[key: string]: string | [string, string]};
outputs?: {[key: string]: string}; outputs?: {[key: string]: string};
viewQueries?: R3QueryMetadata[]; viewQueries?: R3QueryMetadata[];
queries?: R3QueryMetadata[]; queries?: R3QueryMetadata[];
host?: R3HostMetadata;
} }
/** /**
* Compile a base definition for the render3 runtime as defined by {@link R3BaseRefMetadata} * Compile a base definition for the render3 runtime as defined by {@link R3BaseRefMetadata}
* @param meta the metadata used for compilation. * @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(); const definitionMap = new DefinitionMap();
if (meta.inputs) { if (meta.inputs) {
const inputs = meta.inputs; const inputs = meta.inputs;
@ -198,6 +181,12 @@ export function compileBaseDefFromMetadata(meta: R3BaseRefMetaData, constantPool
if (meta.queries && meta.queries.length > 0) { if (meta.queries && meta.queries.length > 0) {
definitionMap.set('contentQueries', createContentQueriesFunction(meta.queries, constantPool)); 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 expression = o.importExpr(R3.defineBase).callFn([definitionMap.toLiteralMap()]);
const type = new o.ExpressionType(o.importExpr(R3.BaseDef)); 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. // Return a host binding function or null if one is not necessary.
function createHostBindingsFunction( function createHostBindingsFunction(
meta: R3DirectiveMetadata, elVarExp: o.ReadVarExpr, bindingContext: o.ReadVarExpr, hostBindingsMetadata: R3HostMetadata, typeSourceSpan: ParseSourceSpan,
staticAttributesAndValues: {[name: string]: o.Expression}, styleBuilder: StylingBuilder, bindingParser: BindingParser, constantPool: ConstantPool, selector: string,
bindingParser: BindingParser, constantPool: ConstantPool, hostVarsCount: number): o.Expression| name?: string): o.Expression|null {
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 createStatements: o.Statement[] = [];
const updateStatements: o.Statement[] = []; const updateStatements: o.Statement[] = [];
let totalHostVarsCount = hostVarsCount; let totalHostVarsCount = hostVarsCount;
const hostBindingSourceSpan = meta.typeSourceSpan; const hostBindingSourceSpan = typeSourceSpan;
const directiveSummary = metadataAsSummary(meta); const directiveSummary = metadataAsSummary(hostBindingsMetadata);
let valueConverter: ValueConverter; let valueConverter: ValueConverter;
const getValueConverter = () => { const getValueConverter = () => {
@ -625,7 +633,7 @@ function createHostBindingsFunction(
const eventBindings = const eventBindings =
bindingParser.createDirectiveHostEventAsts(directiveSummary, hostBindingSourceSpan); bindingParser.createDirectiveHostEventAsts(directiveSummary, hostBindingSourceSpan);
if (eventBindings && eventBindings.length) { if (eventBindings && eventBindings.length) {
const listeners = createHostListeners(bindingContext, eventBindings, meta); const listeners = createHostListeners(bindingContext, eventBindings, name);
createStatements.push(...listeners); createStatements.push(...listeners);
} }
@ -643,7 +651,7 @@ function createHostBindingsFunction(
const {bindingName, instruction, isAttribute} = getBindingNameAndInstruction(binding); const {bindingName, instruction, isAttribute} = getBindingNameAndInstruction(binding);
const securityContexts = const securityContexts =
bindingParser.calcPossibleSecurityContexts(meta.selector || '', bindingName, isAttribute) bindingParser.calcPossibleSecurityContexts(selector, bindingName, isAttribute)
.filter(context => context !== core.SecurityContext.NONE); .filter(context => context !== core.SecurityContext.NONE);
let sanitizerFn: o.ExternalExpr|null = null; 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 // 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 // to the host element alongside any of the provided host attributes that were
// collected earlier. // collected earlier.
const hostAttrs = convertAttributesToExpressions(staticAttributesAndValues); const hostAttrs = convertAttributesToExpressions(hostBindingsMetadata.attributes);
const hostInstruction = styleBuilder.buildHostAttrsInstruction(null, hostAttrs, constantPool); const hostInstruction = styleBuilder.buildHostAttrsInstruction(null, hostAttrs, constantPool);
if (hostInstruction) { if (hostInstruction) {
createStatements.push(createStylingStmt(hostInstruction, bindingContext, bindingFn)); createStatements.push(createStylingStmt(hostInstruction, bindingContext, bindingFn));
@ -729,7 +737,7 @@ function createHostBindingsFunction(
} }
if (createStatements.length > 0 || updateStatements.length > 0) { 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[] = []; const statements: o.Statement[] = [];
if (createStatements.length > 0) { if (createStatements.length > 0) {
statements.push(renderFlagCheckIfStmt(core.RenderFlags.Create, createStatements)); statements.push(renderFlagCheckIfStmt(core.RenderFlags.Create, createStatements));
@ -787,15 +795,13 @@ function getBindingNameAndInstruction(binding: ParsedProperty):
} }
function createHostListeners( function createHostListeners(
bindingContext: o.Expression, eventBindings: ParsedEvent[], bindingContext: o.Expression, eventBindings: ParsedEvent[], name?: string): o.Statement[] {
meta: R3DirectiveMetadata): o.Statement[] {
return eventBindings.map(binding => { return eventBindings.map(binding => {
let bindingName = binding.name && sanitizeIdentifier(binding.name); let bindingName = binding.name && sanitizeIdentifier(binding.name);
const bindingFnName = binding.type === ParsedEventType.Animation ? const bindingFnName = binding.type === ParsedEventType.Animation ?
prepareSyntheticListenerFunctionName(bindingName, binding.targetOrPhase) : prepareSyntheticListenerFunctionName(bindingName, binding.targetOrPhase) :
bindingName; bindingName;
const handlerName = const handlerName = name && bindingName ? `${name}_${bindingFnName}_HostBindingHandler` : null;
meta.name && bindingName ? `${meta.name}_${bindingFnName}_HostBindingHandler` : null;
const params = prepareEventListenerParameters( const params = prepareEventListenerParameters(
BoundEvent.fromParsedEvent(binding), bindingContext, handlerName); BoundEvent.fromParsedEvent(binding), bindingContext, handlerName);
const instruction = const instruction =
@ -804,14 +810,14 @@ function createHostListeners(
}); });
} }
function metadataAsSummary(meta: R3DirectiveMetadata): CompileDirectiveSummary { function metadataAsSummary(meta: R3HostMetadata): CompileDirectiveSummary {
// clang-format off // clang-format off
return { return {
// This is used by the BindingParser, which only deals with listeners and properties. There's no // This is used by the BindingParser, which only deals with listeners and properties. There's no
// need to pass attributes to it. // need to pass attributes to it.
hostAttributes: {}, hostAttributes: {},
hostListeners: meta.host.listeners, hostListeners: meta.listeners,
hostProperties: meta.host.properties, hostProperties: meta.properties,
} as CompileDirectiveSummary; } as CompileDirectiveSummary;
// clang-format on // clang-format on
} }
@ -909,7 +915,7 @@ export function parseHostBindings(host: {[key: string]: string | o.Expression}):
*/ */
export function verifyHostBindings( export function verifyHostBindings(
bindings: ParsedHostBindings, sourceSpan: ParseSourceSpan): ParseError[] { 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 // TODO: abstract out host bindings verification logic and use it instead of
// creating events and properties ASTs to detect errors (FW-996) // creating events and properties ASTs to detect errors (FW-996)
const bindingParser = makeBindingParser(); const bindingParser = makeBindingParser();

View File

@ -149,6 +149,8 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade {
} }
export interface R3BaseMetadataFacade { export interface R3BaseMetadataFacade {
name: string;
propMetadata: {[key: string]: any[]};
inputs?: {[key: string]: string | [string, string]}; inputs?: {[key: string]: string | [string, string]};
outputs?: {[key: string]: string}; outputs?: {[key: string]: string};
queries?: R3QueryMetadataFacade[]; queries?: R3QueryMetadataFacade[];

View File

@ -571,6 +571,11 @@ export function ɵɵdefineBase<T>(baseDefinition: {
* set of instructions to be inserted into the template function. * set of instructions to be inserted into the template function.
*/ */
viewQuery?: ViewQueriesFunction<T>| null; viewQuery?: ViewQueriesFunction<T>| null;
/**
* Function executed by the parent template to allow children to apply host bindings.
*/
hostBindings?: HostBindingsFunction<T>;
}): ɵɵBaseDef<T> { }): ɵɵBaseDef<T> {
const declaredInputs: {[P in keyof T]: string} = {} as any; const declaredInputs: {[P in keyof T]: string} = {} as any;
return { return {
@ -579,6 +584,7 @@ export function ɵɵdefineBase<T>(baseDefinition: {
outputs: invertObject<T>(baseDefinition.outputs as any), outputs: invertObject<T>(baseDefinition.outputs as any),
viewQuery: baseDefinition.viewQuery || null, viewQuery: baseDefinition.viewQuery || null,
contentQueries: baseDefinition.contentQueries || null, contentQueries: baseDefinition.contentQueries || null,
hostBindings: baseDefinition.hostBindings || null
}; };
} }

View File

@ -9,7 +9,7 @@
import {Type} from '../../interface/type'; import {Type} from '../../interface/type';
import {fillProperties} from '../../util/property'; import {fillProperties} from '../../util/property';
import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; 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 {adjustActiveDirectiveSuperClassDepthPosition} from '../state';
import {isComponentDef} from '../util/view_utils'; import {isComponentDef} from '../util/view_utils';
@ -56,6 +56,8 @@ export function ɵɵInheritDefinitionFeature(definition: DirectiveDef<any>| Comp
if (baseDef) { if (baseDef) {
const baseViewQuery = baseDef.viewQuery; const baseViewQuery = baseDef.viewQuery;
const baseContentQueries = baseDef.contentQueries; const baseContentQueries = baseDef.contentQueries;
const baseHostBindings = baseDef.hostBindings;
baseHostBindings && inheritHostBindings(definition, baseHostBindings);
baseViewQuery && inheritViewQuery(definition, baseViewQuery); baseViewQuery && inheritViewQuery(definition, baseViewQuery);
baseContentQueries && inheritContentQueries(definition, baseContentQueries); baseContentQueries && inheritContentQueries(definition, baseContentQueries);
fillProperties(definition.inputs, baseDef.inputs); fillProperties(definition.inputs, baseDef.inputs);
@ -65,34 +67,8 @@ export function ɵɵInheritDefinitionFeature(definition: DirectiveDef<any>| Comp
if (superDef) { if (superDef) {
// Merge hostBindings // Merge hostBindings
const prevHostBindings = definition.hostBindings;
const superHostBindings = superDef.hostBindings; const superHostBindings = superDef.hostBindings;
if (superHostBindings) { superHostBindings && inheritHostBindings(definition, 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;
}
}
// Merge queries // Merge queries
const superViewQuery = superDef.viewQuery; const superViewQuery = superDef.viewQuery;
@ -190,3 +166,33 @@ function inheritContentQueries(
definition.contentQueries = superContentQueries; definition.contentQueries = superContentQueries;
} }
} }
function inheritHostBindings(
definition: DirectiveDef<any>| ComponentDef<any>,
superHostBindings: HostBindingsFunction<any>) {
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;
}
}

View File

@ -135,6 +135,11 @@ export interface ɵɵBaseDef<T> {
* components that extend the directive. * components that extend the directive.
*/ */
viewQuery: ViewQueriesFunction<T>|null; viewQuery: ViewQueriesFunction<T>|null;
/**
* Refreshes host bindings on the associated directive.
*/
hostBindings: HostBindingsFunction<T>|null;
} }
/** /**
@ -173,11 +178,6 @@ export interface DirectiveDef<T> extends ɵɵBaseDef<T> {
*/ */
factory: FactoryFn<T>; factory: FactoryFn<T>;
/**
* Refreshes host bindings on the associated directive.
*/
hostBindings: HostBindingsFunction<T>|null;
/* The following are lifecycle hooks for this component */ /* The following are lifecycle hooks for this component */
onChanges: (() => void)|null; onChanges: (() => void)|null;
onInit: (() => void)|null; onInit: (() => void)|null;

View File

@ -220,22 +220,28 @@ function extractBaseDefMetadata(type: Type<any>): R3BaseMetadataFacade|null {
const queries = extractQueriesMetadata(type, propMetadata, isContentQuery); const queries = extractQueriesMetadata(type, propMetadata, isContentQuery);
let inputs: {[key: string]: string | [string, string]}|undefined; let inputs: {[key: string]: string | [string, string]}|undefined;
let outputs: {[key: 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) { for (const field in propMetadata) {
propMetadata[field].forEach(ann => { propMetadata[field].forEach(ann => {
if (ann.ngMetadataName === 'Input') { const metadataName = ann.ngMetadataName;
if (metadataName === 'Input') {
inputs = inputs || {}; inputs = inputs || {};
inputs[field] = ann.bindingPropertyName ? [ann.bindingPropertyName, field] : field; inputs[field] = ann.bindingPropertyName ? [ann.bindingPropertyName, field] : field;
} else if (ann.ngMetadataName === 'Output') { } else if (metadataName === 'Output') {
outputs = outputs || {}; outputs = outputs || {};
outputs[field] = ann.bindingPropertyName || field; 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. // Only generate the base def if there's any info inside it.
if (inputs || outputs || viewQueries.length || queries.length) { if (inputs || outputs || viewQueries.length || queries.length || hasHostDecorators) {
return {inputs, outputs, viewQueries, queries}; return {name: type.name, inputs, outputs, viewQueries, queries, propMetadata};
} }
return null; return null;

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser'; import {By} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
@ -171,4 +171,116 @@ describe('acceptance integration tests', () => {
expect(clicks).toBe(1); expect(clicks).toBe(1);
}); });
it('should inherit host bindings from undecorated superclasses', () => {
class BaseButton {
@HostBinding('attr.tabindex')
tabindex = -1;
}
@Component({selector: '[sub-button]', template: '<ng-content></ng-content>'})
class SubButton extends BaseButton {
}
@Component({template: '<button sub-button>Click me</button>'})
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: '<ng-content></ng-content>'})
class SubButton extends BaseButton {
}
@Component({template: '<button sub-button>Click me</button>'})
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: '<ng-content></ng-content>'})
class SubButton extends BaseButton {
}
@Component({template: '<button sub-button>Click me</button>'})
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: '<ng-content></ng-content>'})
class SubButton extends BaseButton {
}
@Component({template: '<button sub-button>Click me</button>'})
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);
});
}); });

View File

@ -666,6 +666,7 @@ export interface ɵɵBaseDef<T> {
/** @deprecated */ readonly declaredInputs: { /** @deprecated */ readonly declaredInputs: {
[P in keyof T]: string; [P in keyof T]: string;
}; };
hostBindings: HostBindingsFunction<T> | null;
readonly inputs: { readonly inputs: {
[P in keyof T]: string; [P in keyof T]: string;
}; };
@ -706,6 +707,7 @@ export declare function ɵɵdefineBase<T>(baseDefinition: {
}; };
contentQueries?: ContentQueriesFunction<T> | null; contentQueries?: ContentQueriesFunction<T> | null;
viewQuery?: ViewQueriesFunction<T> | null; viewQuery?: ViewQueriesFunction<T> | null;
hostBindings?: HostBindingsFunction<T>;
}): ɵɵBaseDef<T>; }): ɵɵBaseDef<T>;
export declare function ɵɵdefineComponent<T>(componentDefinition: { export declare function ɵɵdefineComponent<T>(componentDefinition: {