diff --git a/aio/content/guide/deprecations.md b/aio/content/guide/deprecations.md index fcc3bf33fc96b..de199704afb2a 100644 --- a/aio/content/guide/deprecations.md +++ b/aio/content/guide/deprecations.md @@ -36,6 +36,7 @@ v12 - v15 v13 - v16 v14 - v17 v15 - v18 +v16 - v19 --> ### Deprecated features that can be removed in v11 or later @@ -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 | @@ -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 | @@ -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: ``` diff --git a/goldens/public-api/core/index.md b/goldens/public-api/core/index.md index 1c3c2e9f0e494..d8f92aea4972b 100644 --- a/goldens/public-api/core/index.md +++ b/goldens/public-api/core/index.md @@ -528,6 +528,7 @@ export abstract class EnvironmentInjector implements Injector { abstract get(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags): T; // @deprecated (undocumented) abstract get(token: any, notFoundValue?: any): any; + // @deprecated abstract runInContext(fn: () => ReturnT): ReturnT; } @@ -1284,6 +1285,9 @@ export interface ResolvedReflectiveProvider { // @public export function resolveForwardRef(type: T): T; +// @public +export function runInInjectionContext(injector: Injector, fn: () => ReturnT): ReturnT; + // @public export abstract class Sanitizer { // (undocumented) diff --git a/packages/core/src/di/contextual.ts b/packages/core/src/di/contextual.ts new file mode 100644 index 0000000000000..da3f003b7394c --- /dev/null +++ b/packages/core/src/di/contextual.ts @@ -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(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); + } +} diff --git a/packages/core/src/di/index.ts b/packages/core/src/di/index.ts index 6e501ca23fa9c..68d1fd14aec65 100644 --- a/packages/core/src/di/index.ts +++ b/packages/core/src/di/index.ts @@ -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'; diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts index 8ceb5fe1a957f..e61bb1f5a6583 100644 --- a/packages/core/src/di/r3_injector.ts +++ b/packages/core/src/di/r3_injector.ts @@ -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(fn: () => ReturnT): ReturnT; @@ -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, diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index 747b21615d946..f661180c36fa9 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -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'; @@ -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('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('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('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('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('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: // ---------