Skip to content

Commit e4dcaa5

Browse files
crisbetoAndrewKushnir
authored andcommitted
fix(core): unable to inject ChangeDetectorRef inside host directives (#48355)
When injecting the `ChangeDetectorRef` into a node that matches a component, we create a new ref using the component's LView. This breaks down for host directives, because they run before the component's LView has been created. These changes resolve the issue by creating the LView before creating the node injector for the directives. Fixes #48249. PR Close #48355
1 parent c0d0417 commit e4dcaa5

File tree

8 files changed

+54
-28
lines changed

8 files changed

+54
-28
lines changed

packages/core/src/render3/instructions/shared.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,16 @@ function instantiateAllDirectives(
11421142
tView: TView, lView: LView, tNode: TDirectiveHostNode, native: RNode) {
11431143
const start = tNode.directiveStart;
11441144
const end = tNode.directiveEnd;
1145+
1146+
// The component view needs to be created before creating the node injector
1147+
// since it is used to inject some special symbols like `ChangeDetectorRef`.
1148+
if (isComponentHost(tNode)) {
1149+
ngDevMode && assertTNodeType(tNode, TNodeType.AnyRNode);
1150+
addComponentLogic(
1151+
lView, tNode as TElementNode,
1152+
tView.data[start + tNode.componentOffset] as ComponentDef<unknown>);
1153+
}
1154+
11451155
if (!tView.firstCreatePass) {
11461156
getOrCreateNodeInjectorForNode(tNode, lView);
11471157
}
@@ -1151,23 +1161,16 @@ function instantiateAllDirectives(
11511161
const initialInputs = tNode.initialInputs;
11521162
for (let i = start; i < end; i++) {
11531163
const def = tView.data[i] as DirectiveDef<any>;
1154-
const isComponent = isComponentDef(def);
1155-
1156-
if (isComponent) {
1157-
ngDevMode && assertTNodeType(tNode, TNodeType.AnyRNode);
1158-
addComponentLogic(lView, tNode as TElementNode, def as ComponentDef<any>);
1159-
}
1160-
11611164
const directive = getNodeInjectable(lView, tView, i, tNode);
11621165
attachPatchData(directive, lView);
11631166

11641167
if (initialInputs !== null) {
11651168
setInputsFromAttrs(lView, i - start, directive, def, tNode, initialInputs!);
11661169
}
11671170

1168-
if (isComponent) {
1171+
if (isComponentDef(def)) {
11691172
const componentView = getComponentLViewByIndex(tNode.index, lView);
1170-
componentView[CONTEXT] = directive;
1173+
componentView[CONTEXT] = getNodeInjectable(lView, tView, i, tNode);
11711174
}
11721175
}
11731176
}

packages/core/test/acceptance/host_directives_spec.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {AfterViewChecked, AfterViewInit, Component, Directive, ElementRef, EventEmitter, forwardRef, inject, Inject, InjectionToken, Input, OnChanges, OnInit, Output, SimpleChanges, Type, ViewChild, ViewContainerRef} from '@angular/core';
9+
import {AfterViewChecked, AfterViewInit, ChangeDetectorRef, Component, Directive, ElementRef, EventEmitter, forwardRef, inject, Inject, InjectionToken, Input, OnChanges, OnInit, Output, SimpleChanges, Type, ViewChild, ViewContainerRef} from '@angular/core';
1010
import {TestBed} from '@angular/core/testing';
1111
import {By} from '@angular/platform-browser';
1212

@@ -919,6 +919,44 @@ describe('host directives', () => {
919919
expect(() => TestBed.createComponent(App))
920920
.toThrowError(/NG0200: Circular dependency in DI detected for HostDir/);
921921
});
922+
923+
it('should inject a valid ChangeDetectorRef when attached to a component', () => {
924+
type InternalChangeDetectorRef = ChangeDetectorRef&{_lView: unknown};
925+
926+
@Directive({standalone: true})
927+
class HostDir {
928+
changeDetectorRef = inject(ChangeDetectorRef) as InternalChangeDetectorRef;
929+
}
930+
931+
@Component({selector: 'my-comp', hostDirectives: [HostDir], template: ''})
932+
class Comp {
933+
changeDetectorRef = inject(ChangeDetectorRef) as InternalChangeDetectorRef;
934+
}
935+
936+
@Component({template: '<my-comp></my-comp>'})
937+
class App {
938+
@ViewChild(HostDir) hostDir!: HostDir;
939+
@ViewChild(Comp) comp!: Comp;
940+
}
941+
942+
TestBed.configureTestingModule({declarations: [App, Comp]});
943+
const fixture = TestBed.createComponent(App);
944+
fixture.detectChanges();
945+
946+
const hostDirectiveCdr = fixture.componentInstance.hostDir.changeDetectorRef;
947+
const componentCdr = fixture.componentInstance.comp.changeDetectorRef;
948+
949+
// We can't assert that the change detectors are the same by comparing
950+
// them directly, because a new one is created each time. Instead of we
951+
// compare that they're associated with the same LView.
952+
expect(hostDirectiveCdr._lView).toBeTruthy();
953+
expect(componentCdr._lView).toBeTruthy();
954+
expect(hostDirectiveCdr._lView).toBe(componentCdr._lView);
955+
expect(() => {
956+
hostDirectiveCdr.markForCheck();
957+
hostDirectiveCdr.detectChanges();
958+
}).not.toThrow();
959+
});
922960
});
923961

924962
describe('outputs', () => {

packages/core/test/bundling/animations/bundle.golden_symbols.json

-3
Original file line numberDiff line numberDiff line change
@@ -548,9 +548,6 @@
548548
{
549549
"name": "addClass"
550550
},
551-
{
552-
"name": "addComponentLogic"
553-
},
554551
{
555552
"name": "addPropertyAlias"
556553
},

packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -386,9 +386,6 @@
386386
{
387387
"name": "_wrapInTimeout"
388388
},
389-
{
390-
"name": "addComponentLogic"
391-
},
392389
{
393390
"name": "addPropertyAlias"
394391
},
@@ -743,6 +740,9 @@
743740
{
744741
"name": "isComponentDef"
745742
},
743+
{
744+
"name": "isComponentHost"
745+
},
746746
{
747747
"name": "isContentQueryHost"
748748
},

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

-3
Original file line numberDiff line numberDiff line change
@@ -551,9 +551,6 @@
551551
{
552552
"name": "_wrapInTimeout"
553553
},
554-
{
555-
"name": "addComponentLogic"
556-
},
557554
{
558555
"name": "addPropertyAlias"
559556
},

packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json

-3
Original file line numberDiff line numberDiff line change
@@ -539,9 +539,6 @@
539539
{
540540
"name": "_wrapInTimeout"
541541
},
542-
{
543-
"name": "addComponentLogic"
544-
},
545542
{
546543
"name": "addPropertyAlias"
547544
},

packages/core/test/bundling/router/bundle.golden_symbols.json

-3
Original file line numberDiff line numberDiff line change
@@ -707,9 +707,6 @@
707707
{
708708
"name": "absoluteRedirect"
709709
},
710-
{
711-
"name": "addComponentLogic"
712-
},
713710
{
714711
"name": "addPropertyAlias"
715712
},

packages/core/test/bundling/todo/bundle.golden_symbols.json

-3
Original file line numberDiff line numberDiff line change
@@ -464,9 +464,6 @@
464464
{
465465
"name": "_wrapInTimeout"
466466
},
467-
{
468-
"name": "addComponentLogic"
469-
},
470467
{
471468
"name": "addPropertyAlias"
472469
},

0 commit comments

Comments
 (0)