From c91c0b82da1a466f3a3944813466887d47f5e56b Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 5 Dec 2022 11:58:56 +0100 Subject: [PATCH] fix(core): unable to inject ChangeDetectorRef inside host directives 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. --- .../core/src/render3/instructions/shared.ts | 21 +++++----- .../test/acceptance/host_directives_spec.ts | 40 ++++++++++++++++++- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 6bf616c199674c..145bf73cefacbd 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -1218,6 +1218,16 @@ function instantiateAllDirectives( tView: TView, lView: LView, tNode: TDirectiveHostNode, native: RNode) { const start = tNode.directiveStart; const end = tNode.directiveEnd; + + // The component view needs to be created before creating the node injector + // since it is used to inject some special symbols like `ChangeDetectorRef`. + if (isComponentHost(tNode)) { + ngDevMode && assertTNodeType(tNode, TNodeType.AnyRNode); + addComponentLogic( + lView, tNode as TElementNode, + tView.data[start + tNode.componentOffset] as ComponentDef); + } + if (!tView.firstCreatePass) { getOrCreateNodeInjectorForNode(tNode, lView); } @@ -1227,13 +1237,6 @@ function instantiateAllDirectives( const initialInputs = tNode.initialInputs; for (let i = start; i < end; i++) { const def = tView.data[i] as DirectiveDef; - const isComponent = isComponentDef(def); - - if (isComponent) { - ngDevMode && assertTNodeType(tNode, TNodeType.AnyRNode); - addComponentLogic(lView, tNode as TElementNode, def as ComponentDef); - } - const directive = getNodeInjectable(lView, tView, i, tNode); attachPatchData(directive, lView); @@ -1241,9 +1244,9 @@ function instantiateAllDirectives( setInputsFromAttrs(lView, i - start, directive, def, tNode, initialInputs!); } - if (isComponent) { + if (isComponentDef(def)) { const componentView = getComponentLViewByIndex(tNode.index, lView); - componentView[CONTEXT] = directive; + componentView[CONTEXT] = getNodeInjectable(lView, tView, i, tNode); } } } diff --git a/packages/core/test/acceptance/host_directives_spec.ts b/packages/core/test/acceptance/host_directives_spec.ts index 828f7bd8e582b0..85b6a951814d84 100644 --- a/packages/core/test/acceptance/host_directives_spec.ts +++ b/packages/core/test/acceptance/host_directives_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AfterViewChecked, AfterViewInit, Component, Directive, ElementRef, EventEmitter, forwardRef, inject, Inject, InjectionToken, Input, OnChanges, OnInit, Output, SimpleChanges, Type, ViewChild, ViewContainerRef} from '@angular/core'; +import {AfterViewChecked, AfterViewInit, ChangeDetectorRef, Component, Directive, ElementRef, EventEmitter, forwardRef, inject, Inject, InjectionToken, Input, OnChanges, OnInit, Output, SimpleChanges, Type, ViewChild, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; @@ -919,6 +919,44 @@ describe('host directives', () => { expect(() => TestBed.createComponent(App)) .toThrowError(/NG0200: Circular dependency in DI detected for HostDir/); }); + + it('should inject a valid ChangeDetectorRef when attached to a component', () => { + type InternalChangeDetectorRef = ChangeDetectorRef&{_lView: unknown}; + + @Directive({standalone: true}) + class HostDir { + changeDetectorRef = inject(ChangeDetectorRef) as InternalChangeDetectorRef; + } + + @Component({selector: 'my-comp', hostDirectives: [HostDir], template: ''}) + class Comp { + changeDetectorRef = inject(ChangeDetectorRef) as InternalChangeDetectorRef; + } + + @Component({template: ''}) + class App { + @ViewChild(HostDir) hostDir!: HostDir; + @ViewChild(Comp) comp!: Comp; + } + + TestBed.configureTestingModule({declarations: [App, Comp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const hostDirectiveCdr = fixture.componentInstance.hostDir.changeDetectorRef; + const componentCdr = fixture.componentInstance.comp.changeDetectorRef; + + // We can't assert that the change detectors are the same by comparing + // them directly, because a new one is created each time. Instead of we + // compare that they're associated with the same LView. + expect(hostDirectiveCdr._lView).toBeTruthy(); + expect(componentCdr._lView).toBeTruthy(); + expect(hostDirectiveCdr._lView).toBe(componentCdr._lView); + expect(() => { + hostDirectiveCdr.markForCheck(); + hostDirectiveCdr.detectChanges(); + }).not.toThrow(); + }); }); describe('outputs', () => {