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:

committed by
Andrew Kushnir

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