Skip to content

Commit a6db152

Browse files
JoostKalxhub
authored andcommitted
fix(core): use correct injector when resolving DI tokens from within a directive provider factory (#42886)
When a directive provides a DI token using a factory function and interacting with a standalone injector from within that factory, the standalone injector should not have access to either the directive injector nor the NgModule injector; only the standalone injector should be used. This commit ensures that a standalone injector never reaches into the directive-level injection context while resolving DI tokens. Fixes #42651 PR Close #42886
1 parent 778edfc commit a6db152

File tree

3 files changed

+98
-2
lines changed

3 files changed

+98
-2
lines changed

packages/core/src/di/r3_injector.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {EMPTY_ARRAY} from '../util/empty';
1717
import {stringify} from '../util/stringify';
1818

1919
import {resolveForwardRef} from './forward_ref';
20+
import {setInjectImplementation} from './inject_switch';
2021
import {InjectionToken} from './injection_token';
2122
import {Injector} from './injector';
2223
import {catchInjectorError, injectArgs, NG_TEMP_TOKEN_PATH, setCurrentInjector, THROW_IF_NOT_FOUND, USE_VALUE, ɵɵinject} from './injector_compatibility';
@@ -186,6 +187,7 @@ export class R3Injector {
186187
this.assertNotDestroyed();
187188
// Set the injection context.
188189
const previousInjector = setCurrentInjector(this);
190+
const previousInjectImplementation = setInjectImplementation(undefined);
189191
try {
190192
// Check for the SkipSelf flag.
191193
if (!(flags & InjectFlags.SkipSelf)) {
@@ -234,7 +236,8 @@ export class R3Injector {
234236
throw e;
235237
}
236238
} finally {
237-
// Lastly, clean up the state by restoring the previous injector.
239+
// Lastly, restore the previous injection context.
240+
setInjectImplementation(previousInjectImplementation);
238241
setCurrentInjector(previousInjector);
239242
}
240243
}

packages/core/test/acceptance/di_spec.ts

+88-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {CommonModule} from '@angular/common';
10-
import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, ContentChild, Directive, ElementRef, EventEmitter, forwardRef, Host, HostBinding, Inject, Injectable, InjectionToken, INJECTOR, Injector, Input, LOCALE_ID, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, ViewRef, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID} from '@angular/core';
10+
import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, forwardRef, Host, HostBinding, Inject, Injectable, InjectionToken, INJECTOR, Injector, Input, LOCALE_ID, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, ViewRef, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID} from '@angular/core';
1111
import {ɵINJECTOR_SCOPE} from '@angular/core/src/core';
1212
import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref';
1313
import {TestBed} from '@angular/core/testing';
@@ -599,6 +599,93 @@ describe('di', () => {
599599
expect(() => TestBed.createComponent(MyComp)).toThrowError(/No provider for DirectiveB/);
600600
});
601601

602+
it('should not have access to the directive injector in a standalone injector from within a directive-level provider factory',
603+
() => {
604+
// https://github.com/angular/angular/issues/42651
605+
class TestA {
606+
constructor(public injector: string) {}
607+
}
608+
class TestB {
609+
constructor(public a: TestA) {}
610+
}
611+
612+
function createTestB() {
613+
// Setup a standalone injector that provides `TestA`, which is resolved from a
614+
// standalone child injector that requests `TestA` as a dependency for `TestB`.
615+
// Although we're inside a directive factory and therefore have access to the
616+
// directive-level injector, `TestA` has to be resolved from the standalone injector.
617+
const parent = Injector.create({
618+
providers: [{provide: TestA, useFactory: () => new TestA('standalone'), deps: []}],
619+
name: 'TestA',
620+
});
621+
const child = Injector.create({
622+
providers: [{provide: TestB, useClass: TestB, deps: [TestA]}],
623+
parent,
624+
name: 'TestB',
625+
});
626+
return child.get(TestB);
627+
}
628+
629+
@Component({
630+
template: '',
631+
providers: [
632+
{provide: TestA, useFactory: () => new TestA('component'), deps: []},
633+
{provide: TestB, useFactory: createTestB},
634+
],
635+
})
636+
class MyComp {
637+
constructor(public readonly testB: TestB) {}
638+
}
639+
640+
TestBed.configureTestingModule({declarations: [MyComp]});
641+
642+
const cmp = TestBed.createComponent(MyComp);
643+
expect(cmp.componentInstance.testB).toBeInstanceOf(TestB);
644+
expect(cmp.componentInstance.testB.a.injector).toBe('standalone');
645+
});
646+
647+
it('should not have access to the directive injector in a standalone injector from within a directive-level provider factory',
648+
() => {
649+
class TestA {
650+
constructor(public injector: string) {}
651+
}
652+
class TestB {
653+
constructor(public a: TestA|null) {}
654+
}
655+
656+
function createTestB() {
657+
// Setup a standalone injector that provides `TestB` with an optional dependency of
658+
// `TestA`. Since `TestA` is not provided by the standalone injector it should resolve
659+
// to null; both the NgModule providers and the component-level providers should not
660+
// be considered.
661+
const injector = Injector.create({
662+
providers: [{provide: TestB, useClass: TestB, deps: [[TestA, new Optional()]]}],
663+
name: 'TestB',
664+
});
665+
return injector.get(TestB);
666+
}
667+
668+
@Component({
669+
template: '',
670+
providers: [
671+
{provide: TestA, useFactory: () => new TestA('component'), deps: []},
672+
{provide: TestB, useFactory: createTestB},
673+
],
674+
})
675+
class MyComp {
676+
constructor(public readonly testB: TestB) {}
677+
}
678+
679+
TestBed.configureTestingModule({
680+
declarations: [MyComp],
681+
providers: [{provide: TestA, useFactory: () => new TestA('module'), deps: []}]
682+
});
683+
684+
const cmp = TestBed.createComponent(MyComp);
685+
expect(cmp.componentInstance.testB).toBeInstanceOf(TestB);
686+
expect(cmp.componentInstance.testB.a).toBeNull();
687+
});
688+
602689
onlyInIvy('Ivy has different error message for circular dependency')
603690
.it('should throw if directives try to inject each other', () => {
604691
@Directive({selector: '[dirB]'})

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

+6
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@
9292
{
9393
"name": "injectArgs"
9494
},
95+
{
96+
"name": "injectInjectorOnly"
97+
},
9598
{
9699
"name": "injectableDefOrInjectorDefFactory"
97100
},
@@ -110,6 +113,9 @@
110113
{
111114
"name": "setCurrentInjector"
112115
},
116+
{
117+
"name": "setInjectImplementation"
118+
},
113119
{
114120
"name": "stringify"
115121
},

0 commit comments

Comments
 (0)