Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6e0af6d

Browse files
crisbetopkozlowski-opensource
authored andcommitted
fix(core): resolve forward-referenced host directives during directive matching (#58492)
When the compiler generates the `HostDirectivesFeature`, it generates either an eager call (`ɵɵHostDirectivesFeature([])`) or a lazy call (`ɵɵHostDirectivesFeature(() => [])`. The lazy call is necessary when there are forward references within the `hostDirectives` array. Currently we resolve the lazy variant when the component definition is created which has been enough for most cases, however if the host is injected by one of its host directives, we can run into a reference error because DI is synchronous and the host's class hasn't been defined yet. These changes resolve the issue by pushing the lazy resolution later during directive matching when all classes are guanrateed to exist. Fixes #58485. PR Close #58492
1 parent 36f4457 commit 6e0af6d

File tree

3 files changed

+144
-48
lines changed

3 files changed

+144
-48
lines changed

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

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,18 @@
77
*/
88
import {resolveForwardRef} from '../../di';
99
import {RuntimeError, RuntimeErrorCode} from '../../errors';
10-
import {Type} from '../../interface/type';
1110
import {assertEqual} from '../../util/assert';
1211
import {EMPTY_OBJ} from '../../util/empty';
1312
import {getComponentDef, getDirectiveDef} from '../def_getters';
14-
import {
13+
import type {
1514
DirectiveDef,
1615
DirectiveDefFeature,
1716
HostDirectiveBindingMap,
17+
HostDirectiveConfig,
1818
HostDirectiveDef,
1919
HostDirectiveDefs,
2020
} from '../interfaces/definition';
2121

22-
/** Values that can be used to define a host directive through the `HostDirectivesFeature`. */
23-
type HostDirectiveConfig =
24-
| Type<unknown>
25-
| {
26-
directive: Type<unknown>;
27-
inputs?: string[];
28-
outputs?: string[];
29-
};
30-
3122
/**
3223
* This feature adds the host directives behavior to a directive definition by patching a
3324
* function onto it. The expectation is that the runtime will invoke the function during
@@ -52,22 +43,17 @@ export function ɵɵHostDirectivesFeature(
5243
rawHostDirectives: HostDirectiveConfig[] | (() => HostDirectiveConfig[]),
5344
) {
5445
const feature: DirectiveDefFeature = (definition: DirectiveDef<unknown>) => {
55-
const resolved = (
56-
Array.isArray(rawHostDirectives) ? rawHostDirectives : rawHostDirectives()
57-
).map((dir) => {
58-
return typeof dir === 'function'
59-
? {directive: resolveForwardRef(dir), inputs: EMPTY_OBJ, outputs: EMPTY_OBJ}
60-
: {
61-
directive: resolveForwardRef(dir.directive),
62-
inputs: bindingArrayToMap(dir.inputs),
63-
outputs: bindingArrayToMap(dir.outputs),
64-
};
65-
});
46+
const isEager = Array.isArray(rawHostDirectives);
47+
6648
if (definition.hostDirectives === null) {
6749
definition.findHostDirectiveDefs = findHostDirectiveDefs;
68-
definition.hostDirectives = resolved;
50+
definition.hostDirectives = isEager
51+
? rawHostDirectives.map(createHostDirectiveDef)
52+
: [rawHostDirectives];
53+
} else if (isEager) {
54+
definition.hostDirectives.unshift(...rawHostDirectives.map(createHostDirectiveDef));
6955
} else {
70-
definition.hostDirectives.unshift(...resolved);
56+
definition.hostDirectives.unshift(rawHostDirectives);
7157
}
7258
};
7359
feature.ngInherit = true;
@@ -80,23 +66,50 @@ function findHostDirectiveDefs(
8066
hostDirectiveDefs: HostDirectiveDefs,
8167
): void {
8268
if (currentDef.hostDirectives !== null) {
83-
for (const hostDirectiveConfig of currentDef.hostDirectives) {
84-
const hostDirectiveDef = getDirectiveDef(hostDirectiveConfig.directive)!;
85-
86-
if (typeof ngDevMode === 'undefined' || ngDevMode) {
87-
validateHostDirective(hostDirectiveConfig, hostDirectiveDef);
69+
for (const configOrFn of currentDef.hostDirectives) {
70+
if (typeof configOrFn === 'function') {
71+
const resolved = configOrFn();
72+
for (const config of resolved) {
73+
trackHostDirectiveDef(createHostDirectiveDef(config), matchedDefs, hostDirectiveDefs);
74+
}
75+
} else {
76+
trackHostDirectiveDef(configOrFn, matchedDefs, hostDirectiveDefs);
8877
}
78+
}
79+
}
80+
}
8981

90-
// We need to patch the `declaredInputs` so that
91-
// `ngOnChanges` can map the properties correctly.
92-
patchDeclaredInputs(hostDirectiveDef.declaredInputs, hostDirectiveConfig.inputs);
82+
/** Tracks a single host directive during directive matching. */
83+
function trackHostDirectiveDef(
84+
def: HostDirectiveDef,
85+
matchedDefs: DirectiveDef<unknown>[],
86+
hostDirectiveDefs: HostDirectiveDefs,
87+
) {
88+
const hostDirectiveDef = getDirectiveDef(def.directive)!;
9389

94-
// Host directives execute before the host so that its host bindings can be overwritten.
95-
findHostDirectiveDefs(hostDirectiveDef, matchedDefs, hostDirectiveDefs);
96-
hostDirectiveDefs.set(hostDirectiveDef, hostDirectiveConfig);
97-
matchedDefs.push(hostDirectiveDef);
98-
}
90+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
91+
validateHostDirective(def, hostDirectiveDef);
9992
}
93+
94+
// We need to patch the `declaredInputs` so that
95+
// `ngOnChanges` can map the properties correctly.
96+
patchDeclaredInputs(hostDirectiveDef.declaredInputs, def.inputs);
97+
98+
// Host directives execute before the host so that its host bindings can be overwritten.
99+
findHostDirectiveDefs(hostDirectiveDef, matchedDefs, hostDirectiveDefs);
100+
hostDirectiveDefs.set(hostDirectiveDef, def);
101+
matchedDefs.push(hostDirectiveDef);
102+
}
103+
104+
/** Creates a `HostDirectiveDef` from a used-defined host directive configuration. */
105+
function createHostDirectiveDef(config: HostDirectiveConfig): HostDirectiveDef {
106+
return typeof config === 'function'
107+
? {directive: resolveForwardRef(config), inputs: EMPTY_OBJ, outputs: EMPTY_OBJ}
108+
: {
109+
directive: resolveForwardRef(config.directive),
110+
inputs: bindingArrayToMap(config.inputs),
111+
outputs: bindingArrayToMap(config.outputs),
112+
};
100113
}
101114

102115
/**

packages/core/src/render3/interfaces/definition.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,17 @@ export interface DirectiveDef<T> {
260260
) => void)
261261
| null;
262262

263-
/** Additional directives to be applied whenever the directive has been matched. */
264-
hostDirectives: HostDirectiveDef[] | null;
263+
/**
264+
* Additional directives to be applied whenever the directive has been matched.
265+
*
266+
* `HostDirectiveConfig` objects represent a host directive that can be resolved eagerly and were
267+
* already pre-processed when the definition was created. A function needs to be resolved lazily
268+
* during directive matching, because it's a forward reference.
269+
*
270+
* **Note:** we can't `HostDirectiveConfig` in the array, because there's no way to distinguish if
271+
* a function in the array is a `Type` or a `() => HostDirectiveConfig[]`.
272+
*/
273+
hostDirectives: (HostDirectiveDef | (() => HostDirectiveConfig[]))[] | null;
265274

266275
setInput:
267276
| (<U extends T>(
@@ -498,6 +507,15 @@ export type HostDirectiveBindingMap = {
498507
*/
499508
export type HostDirectiveDefs = Map<DirectiveDef<unknown>, HostDirectiveDef>;
500509

510+
/** Value that can be used to configure a host directive. */
511+
export type HostDirectiveConfig =
512+
| Type<unknown>
513+
| {
514+
directive: Type<unknown>;
515+
inputs?: string[];
516+
outputs?: string[];
517+
};
518+
501519
export interface ComponentDefFeature {
502520
<T>(componentDef: ComponentDef<T>): void;
503521
/**

packages/core/test/acceptance/host_directives_spec.ts

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
Directive,
1515
ElementRef,
1616
EventEmitter,
17-
forwardRef,
1817
inject,
1918
Inject,
2019
InjectionToken,
@@ -26,6 +25,8 @@ import {
2625
Type,
2726
ViewChild,
2827
ViewContainerRef,
28+
ɵɵdefineDirective,
29+
ɵɵHostDirectivesFeature,
2930
} from '@angular/core';
3031
import {TestBed} from '@angular/core/testing';
3132
import {By} from '@angular/platform-browser';
@@ -78,28 +79,87 @@ describe('host directives', () => {
7879
it('should apply a host directive referenced through a forwardRef', () => {
7980
const logs: string[] = [];
8081

81-
@Directive({
82-
selector: '[dir]',
83-
hostDirectives: [forwardRef(() => HostDir), {directive: forwardRef(() => OtherHostDir)}],
84-
standalone: false,
85-
})
82+
// This directive was "compiled" manually, because our tests are JIT-compiled and the JIT
83+
// compiler doesn't produce the callback-based variant of the `ɵɵHostDirectivesFeature`.
84+
// This represents the following metadata:
85+
// @Directive({
86+
// selector: '[dir]',
87+
// hostDirectives: [forwardRef(() => HostDir), {directive: forwardRef(() => OtherHostDir)}],
88+
// standalone: false,
89+
// })
8690
class Dir {
91+
static ɵfac = () => new Dir();
92+
static ɵdir = ɵɵdefineDirective({
93+
type: Dir,
94+
selectors: [['', 'dir', '']],
95+
standalone: false,
96+
features: [ɵɵHostDirectivesFeature(() => [HostDir, {directive: OtherHostDir}])],
97+
});
98+
8799
constructor() {
88100
logs.push('Dir');
89101
}
90102
}
91103

104+
@Directive({standalone: true})
105+
class OtherHostDir {
106+
constructor() {
107+
logs.push('OtherHostDir');
108+
}
109+
}
110+
92111
@Directive({standalone: true})
93112
class HostDir {
94113
constructor() {
95114
logs.push('HostDir');
96115
}
97116
}
98117

99-
@Directive({standalone: true})
118+
@Component({
119+
template: '<div dir></div>',
120+
standalone: false,
121+
})
122+
class App {}
123+
124+
TestBed.configureTestingModule({declarations: [App, Dir]});
125+
const fixture = TestBed.createComponent(App);
126+
fixture.detectChanges();
127+
128+
expect(logs).toEqual(['HostDir', 'OtherHostDir', 'Dir']);
129+
});
130+
131+
it('should apply a directive that references host directives through a forwardRef and is injected by its host directives', () => {
132+
// This directive was "compiled" manually, because our tests are JIT-compiled and the JIT
133+
// compiler doesn't produce the callback-based variant of the `ɵɵHostDirectivesFeature`.
134+
// This represents the following metadata:
135+
// @Directive({
136+
// selector: '[dir]',
137+
// hostDirectives: [forwardRef(() => HostDir), {directive: forwardRef(() => OtherHostDir)}],
138+
// standalone: false,
139+
// host: {'one': 'override', 'two': 'override'}
140+
// })
141+
class Dir {
142+
static ɵfac = () => new Dir();
143+
static ɵdir = ɵɵdefineDirective({
144+
type: Dir,
145+
selectors: [['', 'dir', '']],
146+
standalone: false,
147+
hostAttrs: ['one', 'override', 'two', 'override'],
148+
features: [ɵɵHostDirectivesFeature(() => [HostDir, {directive: OtherHostDir}])],
149+
});
150+
}
151+
152+
@Directive({standalone: true, host: {'one': 'base'}})
100153
class OtherHostDir {
101154
constructor() {
102-
logs.push('OtherHostDir');
155+
inject(Dir);
156+
}
157+
}
158+
159+
@Directive({standalone: true, host: {'two': 'base'}})
160+
class HostDir {
161+
constructor() {
162+
inject(Dir);
103163
}
104164
}
105165

@@ -113,7 +173,12 @@ describe('host directives', () => {
113173
const fixture = TestBed.createComponent(App);
114174
fixture.detectChanges();
115175

116-
expect(logs).toEqual(['HostDir', 'OtherHostDir', 'Dir']);
176+
// Note: we can't use the constructor call order here to determine the initialization order,
177+
// because the act of injecting `Dir` will cause it to be created earlier than its host bindings
178+
// will be invoked. Instead we check that the host bindings apply in the right order.
179+
const host = fixture.nativeElement.querySelector('[dir]');
180+
expect(host.getAttribute('one')).toBe('override');
181+
expect(host.getAttribute('two')).toBe('override');
117182
});
118183

119184
it('should apply a chain of host directives', () => {

0 commit comments

Comments
 (0)