Skip to content

Commit

Permalink
feat(core): introduce runInInjectionContext and deprecate prior ver…
Browse files Browse the repository at this point in the history
…sion (#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
  • Loading branch information
alxhub committed Mar 14, 2023
1 parent 3d2351c commit 0814f20
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 3 deletions.
10 changes: 9 additions & 1 deletion aio/content/guide/deprecations.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ v12 - v15
v13 - v16
v14 - v17
v15 - v18
v16 - v19
-->

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

### Deprecated features that can be removed in v18 or later

| Area | API or Feature | Deprecated in | May be removed in |
|:--- |:--- |:--- |:--- |
| `@angular/core` | `EnvironmentInjector.runInContext` | v16 | v18 |

### Deprecated features with no planned removal version

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

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

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

```
Expand Down
4 changes: 4 additions & 0 deletions goldens/public-api/core/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ export abstract class EnvironmentInjector implements Injector {
abstract get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
// @deprecated (undocumented)
abstract get(token: any, notFoundValue?: any): any;
// @deprecated
abstract runInContext<ReturnT>(fn: () => ReturnT): ReturnT;
}

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

// @public
export function runInInjectionContext<ReturnT>(injector: Injector, fn: () => ReturnT): ReturnT;

// @public
export abstract class Sanitizer {
// (undocumented)
Expand Down
39 changes: 39 additions & 0 deletions packages/core/src/di/contextual.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import type {Injector} from './injector';
import {setCurrentInjector} from './injector_compatibility';
import {setInjectImplementation} from './inject_switch';
import {R3Injector} from './r3_injector';

/**
* Runs the given function in the context of the given `Injector`.
*
* Within the function's stack frame, `inject` can be used to inject dependencies from the given
* `Injector`. Note that `inject` is only usable synchronously, and cannot be used in any
* asynchronous callbacks or after any `await` points.
*
* @param injector the injector which will satisfy calls to `inject` while `fn` is executing
* @param fn the closure to be run in the context of `injector`
* @returns the return value of the function, if any
* @publicApi
*/
export function runInInjectionContext<ReturnT>(injector: Injector, fn: () => ReturnT): ReturnT {
if (injector instanceof R3Injector) {
injector.assertNotDestroyed();
}

const prevInjector = setCurrentInjector(injector);
const previousInjectImplementation = setInjectImplementation(undefined);
try {
return fn();
} finally {
setCurrentInjector(prevInjector);
setInjectImplementation(previousInjectImplementation);
}
}
1 change: 1 addition & 0 deletions packages/core/src/di/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/

export * from './metadata';
export {runInInjectionContext} from './contextual';
export {InjectFlags} from './interface/injector';
export {ɵɵdefineInjectable, defineInjectable, ɵɵdefineInjector, InjectableType, InjectorType} from './interface/defs';
export {forwardRef, resolveForwardRef, ForwardRefFn} from './forward_ref';
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/di/r3_injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export abstract class EnvironmentInjector implements Injector {
*
* @param fn the closure to be run in the context of this injector
* @returns the return value of the function, if any
* @deprecated use the standalone function `runInInjectionContext` instead
*/
abstract runInContext<ReturnT>(fn: () => ReturnT): ReturnT;

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

private assertNotDestroyed(): void {
assertNotDestroyed(): void {
if (this._destroyed) {
throw new RuntimeError(
RuntimeErrorCode.INJECTOR_ALREADY_DESTROYED,
Expand Down
121 changes: 120 additions & 1 deletion packages/core/test/acceptance/di_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {CommonModule} from '@angular/common';
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';
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';
import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
Expand Down Expand Up @@ -3691,6 +3691,125 @@ describe('di', () => {
});
});

describe('runInInjectionContext', () => {
it('should return the function\'s return value', () => {
const injector = TestBed.inject(EnvironmentInjector);
const returnValue = runInInjectionContext(injector, () => 3);
expect(returnValue).toBe(3);
});

it('should work with an NgModuleRef injector', () => {
const ref = TestBed.inject(NgModuleRef);
const returnValue = runInInjectionContext(ref.injector, () => 3);
expect(returnValue).toBe(3);
});

it('should return correct injector reference', () => {
const ngModuleRef = TestBed.inject(NgModuleRef);
const ref1 = runInInjectionContext(ngModuleRef.injector, () => inject(Injector));
const ref2 = ngModuleRef.injector.get(Injector);
expect(ref1).toBe(ref2);
});

it('should make inject() available', () => {
const TOKEN = new InjectionToken<string>('TOKEN');
const injector = createEnvironmentInjector(
[{provide: TOKEN, useValue: 'from injector'}], TestBed.inject(EnvironmentInjector));

const result = runInInjectionContext(injector, () => inject(TOKEN));
expect(result).toEqual('from injector');
});

it('should properly clean up after the function returns', () => {
const TOKEN = new InjectionToken<string>('TOKEN');
const injector = TestBed.inject(EnvironmentInjector);
runInInjectionContext(injector, () => {});
expect(() => inject(TOKEN, InjectFlags.Optional)).toThrow();
});

it('should properly clean up after the function throws', () => {
const TOKEN = new InjectionToken<string>('TOKEN');
const injector = TestBed.inject(EnvironmentInjector);
expect(() => runInInjectionContext(injector, () => {
throw new Error('crashes!');
})).toThrow();
expect(() => inject(TOKEN, InjectFlags.Optional)).toThrow();
});

it('should set the correct inject implementation', () => {
const TOKEN = new InjectionToken<string>('TOKEN', {
providedIn: 'root',
factory: () => 'from root',
});

@Component({
standalone: true,
template: '',
providers: [{provide: TOKEN, useValue: 'from component'}],
})
class TestCmp {
envInjector = inject(EnvironmentInjector);

tokenFromComponent = inject(TOKEN);
tokenFromEnvContext = runInInjectionContext(this.envInjector, () => inject(TOKEN));

// Attempt to inject ViewContainerRef within the environment injector's context. This should
// not be available, so the result should be `null`.
vcrFromEnvContext = runInInjectionContext(
this.envInjector, () => inject(ViewContainerRef, InjectFlags.Optional));
}

const instance = TestBed.createComponent(TestCmp).componentInstance;
expect(instance.tokenFromComponent).toEqual('from component');
expect(instance.tokenFromEnvContext).toEqual('from root');
expect(instance.vcrFromEnvContext).toBeNull();
});

it('should support node injectors', () => {
@Component({
standalone: true,
template: '',
})
class TestCmp {
injector = inject(Injector);

vcrFromEnvContext =
runInInjectionContext(this.injector, () => inject(ViewContainerRef, {optional: true}));
}

const instance = TestBed.createComponent(TestCmp).componentInstance;
expect(instance.vcrFromEnvContext).not.toBeNull();
});

it('should be reentrant', () => {
const TOKEN = new InjectionToken<string>('TOKEN', {
providedIn: 'root',
factory: () => 'from root',
});

const parentInjector = TestBed.inject(EnvironmentInjector);
const childInjector =
createEnvironmentInjector([{provide: TOKEN, useValue: 'from child'}], parentInjector);

const results = runInInjectionContext(parentInjector, () => {
const fromParentBefore = inject(TOKEN);
const fromChild = runInInjectionContext(childInjector, () => inject(TOKEN));
const fromParentAfter = inject(TOKEN);
return {fromParentBefore, fromChild, fromParentAfter};
});

expect(results.fromParentBefore).toEqual('from root');
expect(results.fromChild).toEqual('from child');
expect(results.fromParentAfter).toEqual('from root');
});

it('should not function on a destroyed injector', () => {
const injector = createEnvironmentInjector([], TestBed.inject(EnvironmentInjector));
injector.destroy();
expect(() => runInInjectionContext(injector, () => {})).toThrow();
});
});

it('should be able to use Host in `useFactory` dependency config', () => {
// Scenario:
// ---------
Expand Down

0 comments on commit 0814f20

Please sign in to comment.