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

@ -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[];

View File

@ -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 || {});

View File

@ -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;};
}

View File

@ -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();