Skip to content

Commit

Permalink
fix(core): unable to inject ChangeDetectorRef inside host directives
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
crisbeto committed Dec 5, 2022
1 parent aa66f70 commit c91c0b8
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 10 deletions.
21 changes: 12 additions & 9 deletions packages/core/src/render3/instructions/shared.ts
Expand Up @@ -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<unknown>);
}

if (!tView.firstCreatePass) {
getOrCreateNodeInjectorForNode(tNode, lView);
}
Expand All @@ -1227,23 +1237,16 @@ function instantiateAllDirectives(
const initialInputs = tNode.initialInputs;
for (let i = start; i < end; i++) {
const def = tView.data[i] as DirectiveDef<any>;
const isComponent = isComponentDef(def);

if (isComponent) {
ngDevMode && assertTNodeType(tNode, TNodeType.AnyRNode);
addComponentLogic(lView, tNode as TElementNode, def as ComponentDef<any>);
}

const directive = getNodeInjectable(lView, tView, i, tNode);
attachPatchData(directive, lView);

if (initialInputs !== null) {
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);
}
}
}
Expand Down
40 changes: 39 additions & 1 deletion packages/core/test/acceptance/host_directives_spec.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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: '<my-comp></my-comp>'})
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', () => {
Expand Down

0 comments on commit c91c0b8

Please sign in to comment.