Skip to content

Commit 0814f20

Browse files
committedMar 14, 2023
feat(core): introduce runInInjectionContext and deprecate prior version (#49396)
With the introduction of `EnvironmentInjector`, we added an operation to run a function with access to `inject` tokens from that injector. This operation only worked for `EnvironmentInjector`s and not for element/node injectors. This commit deprecates `EnvironmentInjector.runInContext` in favor of a standalone API `runInInjectionContext`, which supports any type of injector. DEPRECATED: `EnvironmentInjector.runInContext` is now deprecated, with `runInInjectionContext` functioning as a direct replacement: ```typescript // Previous method version (deprecated): envInjector.runInContext(fn); // New standalone function: runInInjectionContext(envInjector, fn); ``` PR Close #49396
1 parent 3d2351c commit 0814f20

File tree

6 files changed

+175
-3
lines changed

6 files changed

+175
-3
lines changed
 

‎aio/content/guide/deprecations.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ v12 - v15
3636
v13 - v16
3737
v14 - v17
3838
v15 - v18
39+
v16 - v19
3940
-->
4041

4142
### Deprecated features that can be removed in v11 or later
@@ -115,6 +116,12 @@ v15 - v18
115116
| `@angular/router` | [Router CanLoad guards](#router-can-load) | v15.1 | v17 |
116117
| `@angular/router` | [class and `InjectionToken` guards and resolvers](#router-class-and-injection-token-guards) | v15.2 | v17 |
117118

119+
### Deprecated features that can be removed in v18 or later
120+
121+
| Area | API or Feature | Deprecated in | May be removed in |
122+
|:--- |:--- |:--- |:--- |
123+
| `@angular/core` | `EnvironmentInjector.runInContext` | v16 | v18 |
124+
118125
### Deprecated features with no planned removal version
119126

120127
| Area | API or Feature | Deprecated in | May be removed in |
@@ -170,6 +177,7 @@ In the [API reference section](api) of this site, deprecated APIs are indicated
170177
| [`CompilerOptions.useJit and CompilerOptions.missingTranslation config options`](api/core/CompilerOptions) | none | v13 | Since Ivy, those config options are unused, passing them has no effect. |
171178
| [`providedIn`](api/core/Injectable#providedIn) with NgModule | Prefer `'root'` providers, or use NgModule `providers` if scoping to an NgModule is necessary | v15 | none |
172179
| [`providedIn: 'any'`](api/core/Injectable#providedIn) | none | v15 | This option has confusing semantics and nearly zero usage. |
180+
| [`EnvironmentInjector.runInContext`](api/core/EnvironmentInjector#runInContext) | `runInInjectionContext` | v16 | `runInInjectionContext` is a more flexible operation which supports element injectors as well |
173181

174182
<a id="testing"></a>
175183

@@ -377,7 +385,7 @@ be converted to functions by instead using `inject` to get dependencies.
377385
For testing a function `canActivate` guard, using `TestBed` and `TestBed.runInInjectionContext` is recommended.
378386
Test mocks and stubs can be provided through DI with `{provide: X, useValue: StubX}`.
379387
Functional guards can also be written in a way that's either testable with
380-
`runInContext` or by passing mock implementations of dependencies.
388+
`runInInjectionContext` or by passing mock implementations of dependencies.
381389
For example:
382390

383391
```

‎goldens/public-api/core/index.md

+4
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@ export abstract class EnvironmentInjector implements Injector {
528528
abstract get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
529529
// @deprecated (undocumented)
530530
abstract get(token: any, notFoundValue?: any): any;
531+
// @deprecated
531532
abstract runInContext<ReturnT>(fn: () => ReturnT): ReturnT;
532533
}
533534

@@ -1284,6 +1285,9 @@ export interface ResolvedReflectiveProvider {
12841285
// @public
12851286
export function resolveForwardRef<T>(type: T): T;
12861287

1288+
// @public
1289+
export function runInInjectionContext<ReturnT>(injector: Injector, fn: () => ReturnT): ReturnT;
1290+
12871291
// @public
12881292
export abstract class Sanitizer {
12891293
// (undocumented)

‎packages/core/src/di/contextual.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import type {Injector} from './injector';
10+
import {setCurrentInjector} from './injector_compatibility';
11+
import {setInjectImplementation} from './inject_switch';
12+
import {R3Injector} from './r3_injector';
13+
14+
/**
15+
* Runs the given function in the context of the given `Injector`.
16+
*
17+
* Within the function's stack frame, `inject` can be used to inject dependencies from the given
18+
* `Injector`. Note that `inject` is only usable synchronously, and cannot be used in any
19+
* asynchronous callbacks or after any `await` points.
20+
*
21+
* @param injector the injector which will satisfy calls to `inject` while `fn` is executing
22+
* @param fn the closure to be run in the context of `injector`
23+
* @returns the return value of the function, if any
24+
* @publicApi
25+
*/
26+
export function runInInjectionContext<ReturnT>(injector: Injector, fn: () => ReturnT): ReturnT {
27+
if (injector instanceof R3Injector) {
28+
injector.assertNotDestroyed();
29+
}
30+
31+
const prevInjector = setCurrentInjector(injector);
32+
const previousInjectImplementation = setInjectImplementation(undefined);
33+
try {
34+
return fn();
35+
} finally {
36+
setCurrentInjector(prevInjector);
37+
setInjectImplementation(previousInjectImplementation);
38+
}
39+
}

‎packages/core/src/di/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414

1515
export * from './metadata';
16+
export {runInInjectionContext} from './contextual';
1617
export {InjectFlags} from './interface/injector';
1718
export {ɵɵdefineInjectable, defineInjectable, ɵɵdefineInjector, InjectableType, InjectorType} from './interface/defs';
1819
export {forwardRef, resolveForwardRef, ForwardRefFn} from './forward_ref';

‎packages/core/src/di/r3_injector.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export abstract class EnvironmentInjector implements Injector {
118118
*
119119
* @param fn the closure to be run in the context of this injector
120120
* @returns the return value of the function, if any
121+
* @deprecated use the standalone function `runInInjectionContext` instead
121122
*/
122123
abstract runInContext<ReturnT>(fn: () => ReturnT): ReturnT;
123124

@@ -327,7 +328,7 @@ export class R3Injector extends EnvironmentInjector {
327328
return `R3Injector[${tokens.join(', ')}]`;
328329
}
329330

330-
private assertNotDestroyed(): void {
331+
assertNotDestroyed(): void {
331332
if (this._destroyed) {
332333
throw new RuntimeError(
333334
RuntimeErrorCode.INJECTOR_ALREADY_DESTROYED,

‎packages/core/test/acceptance/di_spec.ts

+120-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, createEnvironmentInjector, createNgModule, Directive, ElementRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EventEmitter, forwardRef, Host, HostBinding, ImportedNgModuleProviders, importProvidersFrom, ImportProvidersSource, inject, Inject, Injectable, InjectFlags, InjectionToken, InjectOptions, INJECTOR, Injector, Input, LOCALE_ID, makeEnvironmentProviders, ModuleWithProviders, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ViewRef, ɵcreateInjector as createInjector, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵINJECTOR_SCOPE, ɵInternalEnvironmentProviders as InternalEnvironmentProviders} from '@angular/core';
10+
import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, createEnvironmentInjector, createNgModule, Directive, ElementRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EventEmitter, forwardRef, Host, HostBinding, ImportedNgModuleProviders, importProvidersFrom, ImportProvidersSource, inject, Inject, Injectable, InjectFlags, InjectionToken, InjectOptions, INJECTOR, Injector, Input, LOCALE_ID, makeEnvironmentProviders, ModuleWithProviders, NgModule, NgModuleRef, NgZone, Optional, Output, Pipe, PipeTransform, Provider, runInInjectionContext, Self, SkipSelf, TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ViewRef, ɵcreateInjector as createInjector, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵINJECTOR_SCOPE, ɵInternalEnvironmentProviders as InternalEnvironmentProviders} from '@angular/core';
1111
import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref';
1212
import {TestBed} from '@angular/core/testing';
1313
import {By} from '@angular/platform-browser';
@@ -3691,6 +3691,125 @@ describe('di', () => {
36913691
});
36923692
});
36933693

3694+
describe('runInInjectionContext', () => {
3695+
it('should return the function\'s return value', () => {
3696+
const injector = TestBed.inject(EnvironmentInjector);
3697+
const returnValue = runInInjectionContext(injector, () => 3);
3698+
expect(returnValue).toBe(3);
3699+
});
3700+
3701+
it('should work with an NgModuleRef injector', () => {
3702+
const ref = TestBed.inject(NgModuleRef);
3703+
const returnValue = runInInjectionContext(ref.injector, () => 3);
3704+
expect(returnValue).toBe(3);
3705+
});
3706+
3707+
it('should return correct injector reference', () => {
3708+
const ngModuleRef = TestBed.inject(NgModuleRef);
3709+
const ref1 = runInInjectionContext(ngModuleRef.injector, () => inject(Injector));
3710+
const ref2 = ngModuleRef.injector.get(Injector);
3711+
expect(ref1).toBe(ref2);
3712+
});
3713+
3714+
it('should make inject() available', () => {
3715+
const TOKEN = new InjectionToken<string>('TOKEN');
3716+
const injector = createEnvironmentInjector(
3717+
[{provide: TOKEN, useValue: 'from injector'}], TestBed.inject(EnvironmentInjector));
3718+
3719+
const result = runInInjectionContext(injector, () => inject(TOKEN));
3720+
expect(result).toEqual('from injector');
3721+
});
3722+
3723+
it('should properly clean up after the function returns', () => {
3724+
const TOKEN = new InjectionToken<string>('TOKEN');
3725+
const injector = TestBed.inject(EnvironmentInjector);
3726+
runInInjectionContext(injector, () => {});
3727+
expect(() => inject(TOKEN, InjectFlags.Optional)).toThrow();
3728+
});
3729+
3730+
it('should properly clean up after the function throws', () => {
3731+
const TOKEN = new InjectionToken<string>('TOKEN');
3732+
const injector = TestBed.inject(EnvironmentInjector);
3733+
expect(() => runInInjectionContext(injector, () => {
3734+
throw new Error('crashes!');
3735+
})).toThrow();
3736+
expect(() => inject(TOKEN, InjectFlags.Optional)).toThrow();
3737+
});
3738+
3739+
it('should set the correct inject implementation', () => {
3740+
const TOKEN = new InjectionToken<string>('TOKEN', {
3741+
providedIn: 'root',
3742+
factory: () => 'from root',
3743+
});
3744+
3745+
@Component({
3746+
standalone: true,
3747+
template: '',
3748+
providers: [{provide: TOKEN, useValue: 'from component'}],
3749+
})
3750+
class TestCmp {
3751+
envInjector = inject(EnvironmentInjector);
3752+
3753+
tokenFromComponent = inject(TOKEN);
3754+
tokenFromEnvContext = runInInjectionContext(this.envInjector, () => inject(TOKEN));
3755+
3756+
// Attempt to inject ViewContainerRef within the environment injector's context. This should
3757+
// not be available, so the result should be `null`.
3758+
vcrFromEnvContext = runInInjectionContext(
3759+
this.envInjector, () => inject(ViewContainerRef, InjectFlags.Optional));
3760+
}
3761+
3762+
const instance = TestBed.createComponent(TestCmp).componentInstance;
3763+
expect(instance.tokenFromComponent).toEqual('from component');
3764+
expect(instance.tokenFromEnvContext).toEqual('from root');
3765+
expect(instance.vcrFromEnvContext).toBeNull();
3766+
});
3767+
3768+
it('should support node injectors', () => {
3769+
@Component({
3770+
standalone: true,
3771+
template: '',
3772+
})
3773+
class TestCmp {
3774+
injector = inject(Injector);
3775+
3776+
vcrFromEnvContext =
3777+
runInInjectionContext(this.injector, () => inject(ViewContainerRef, {optional: true}));
3778+
}
3779+
3780+
const instance = TestBed.createComponent(TestCmp).componentInstance;
3781+
expect(instance.vcrFromEnvContext).not.toBeNull();
3782+
});
3783+
3784+
it('should be reentrant', () => {
3785+
const TOKEN = new InjectionToken<string>('TOKEN', {
3786+
providedIn: 'root',
3787+
factory: () => 'from root',
3788+
});
3789+
3790+
const parentInjector = TestBed.inject(EnvironmentInjector);
3791+
const childInjector =
3792+
createEnvironmentInjector([{provide: TOKEN, useValue: 'from child'}], parentInjector);
3793+
3794+
const results = runInInjectionContext(parentInjector, () => {
3795+
const fromParentBefore = inject(TOKEN);
3796+
const fromChild = runInInjectionContext(childInjector, () => inject(TOKEN));
3797+
const fromParentAfter = inject(TOKEN);
3798+
return {fromParentBefore, fromChild, fromParentAfter};
3799+
});
3800+
3801+
expect(results.fromParentBefore).toEqual('from root');
3802+
expect(results.fromChild).toEqual('from child');
3803+
expect(results.fromParentAfter).toEqual('from root');
3804+
});
3805+
3806+
it('should not function on a destroyed injector', () => {
3807+
const injector = createEnvironmentInjector([], TestBed.inject(EnvironmentInjector));
3808+
injector.destroy();
3809+
expect(() => runInInjectionContext(injector, () => {})).toThrow();
3810+
});
3811+
});
3812+
36943813
it('should be able to use Host in `useFactory` dependency config', () => {
36953814
// Scenario:
36963815
// ---------

0 commit comments

Comments
 (0)
Please sign in to comment.