Skip to content

Commit a7a14df

Browse files
alxhubatscott
authored andcommitted
feat(core): introduce EnvironmentInjector.runInContext API (#46653)
This commit introduces a new API on `EnvironmentInjector` to run a function in the context of the injector. This makes `inject()` available within the body of the function to inject dependencies. We expect this functionality to be very useful for designing ergonomic APIs both in and out of Angular, as it should allow for a smaller and more functional API style. Note that it's possible to implement nearly identical functionality in user code already today: ```typescript function runInContext<T>(injector: EnvironmentInjector, fn: () => T): T { const token = new InjectionToken<T>('TOKEN'); const tmpInjector = createEnvironmentInjector([ {provide: token, useFactory: () => fn()}, ], injector); return tmpInjector.get(token); } ``` The factory provider for `token` in this example runs in the context of the `tmpInjector`, giving it access to `inject` that can retrieve values from `injector` as well. This is nearly identical, although because of the child injector `self` and `skipSelf` injection options don't function correctly. PR Close #46653
1 parent 6f11a58 commit a7a14df

4 files changed

Lines changed: 121 additions & 1 deletion

File tree

goldens/public-api/core/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ export abstract class EnvironmentInjector implements Injector {
457457
abstract get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
458458
// @deprecated (undocumented)
459459
abstract get(token: any, notFoundValue?: any): any;
460+
abstract runInContext<ReturnT>(fn: () => ReturnT): ReturnT;
460461
}
461462

462463
// @public

packages/core/src/di/r3_injector.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,18 @@ export abstract class EnvironmentInjector implements Injector {
8989
*/
9090
abstract get(token: any, notFoundValue?: any): any;
9191

92+
/**
93+
* Runs the given function in the context of this `EnvironmentInjector`.
94+
*
95+
* Within the function's stack frame, `inject` can be used to inject dependencies from this
96+
* injector. Note that `inject` is only usable synchronously, and cannot be used in any
97+
* asynchronous callbacks or after any `await` points.
98+
*
99+
* @param fn the closure to be run in the context of this injector
100+
* @returns the return value of the function, if any
101+
*/
102+
abstract runInContext<ReturnT>(fn: () => ReturnT): ReturnT;
103+
92104
abstract destroy(): void;
93105

94106
/**
@@ -180,6 +192,19 @@ export class R3Injector extends EnvironmentInjector {
180192
this._onDestroyHooks.push(callback);
181193
}
182194

195+
override runInContext<ReturnT>(fn: () => ReturnT): ReturnT {
196+
this.assertNotDestroyed();
197+
198+
const previousInjector = setCurrentInjector(this);
199+
const previousInjectImplementation = setInjectImplementation(undefined);
200+
try {
201+
return fn();
202+
} finally {
203+
setCurrentInjector(previousInjector);
204+
setInjectImplementation(previousInjectImplementation);
205+
}
206+
}
207+
183208
override get<T>(
184209
token: ProviderToken<T>, notFoundValue: any = THROW_IF_NOT_FOUND,
185210
flags = InjectFlags.Default): T {

packages/core/src/render3/ng_module_ref.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
8686
return this._r3Injector.get(token, notFoundValue, injectFlags);
8787
}
8888

89+
runInContext<ReturnT>(fn: () => ReturnT): ReturnT {
90+
return this.injector.runInContext(fn);
91+
}
92+
8993
override destroy(): void {
9094
ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed');
9195
const injector = this._r3Injector;

packages/core/test/acceptance/environment_injector_spec.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Component, ComponentFactoryResolver, createEnvironmentInjector, ENVIRONMENT_INITIALIZER, EnvironmentInjector, InjectionToken, INJECTOR, Injector, NgModuleRef} from '@angular/core';
9+
import {Component, ComponentFactoryResolver, createEnvironmentInjector, ENVIRONMENT_INITIALIZER, EnvironmentInjector, inject, InjectFlags, InjectionToken, INJECTOR, Injector, NgModuleRef, ViewContainerRef} from '@angular/core';
1010
import {R3Injector} from '@angular/core/src/di/r3_injector';
1111
import {TestBed} from '@angular/core/testing';
1212

@@ -119,4 +119,94 @@ describe('environment injector', () => {
119119
});
120120
expect(injector.get(EnvScopedToken, false)).toBeTrue();
121121
});
122+
123+
describe('runInContext()', () => {
124+
it('should return the function\'s return value', () => {
125+
const injector = TestBed.inject(EnvironmentInjector);
126+
const returnValue = injector.runInContext(() => 3);
127+
expect(returnValue).toBe(3);
128+
});
129+
130+
it('should make inject() available', () => {
131+
const TOKEN = new InjectionToken<string>('TOKEN');
132+
const injector = createEnvironmentInjector(
133+
[{provide: TOKEN, useValue: 'from injector'}], TestBed.inject(EnvironmentInjector));
134+
135+
const result = injector.runInContext(() => inject(TOKEN));
136+
expect(result).toEqual('from injector');
137+
});
138+
139+
it('should properly clean up after the function returns', () => {
140+
const TOKEN = new InjectionToken<string>('TOKEN');
141+
const injector = TestBed.inject(EnvironmentInjector);
142+
injector.runInContext(() => {});
143+
expect(() => inject(TOKEN, InjectFlags.Optional)).toThrow();
144+
});
145+
146+
it('should properly clean up after the function throws', () => {
147+
const TOKEN = new InjectionToken<string>('TOKEN');
148+
const injector = TestBed.inject(EnvironmentInjector);
149+
expect(() => injector.runInContext(() => {
150+
throw new Error('crashes!');
151+
})).toThrow();
152+
expect(() => inject(TOKEN, InjectFlags.Optional)).toThrow();
153+
});
154+
155+
it('should set the correct inject implementation', () => {
156+
const TOKEN = new InjectionToken<string>('TOKEN', {
157+
providedIn: 'root',
158+
factory: () => 'from root',
159+
});
160+
161+
@Component({
162+
standalone: true,
163+
template: '',
164+
providers: [{provide: TOKEN, useValue: 'from component'}],
165+
})
166+
class TestCmp {
167+
envInjector = inject(EnvironmentInjector);
168+
169+
tokenFromComponent = inject(TOKEN);
170+
tokenFromEnvContext = this.envInjector.runInContext(() => inject(TOKEN));
171+
172+
// Attempt to inject ViewContainerRef within the environment injector's context. This should
173+
// not be available, so the result should be `null`.
174+
vcrFromEnvContext =
175+
this.envInjector.runInContext(() => inject(ViewContainerRef, InjectFlags.Optional));
176+
}
177+
178+
const instance = TestBed.createComponent(TestCmp).componentInstance;
179+
expect(instance.tokenFromComponent).toEqual('from component');
180+
expect(instance.tokenFromEnvContext).toEqual('from root');
181+
expect(instance.vcrFromEnvContext).toBeNull();
182+
});
183+
184+
it('should be reentrant', () => {
185+
const TOKEN = new InjectionToken<string>('TOKEN', {
186+
providedIn: 'root',
187+
factory: () => 'from root',
188+
});
189+
190+
const parentInjector = TestBed.inject(EnvironmentInjector);
191+
const childInjector =
192+
createEnvironmentInjector([{provide: TOKEN, useValue: 'from child'}], parentInjector);
193+
194+
const results = parentInjector.runInContext(() => {
195+
const fromParentBefore = inject(TOKEN);
196+
const fromChild = childInjector.runInContext(() => inject(TOKEN));
197+
const fromParentAfter = inject(TOKEN);
198+
return {fromParentBefore, fromChild, fromParentAfter};
199+
});
200+
201+
expect(results.fromParentBefore).toEqual('from root');
202+
expect(results.fromChild).toEqual('from child');
203+
expect(results.fromParentAfter).toEqual('from root');
204+
});
205+
206+
it('should not function on a destroyed injector', () => {
207+
const injector = createEnvironmentInjector([], TestBed.inject(EnvironmentInjector));
208+
injector.destroy();
209+
expect(() => injector.runInContext(() => {})).toThrow();
210+
});
211+
});
122212
});

0 commit comments

Comments
 (0)