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

View File

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

View File

@ -3220,6 +3220,89 @@ describe('compiler compliance', () => {
expectEmit(result.source, expectedOutput, 'Invalid base definition');
});
it('should add ngBaseDef if a host binding is present', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule, HostBinding} from '@angular/core';
export class BaseClass {
@HostBinding('attr.tabindex')
tabindex = -1;
}
@Component({
selector: 'my-component',
template: ''
})
export class MyComponent extends BaseClass {
}
@NgModule({
declarations: [MyComponent]
})
export class MyModule {}
`
}
};
const expectedOutput = `
// ...
BaseClass.ngBaseDef = $r3$.ɵɵdefineBase({
hostBindings: function (rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵallocHostVars(1);
}
if (rf & 2) {
$r3$.ɵɵelementAttribute(elIndex, "tabindex", $r3$.ɵɵbind(ctx.tabindex));
}
}
});
// ...
`;
const result = compile(files, angularFiles);
expectEmit(result.source, expectedOutput, 'Invalid base definition');
});
it('should add ngBaseDef if a host listener is present', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule, HostListener} from '@angular/core';
export class BaseClass {
@HostListener('mousedown', ['$event'])
handleMousedown(event: any) {}
}
@Component({
selector: 'my-component',
template: ''
})
export class MyComponent extends BaseClass {
}
@NgModule({
declarations: [MyComponent]
})
export class MyModule {}
`
}
};
const expectedOutput = `
// ...
BaseClass.ngBaseDef = $r3$.ɵɵdefineBase({
hostBindings: function (rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵlistener("mousedown", function ($event) {
return ctx.handleMousedown($event);
});
}
}
});
// ...
`;
const result = compile(files, angularFiles);
expectEmit(result.source, expectedOutput, 'Invalid base definition');
});
it('should NOT add ngBaseDef if @Component is present', () => {
const files = {
app: {

View File

@ -1723,7 +1723,7 @@ describe('ngtsc behavioral tests', () => {
.toContain('Cannot have a pipe in an action expression');
});
it('should throw in case pipes are used in host listeners', () => {
it('should throw in case pipes are used in host bindings', () => {
env.tsconfig();
env.write(`test.ts`, `
import {Component} from '@angular/core';