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:
parent
164d160b22
commit
68ff2cc323
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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[];
|
||||||
|
|
|
@ -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 || {});
|
||||||
|
|
||||||
|
|
|
@ -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;};
|
||||||
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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[];
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
Loading…
Reference in New Issue