Skip to content

Commit dee50f1

Browse files
crisbetoAndrewKushnir
authored andcommitted
fix(core): inherit host directives (#52992)
Adds support for inheriting host directives from the parent class. This is consistent with how we inherit other features like host bindings. Fixes #51203. PR Close #52992
1 parent 91486aa commit dee50f1

File tree

5 files changed

+302
-9
lines changed

5 files changed

+302
-9
lines changed

packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import {Reference} from '../../imports';
1010
import {ClassDeclaration} from '../../reflection';
1111

12-
import {DirectiveMeta, InputMapping, MetadataReader} from './api';
12+
import {DirectiveMeta, HostDirectiveMeta, InputMapping, MetadataReader} from './api';
1313
import {ClassPropertyMapping, ClassPropertyName} from './property_mapping';
1414

1515
/**
@@ -34,6 +34,7 @@ export function flattenInheritedDirectiveMetadata(
3434
const undeclaredInputFields = new Set<ClassPropertyName>();
3535
const restrictedInputFields = new Set<ClassPropertyName>();
3636
const stringLiteralInputFields = new Set<ClassPropertyName>();
37+
let hostDirectives: HostDirectiveMeta[]|null = null;
3738
let isDynamic = false;
3839
let inputs = ClassPropertyMapping.empty<InputMapping>();
3940
let outputs = ClassPropertyMapping.empty();
@@ -69,6 +70,10 @@ export function flattenInheritedDirectiveMetadata(
6970
for (const field of meta.stringLiteralInputFields) {
7071
stringLiteralInputFields.add(field);
7172
}
73+
if (meta.hostDirectives !== null && meta.hostDirectives.length > 0) {
74+
hostDirectives ??= [];
75+
hostDirectives.push(...meta.hostDirectives);
76+
}
7277
};
7378

7479
addMetadata(topMeta);
@@ -83,5 +88,6 @@ export function flattenInheritedDirectiveMetadata(
8388
stringLiteralInputFields,
8489
baseClass: isDynamic ? 'dynamic' : null,
8590
isStructural,
91+
hostDirectives,
8692
};
8793
}

packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3622,6 +3622,103 @@ suppress
36223622
expect(ts.flattenDiagnosticMessageText(diags[0].messageText, ''))
36233623
.toContain('HostBindDirective');
36243624
});
3625+
3626+
it('should check bindings to inherited host directive inputs', () => {
3627+
env.write('test.ts', `
3628+
import {Component, Directive, NgModule, Input} from '@angular/core';
3629+
3630+
@Directive({
3631+
standalone: true,
3632+
})
3633+
class HostDir {
3634+
@Input() input: number;
3635+
@Input() otherInput: string;
3636+
}
3637+
3638+
@Directive({
3639+
hostDirectives: [{directive: HostDir, inputs: ['input', 'otherInput: alias']}]
3640+
})
3641+
class Parent {}
3642+
3643+
@Directive({selector: '[dir]'})
3644+
class Dir extends Parent {}
3645+
3646+
@Component({
3647+
selector: 'test',
3648+
template: '<div dir [input]="person.name" [alias]="person.age"></div>',
3649+
})
3650+
class TestCmp {
3651+
person: {
3652+
name: string;
3653+
age: number;
3654+
};
3655+
}
3656+
3657+
@NgModule({
3658+
declarations: [TestCmp, Dir],
3659+
})
3660+
class Module {}
3661+
`);
3662+
3663+
3664+
const messages = env.driveDiagnostics().map(d => d.messageText);
3665+
3666+
expect(messages).toEqual([
3667+
`Type 'string' is not assignable to type 'number'.`,
3668+
`Type 'number' is not assignable to type 'string'.`
3669+
]);
3670+
});
3671+
3672+
it('should check bindings to inherited host directive outputs', () => {
3673+
env.write('test.ts', `
3674+
import {Component, Directive, NgModule, Output, EventEmitter} from '@angular/core';
3675+
3676+
@Directive({
3677+
standalone: true,
3678+
})
3679+
class HostDir {
3680+
@Output() stringEvent = new EventEmitter<string>();
3681+
@Output() numberEvent = new EventEmitter<number>();
3682+
}
3683+
3684+
@Directive({
3685+
hostDirectives: [
3686+
{directive: HostDir, outputs: ['stringEvent', 'numberEvent: numberAlias']}
3687+
]
3688+
})
3689+
class Parent {}
3690+
3691+
@Directive({selector: '[dir]'})
3692+
class Dir extends Parent {}
3693+
3694+
@Component({
3695+
selector: 'test',
3696+
template: \`
3697+
<div
3698+
dir
3699+
(numberAlias)="handleStringEvent($event)"
3700+
(stringEvent)="handleNumberEvent($event)"></div>
3701+
\`,
3702+
})
3703+
class TestCmp {
3704+
handleStringEvent(event: string): void {}
3705+
handleNumberEvent(event: number): void {}
3706+
}
3707+
3708+
@NgModule({
3709+
declarations: [TestCmp, Dir],
3710+
})
3711+
class Module {}
3712+
`);
3713+
3714+
3715+
const messages = env.driveDiagnostics().map(d => d.messageText);
3716+
3717+
expect(messages).toEqual([
3718+
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
3719+
`Argument of type 'string' is not assignable to parameter of type 'number'.`
3720+
]);
3721+
});
36253722
});
36263723

36273724
describe('deferred blocks', () => {

packages/compiler/src/render3/view/compiler.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ function addFeatures(
118118
break;
119119
}
120120
}
121+
// Note: host directives feature needs to be inserted before the
122+
// inheritance feature to ensure the correct execution order.
123+
if (meta.hostDirectives?.length) {
124+
features.push(o.importExpr(R3.HostDirectivesFeature).callFn([createHostDirectivesFeatureArg(
125+
meta.hostDirectives)]));
126+
}
121127
if (meta.usesInheritance) {
122128
features.push(o.importExpr(R3.InheritDefinitionFeature));
123129
}
@@ -131,10 +137,6 @@ function addFeatures(
131137
if (meta.hasOwnProperty('template') && meta.isStandalone) {
132138
features.push(o.importExpr(R3.StandaloneFeature));
133139
}
134-
if (meta.hostDirectives?.length) {
135-
features.push(o.importExpr(R3.HostDirectivesFeature).callFn([createHostDirectivesFeatureArg(
136-
meta.hostDirectives)]));
137-
}
138140
if (features.length) {
139141
definitionMap.set('features', o.literalArr(features));
140142
}

packages/core/src/render3/features/host_directives_feature.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {Type} from '../../interface/type';
1111
import {assertEqual} from '../../util/assert';
1212
import {EMPTY_OBJ} from '../../util/empty';
1313
import {getComponentDef, getDirectiveDef} from '../definition';
14-
import {DirectiveDef, HostDirectiveBindingMap, HostDirectiveDef, HostDirectiveDefs} from '../interfaces/definition';
14+
import {DirectiveDef, DirectiveDefFeature, HostDirectiveBindingMap, HostDirectiveDef, HostDirectiveDefs} from '../interfaces/definition';
1515

1616
/** Values that can be used to define a host directive through the `HostDirectivesFeature`. */
1717
type HostDirectiveConfig = Type<unknown>|{
@@ -42,9 +42,8 @@ type HostDirectiveConfig = Type<unknown>|{
4242
*/
4343
export function ɵɵHostDirectivesFeature(rawHostDirectives: HostDirectiveConfig[]|
4444
(() => HostDirectiveConfig[])) {
45-
return (definition: DirectiveDef<unknown>) => {
46-
definition.findHostDirectiveDefs = findHostDirectiveDefs;
47-
definition.hostDirectives =
45+
const feature: DirectiveDefFeature = (definition: DirectiveDef<unknown>) => {
46+
const resolved =
4847
(Array.isArray(rawHostDirectives) ? rawHostDirectives : rawHostDirectives()).map(dir => {
4948
return typeof dir === 'function' ?
5049
{directive: resolveForwardRef(dir), inputs: EMPTY_OBJ, outputs: EMPTY_OBJ} :
@@ -54,7 +53,15 @@ export function ɵɵHostDirectivesFeature(rawHostDirectives: HostDirectiveConfig
5453
outputs: bindingArrayToMap(dir.outputs)
5554
};
5655
});
56+
if (definition.hostDirectives === null) {
57+
definition.findHostDirectiveDefs = findHostDirectiveDefs;
58+
definition.hostDirectives = resolved;
59+
} else {
60+
definition.hostDirectives.unshift(...resolved);
61+
}
5762
};
63+
feature.ngInherit = true;
64+
return feature;
5865
}
5966

6067
function findHostDirectiveDefs(

0 commit comments

Comments
 (0)