Skip to content

Commit 553f80f

Browse files
AndrewKushnirmhevery
authored andcommitted
fix(ivy): set proper implementation for module injector (angular#28667)
Prior to this change we used current injector implementation for module injector, which was causing problems and produces circular dependencies in case the same token is referenced (with @SkipSelf flag) in the `deps` array. The origin of the problem was that once `directiveInject` implementation becomes active, it was used for module injector as well, thus searching deps in Component/Directive DI scope. This fix sets `injectInjectorOnly` implementation for module injector to resolve the problem. PR Close angular#28667
1 parent 5cafd44 commit 553f80f

File tree

4 files changed

+259
-571
lines changed

4 files changed

+259
-571
lines changed

packages/core/src/render3/component_ref.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {ChangeDetectorRef as ViewEngine_ChangeDetectorRef} from '../change_detec
1010
import {InjectionToken} from '../di/injection_token';
1111
import {Injector} from '../di/injector';
1212
import {inject} from '../di/injector_compatibility';
13+
import {InjectFlags} from '../di/interface/injector';
1314
import {Type} from '../interface/type';
1415
import {ComponentFactory as viewEngine_ComponentFactory, ComponentRef as viewEngine_ComponentRef} from '../linker/component_factory';
1516
import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver';
@@ -77,8 +78,8 @@ export const SCHEDULER = new InjectionToken<((fn: () => void) => void)>('SCHEDUL
7778

7879
function createChainedInjector(rootViewInjector: Injector, moduleInjector: Injector): Injector {
7980
return {
80-
get: <T>(token: Type<T>| InjectionToken<T>, notFoundValue?: T): T => {
81-
const value = rootViewInjector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR);
81+
get: <T>(token: Type<T>| InjectionToken<T>, notFoundValue?: T, flags?: InjectFlags): T => {
82+
const value = rootViewInjector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as T, flags);
8283

8384
if (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR ||
8485
notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) {
@@ -90,7 +91,7 @@ function createChainedInjector(rootViewInjector: Injector, moduleInjector: Injec
9091
return value;
9192
}
9293

93-
return moduleInjector.get(token, notFoundValue);
94+
return moduleInjector.get(token, notFoundValue, flags);
9495
}
9596
};
9697
}

packages/core/src/render3/di.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -392,10 +392,18 @@ export function getOrCreateInjectable<T>(
392392

393393
if ((flags & (InjectFlags.Self | InjectFlags.Host)) === 0) {
394394
const moduleInjector = lView[INJECTOR];
395-
if (moduleInjector) {
396-
return moduleInjector.get(token, notFoundValue, flags & InjectFlags.Optional);
397-
} else {
398-
return injectRootLimpMode(token, notFoundValue, flags & InjectFlags.Optional);
395+
// switch to `injectInjectorOnly` implementation for module injector, since module injector
396+
// should not have access to Component/Directive DI scope (that may happen through
397+
// `directiveInject` implementation)
398+
const previousInjectImplementation = setInjectImplementation(undefined);
399+
try {
400+
if (moduleInjector) {
401+
return moduleInjector.get(token, notFoundValue, flags & InjectFlags.Optional);
402+
} else {
403+
return injectRootLimpMode(token, notFoundValue, flags & InjectFlags.Optional);
404+
}
405+
} finally {
406+
setInjectImplementation(previousInjectImplementation);
399407
}
400408
}
401409
if (flags & InjectFlags.Optional) {

packages/core/test/acceptance/di_spec.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
*/
88

99
import {CommonModule} from '@angular/common';
10-
import {ChangeDetectorRef, Component, Pipe, PipeTransform} from '@angular/core';
10+
import {ChangeDetectorRef, Component, Directive, Inject, LOCALE_ID, Optional, Pipe, PipeTransform, SkipSelf, ViewChild} from '@angular/core';
1111
import {ViewRef} from '@angular/core/src/render3/view_ref';
1212
import {TestBed} from '@angular/core/testing';
1313

14+
1415
describe('di', () => {
1516
describe('ChangeDetectorRef', () => {
1617
it('should inject host component ChangeDetectorRef into directives on templates', () => {
@@ -39,4 +40,94 @@ describe('di', () => {
3940
expect((pipeInstance !.cdr as ViewRef<MyApp>).context).toBe(fixture.componentInstance);
4041
});
4142
});
43+
44+
it('should not cause cyclic dependency if same token is requested in deps with @SkipSelf', () => {
45+
@Component({
46+
selector: 'my-comp',
47+
template: '...',
48+
providers: [{
49+
provide: LOCALE_ID,
50+
useFactory: () => 'ja-JP',
51+
// Note: `LOCALE_ID` is also provided within APPLICATION_MODULE_PROVIDERS, so we use it here
52+
// as a dep and making sure it doesn't cause cyclic dependency (since @SkipSelf is present)
53+
deps: [[new Inject(LOCALE_ID), new Optional(), new SkipSelf()]]
54+
}]
55+
})
56+
class MyComp {
57+
constructor(@Inject(LOCALE_ID) public localeId: string) {}
58+
}
59+
60+
TestBed.configureTestingModule({declarations: [MyComp]});
61+
const fixture = TestBed.createComponent(MyComp);
62+
fixture.detectChanges();
63+
expect(fixture.componentInstance.localeId).toBe('ja-JP');
64+
});
65+
66+
it('module-level deps should not access Component/Directive providers', () => {
67+
@Component({
68+
selector: 'my-comp',
69+
template: '...',
70+
providers: [{
71+
provide: 'LOCALE_ID_DEP', //
72+
useValue: 'LOCALE_ID_DEP_VALUE'
73+
}]
74+
})
75+
class MyComp {
76+
constructor(@Inject(LOCALE_ID) public localeId: string) {}
77+
}
78+
79+
TestBed.configureTestingModule({
80+
declarations: [MyComp],
81+
providers: [{
82+
provide: LOCALE_ID,
83+
// we expect `localeDepValue` to be undefined, since it's not provided at a module level
84+
useFactory: (localeDepValue: any) => localeDepValue || 'en-GB',
85+
deps: [[new Inject('LOCALE_ID_DEP'), new Optional()]]
86+
}]
87+
});
88+
const fixture = TestBed.createComponent(MyComp);
89+
fixture.detectChanges();
90+
expect(fixture.componentInstance.localeId).toBe('en-GB');
91+
});
92+
93+
it('should skip current level while retrieving tokens if @SkipSelf is defined', () => {
94+
@Component({
95+
selector: 'my-comp',
96+
template: '...',
97+
providers: [{provide: LOCALE_ID, useFactory: () => 'en-GB'}]
98+
})
99+
class MyComp {
100+
constructor(@SkipSelf() @Inject(LOCALE_ID) public localeId: string) {}
101+
}
102+
103+
TestBed.configureTestingModule({declarations: [MyComp]});
104+
const fixture = TestBed.createComponent(MyComp);
105+
fixture.detectChanges();
106+
// takes `LOCALE_ID` from module injector, since we skip Component level with @SkipSelf
107+
expect(fixture.componentInstance.localeId).toBe('en-US');
108+
});
109+
110+
it('should work when injecting dependency in Directives', () => {
111+
@Directive({
112+
selector: '[dir]', //
113+
providers: [{provide: LOCALE_ID, useValue: 'ja-JP'}]
114+
})
115+
class MyDir {
116+
constructor(@SkipSelf() @Inject(LOCALE_ID) public localeId: string) {}
117+
}
118+
@Component({
119+
selector: 'my-comp',
120+
template: '<div dir></div>',
121+
providers: [{provide: LOCALE_ID, useValue: 'en-GB'}]
122+
})
123+
class MyComp {
124+
@ViewChild(MyDir) myDir !: MyDir;
125+
constructor(@Inject(LOCALE_ID) public localeId: string) {}
126+
}
127+
128+
TestBed.configureTestingModule({declarations: [MyDir, MyComp, MyComp]});
129+
const fixture = TestBed.createComponent(MyComp);
130+
fixture.detectChanges();
131+
expect(fixture.componentInstance.myDir.localeId).toBe('en-GB');
132+
});
42133
});

0 commit comments

Comments
 (0)