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

Skip to content

Commit 3aa45a2

Browse files
crisbetopkozlowski-opensource
authored andcommitted
fix(core): resolve forward-referenced host directives during directive matching (#58492) (#58500)
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 PR Close #58500
1 parent 21a4c02 commit 3aa45a2

File tree

3 files changed

+148
-48
lines changed

3 files changed

+148
-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 '../definition';
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>(
@@ -493,6 +502,15 @@ export type HostDirectiveBindingMap = {
493502
*/
494503
export type HostDirectiveDefs = Map<DirectiveDef<unknown>, HostDirectiveDef>;
495504

505+
/** Value that can be used to configure a host directive. */
506+
export type HostDirectiveConfig =
507+
| Type<unknown>
508+
| {
509+
directive: Type<unknown>;
510+
inputs?: string[];
511+
outputs?: string[];
512+
};
513+
496514
export interface ComponentDefFeature {
497515
<T>(componentDef: ComponentDef<T>): void;
498516
/**

packages/core/test/acceptance/host_directives_spec.ts

Lines changed: 78 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';
@@ -74,38 +75,106 @@ describe('host directives', () => {
7475
it('should apply a host directive referenced through a forwardRef', () => {
7576
const logs: string[] = [];
7677

77-
@Directive({
78-
selector: '[dir]',
79-
hostDirectives: [forwardRef(() => HostDir), {directive: forwardRef(() => OtherHostDir)}],
80-
})
78+
// This directive was "compiled" manually, because our tests are JIT-compiled and the JIT
79+
// compiler doesn't produce the callback-based variant of the `ɵɵHostDirectivesFeature`.
80+
// This represents the following metadata:
81+
// @Directive({
82+
// selector: '[dir]',
83+
// hostDirectives: [forwardRef(() => HostDir), {directive: forwardRef(() => OtherHostDir)}],
84+
// standalone: false,
85+
// })
8186
class Dir {
87+
static ɵfac = () => new Dir();
88+
static ɵdir = ɵɵdefineDirective({
89+
type: Dir,
90+
selectors: [['', 'dir', '']],
91+
standalone: false,
92+
features: [ɵɵHostDirectivesFeature(() => [HostDir, {directive: OtherHostDir}])],
93+
});
94+
8295
constructor() {
8396
logs.push('Dir');
8497
}
8598
}
8699

100+
@Directive({standalone: true})
101+
class OtherHostDir {
102+
constructor() {
103+
logs.push('OtherHostDir');
104+
}
105+
}
106+
87107
@Directive({standalone: true})
88108
class HostDir {
89109
constructor() {
90110
logs.push('HostDir');
91111
}
92112
}
93113

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

101-
@Component({template: '<div dir></div>'})
155+
@Directive({standalone: true, host: {'two': 'base'}})
156+
class HostDir {
157+
constructor() {
158+
inject(Dir);
159+
}
160+
}
161+
162+
@Component({
163+
template: '<div dir></div>',
164+
standalone: false,
165+
})
102166
class App {}
103167

104168
TestBed.configureTestingModule({declarations: [App, Dir]});
105169
const fixture = TestBed.createComponent(App);
106170
fixture.detectChanges();
107171

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

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

0 commit comments

Comments
 (0)