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
This commit is contained in:
parent
722eb5dd45
commit
307dac67bc
|
@ -17,6 +17,7 @@ import {EMPTY_ARRAY} from '../util/empty';
|
||||||
import {stringify} from '../util/stringify';
|
import {stringify} from '../util/stringify';
|
||||||
|
|
||||||
import {resolveForwardRef} from './forward_ref';
|
import {resolveForwardRef} from './forward_ref';
|
||||||
|
import {setInjectImplementation} from './inject_switch';
|
||||||
import {InjectionToken} from './injection_token';
|
import {InjectionToken} from './injection_token';
|
||||||
import {Injector} from './injector';
|
import {Injector} from './injector';
|
||||||
import {catchInjectorError, injectArgs, NG_TEMP_TOKEN_PATH, setCurrentInjector, THROW_IF_NOT_FOUND, USE_VALUE, ɵɵinject} from './injector_compatibility';
|
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 {
|
||||||
this.assertNotDestroyed();
|
this.assertNotDestroyed();
|
||||||
// Set the injection context.
|
// Set the injection context.
|
||||||
const previousInjector = setCurrentInjector(this);
|
const previousInjector = setCurrentInjector(this);
|
||||||
|
const previousInjectImplementation = setInjectImplementation(undefined);
|
||||||
try {
|
try {
|
||||||
// Check for the SkipSelf flag.
|
// Check for the SkipSelf flag.
|
||||||
if (!(flags & InjectFlags.SkipSelf)) {
|
if (!(flags & InjectFlags.SkipSelf)) {
|
||||||
|
@ -234,7 +236,8 @@ export class R3Injector {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Lastly, clean up the state by restoring the previous injector.
|
// Lastly, restore the previous injection context.
|
||||||
|
setInjectImplementation(previousInjectImplementation);
|
||||||
setCurrentInjector(previousInjector);
|
setCurrentInjector(previousInjector);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
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';
|
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';
|
||||||
import {ɵINJECTOR_SCOPE} from '@angular/core/src/core';
|
import {ɵINJECTOR_SCOPE} from '@angular/core/src/core';
|
||||||
import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref';
|
import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref';
|
||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
@ -599,6 +599,93 @@ describe('di', () => {
|
||||||
expect(() => TestBed.createComponent(MyComp)).toThrowError(/No provider for DirectiveB/);
|
expect(() => TestBed.createComponent(MyComp)).toThrowError(/No provider for DirectiveB/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not have access to the directive injector in a standalone injector from within a directive-level provider factory',
|
||||||
|
() => {
|
||||||
|
// https://github.com/angular/angular/issues/42651
|
||||||
|
class TestA {
|
||||||
|
constructor(public injector: string) {}
|
||||||
|
}
|
||||||
|
class TestB {
|
||||||
|
constructor(public a: TestA) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTestB() {
|
||||||
|
// Setup a standalone injector that provides `TestA`, which is resolved from a
|
||||||
|
// standalone child injector that requests `TestA` as a dependency for `TestB`.
|
||||||
|
// Although we're inside a directive factory and therefore have access to the
|
||||||
|
// directive-level injector, `TestA` has to be resolved from the standalone injector.
|
||||||
|
const parent = Injector.create({
|
||||||
|
providers: [{provide: TestA, useFactory: () => new TestA('standalone'), deps: []}],
|
||||||
|
name: 'TestA',
|
||||||
|
});
|
||||||
|
const child = Injector.create({
|
||||||
|
providers: [{provide: TestB, useClass: TestB, deps: [TestA]}],
|
||||||
|
parent,
|
||||||
|
name: 'TestB',
|
||||||
|
});
|
||||||
|
return child.get(TestB);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: '',
|
||||||
|
providers: [
|
||||||
|
{provide: TestA, useFactory: () => new TestA('component'), deps: []},
|
||||||
|
{provide: TestB, useFactory: createTestB},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
class MyComp {
|
||||||
|
constructor(public readonly testB: TestB) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [MyComp]});
|
||||||
|
|
||||||
|
const cmp = TestBed.createComponent(MyComp);
|
||||||
|
expect(cmp.componentInstance.testB).toBeInstanceOf(TestB);
|
||||||
|
expect(cmp.componentInstance.testB.a.injector).toBe('standalone');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have access to the directive injector in a standalone injector from within a directive-level provider factory',
|
||||||
|
() => {
|
||||||
|
class TestA {
|
||||||
|
constructor(public injector: string) {}
|
||||||
|
}
|
||||||
|
class TestB {
|
||||||
|
constructor(public a: TestA|null) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTestB() {
|
||||||
|
// Setup a standalone injector that provides `TestB` with an optional dependency of
|
||||||
|
// `TestA`. Since `TestA` is not provided by the standalone injector it should resolve
|
||||||
|
// to null; both the NgModule providers and the component-level providers should not
|
||||||
|
// be considered.
|
||||||
|
const injector = Injector.create({
|
||||||
|
providers: [{provide: TestB, useClass: TestB, deps: [[TestA, new Optional()]]}],
|
||||||
|
name: 'TestB',
|
||||||
|
});
|
||||||
|
return injector.get(TestB);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: '',
|
||||||
|
providers: [
|
||||||
|
{provide: TestA, useFactory: () => new TestA('component'), deps: []},
|
||||||
|
{provide: TestB, useFactory: createTestB},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
class MyComp {
|
||||||
|
constructor(public readonly testB: TestB) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [MyComp],
|
||||||
|
providers: [{provide: TestA, useFactory: () => new TestA('module'), deps: []}]
|
||||||
|
});
|
||||||
|
|
||||||
|
const cmp = TestBed.createComponent(MyComp);
|
||||||
|
expect(cmp.componentInstance.testB).toBeInstanceOf(TestB);
|
||||||
|
expect(cmp.componentInstance.testB.a).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
onlyInIvy('Ivy has different error message for circular dependency')
|
onlyInIvy('Ivy has different error message for circular dependency')
|
||||||
.it('should throw if directives try to inject each other', () => {
|
.it('should throw if directives try to inject each other', () => {
|
||||||
@Directive({selector: '[dirB]'})
|
@Directive({selector: '[dirB]'})
|
||||||
|
|
|
@ -92,6 +92,9 @@
|
||||||
{
|
{
|
||||||
"name": "injectArgs"
|
"name": "injectArgs"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "injectInjectorOnly"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "injectableDefOrInjectorDefFactory"
|
"name": "injectableDefOrInjectorDefFactory"
|
||||||
},
|
},
|
||||||
|
@ -110,6 +113,9 @@
|
||||||
{
|
{
|
||||||
"name": "setCurrentInjector"
|
"name": "setCurrentInjector"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "setInjectImplementation"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "stringify"
|
"name": "stringify"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue