diff --git a/goldens/public-api/core/index.md b/goldens/public-api/core/index.md index 62a66ad47bafe..a1abe016bddcb 100644 --- a/goldens/public-api/core/index.md +++ b/goldens/public-api/core/index.md @@ -492,6 +492,8 @@ export const ENVIRONMENT_INITIALIZER: InjectionToken<() => void>; export abstract class EnvironmentInjector implements Injector { // (undocumented) abstract destroy(): void; + abstract get(token: ProviderToken, notFoundValue?: T, options?: InjectOptions): T; + // @deprecated abstract get(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags): T; // @deprecated (undocumented) abstract get(token: any, notFoundValue?: any): any; @@ -731,6 +733,8 @@ export abstract class Injector { parent?: Injector; name?: string; }): Injector; + abstract get(token: ProviderToken, notFoundValue?: T, options?: InjectOptions | InjectFlags): T; + // @deprecated abstract get(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags): T; // @deprecated (undocumented) abstract get(token: any, notFoundValue?: any): any; diff --git a/packages/core/src/di/index.ts b/packages/core/src/di/index.ts index cab939278b004..c6948e4dd0334 100644 --- a/packages/core/src/di/index.ts +++ b/packages/core/src/di/index.ts @@ -22,7 +22,8 @@ export {EnvironmentInjector} from './r3_injector'; export {importProvidersFrom, ImportProvidersSource} from './provider_collection'; export {ENVIRONMENT_INITIALIZER} from './initializer_token'; export {ProviderToken} from './provider_token'; -export {ɵɵinject, inject, InjectOptions, ɵɵinvalidFactoryDep} from './injector_compatibility'; +export {ɵɵinject, inject, ɵɵinvalidFactoryDep} from './injector_compatibility'; +export {InjectOptions} from './interface/injector'; export {INJECTOR} from './injector_token'; export {ReflectiveInjector} from './reflective_injector'; export {ClassProvider, ModuleWithProviders, ClassSansProvider, ImportedNgModuleProviders, ConstructorProvider, ConstructorSansProvider, ExistingProvider, ExistingSansProvider, FactoryProvider, FactorySansProvider, Provider, StaticClassProvider, StaticClassSansProvider, StaticProvider, TypeProvider, ValueProvider, ValueSansProvider} from './interface/provider'; diff --git a/packages/core/src/di/injector.ts b/packages/core/src/di/injector.ts index b1e3228759874..dc73739dff342 100644 --- a/packages/core/src/di/injector.ts +++ b/packages/core/src/di/injector.ts @@ -12,7 +12,7 @@ import {THROW_IF_NOT_FOUND, ɵɵinject} from './injector_compatibility'; import {InjectorMarkers} from './injector_marker'; import {INJECTOR} from './injector_token'; import {ɵɵdefineInjectable} from './interface/defs'; -import {InjectFlags} from './interface/injector'; +import {InjectFlags, InjectOptions} from './interface/injector'; import {StaticProvider} from './interface/provider'; import {NullInjector} from './null_injector'; import {ProviderToken} from './provider_token'; @@ -50,6 +50,14 @@ export abstract class Injector { * @returns The instance from the injector if defined, otherwise the `notFoundValue`. * @throws When the `notFoundValue` is `undefined` or `Injector.THROW_IF_NOT_FOUND`. */ + abstract get(token: ProviderToken, notFoundValue?: T, options?: InjectOptions|InjectFlags): + T; + /** + * Retrieves an instance from the injector based on the provided token. + * @returns The instance from the injector if defined, otherwise the `notFoundValue`. + * @throws When the `notFoundValue` is `undefined` or `Injector.THROW_IF_NOT_FOUND`. + * @deprecated use object-based flags (`InjectOptions`) instead. + */ abstract get(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags): T; /** * @deprecated from v4.0.0 use ProviderToken diff --git a/packages/core/src/di/injector_compatibility.ts b/packages/core/src/di/injector_compatibility.ts index fee67983f05c8..419dbff252d14 100644 --- a/packages/core/src/di/injector_compatibility.ts +++ b/packages/core/src/di/injector_compatibility.ts @@ -15,7 +15,7 @@ import {stringify} from '../util/stringify'; import {resolveForwardRef} from './forward_ref'; import {getInjectImplementation, injectRootLimpMode} from './inject_switch'; import {Injector} from './injector'; -import {DecoratorFlags, InjectFlags, InternalInjectFlags} from './interface/injector'; +import {DecoratorFlags, InjectFlags, InjectOptions, InternalInjectFlags} from './interface/injector'; import {ProviderToken} from './provider_token'; @@ -102,35 +102,6 @@ Please check that 1) the type for the parameter at index ${ index} is correct and 2) the correct Angular decorators are defined for this class and its ancestors.`); } -/** - * Type of the options argument to `inject`. - * - * @publicApi - */ -export interface InjectOptions { - /** - * Use optional injection, and return `null` if the requested token is not found. - */ - optional?: boolean; - - /** - * Start injection at the parent of the current injector. - */ - skipSelf?: boolean; - - /** - * Only query the current injector for the token, and don't fall back to the parent injector if - * it's not found. - */ - self?: boolean; - - /** - * Stop injection at the host component's injector. Only relevant when injecting from an element - * injector, and a no-op for environment injectors. - */ - host?: boolean; -} - /** * @param token A token that represents a dependency that should be injected. * @returns the injected value if operation is successful, `null` otherwise. @@ -240,17 +211,24 @@ export function inject(token: ProviderToken, options: InjectOptions): T|nu */ export function inject( token: ProviderToken, flags: InjectFlags|InjectOptions = InjectFlags.Default): T|null { - if (typeof flags !== 'number') { - // While TypeScript doesn't accept it without a cast, bitwise OR with false-y values in - // JavaScript is a no-op. We can use that for a very codesize-efficient conversion from - // `InjectOptions` to `InjectFlags`. - flags = (InternalInjectFlags.Default | // comment to force a line break in the formatter - ((flags.optional && InternalInjectFlags.Optional) as number) | - ((flags.host && InternalInjectFlags.Host) as number) | - ((flags.self && InternalInjectFlags.Self) as number) | - ((flags.skipSelf && InternalInjectFlags.SkipSelf) as number)) as InjectFlags; + return ɵɵinject(token, convertToBitFlags(flags)); +} + +// Converts object-based DI flags (`InjectOptions`) to bit flags (`InjectFlags`). +export function convertToBitFlags(flags: InjectOptions|InjectFlags|undefined): InjectFlags| + undefined { + if (typeof flags === 'undefined' || typeof flags === 'number') { + return flags; } - return ɵɵinject(token, flags); + + // While TypeScript doesn't accept it without a cast, bitwise OR with false-y values in + // JavaScript is a no-op. We can use that for a very codesize-efficient conversion from + // `InjectOptions` to `InjectFlags`. + return (InternalInjectFlags.Default | // comment to force a line break in the formatter + ((flags.optional && InternalInjectFlags.Optional) as number) | + ((flags.host && InternalInjectFlags.Host) as number) | + ((flags.self && InternalInjectFlags.Self) as number) | + ((flags.skipSelf && InternalInjectFlags.SkipSelf) as number)) as InjectFlags; } export function injectArgs(types: (ProviderToken|any[])[]): any[] { diff --git a/packages/core/src/di/interface/injector.ts b/packages/core/src/di/interface/injector.ts index dd63971865d2d..101f3284d2aa0 100644 --- a/packages/core/src/di/interface/injector.ts +++ b/packages/core/src/di/interface/injector.ts @@ -80,3 +80,32 @@ export const enum InternalInjectFlags { */ ForPipe = 0b10000, } + +/** + * Type of the options argument to `inject`. + * + * @publicApi + */ +export interface InjectOptions { + /** + * Use optional injection, and return `null` if the requested token is not found. + */ + optional?: boolean; + + /** + * Start injection at the parent of the current injector. + */ + skipSelf?: boolean; + + /** + * Only query the current injector for the token, and don't fall back to the parent injector if + * it's not found. + */ + self?: boolean; + + /** + * Stop injection at the host component's injector. Only relevant when injecting from an element + * injector, and a no-op for environment injectors. + */ + host?: boolean; +} diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts index 08d2ab7a4de3a..9cb2c5469de74 100644 --- a/packages/core/src/di/r3_injector.ts +++ b/packages/core/src/di/r3_injector.ts @@ -23,14 +23,14 @@ import {ENVIRONMENT_INITIALIZER} from './initializer_token'; import {setInjectImplementation} from './inject_switch'; import {InjectionToken} from './injection_token'; import {Injector} from './injector'; -import {catchInjectorError, injectArgs, NG_TEMP_TOKEN_PATH, setCurrentInjector, THROW_IF_NOT_FOUND, ɵɵinject} from './injector_compatibility'; +import {catchInjectorError, convertToBitFlags, injectArgs, NG_TEMP_TOKEN_PATH, setCurrentInjector, THROW_IF_NOT_FOUND, ɵɵinject} from './injector_compatibility'; import {INJECTOR} from './injector_token'; import {getInheritedInjectableDef, getInjectableDef, InjectorType, ɵɵInjectableDeclaration} from './interface/defs'; -import {InjectFlags} from './interface/injector'; +import {InjectFlags, InjectOptions} from './interface/injector'; import {ClassProvider, ConstructorProvider, ImportedNgModuleProviders, Provider, StaticClassProvider} from './interface/provider'; import {INJECTOR_DEF_TYPES} from './internal_tokens'; import {NullInjector} from './null_injector'; -import {importProvidersFrom, isExistingProvider, isFactoryProvider, isTypeProvider, isValueProvider, SingleProvider} from './provider_collection'; +import {isExistingProvider, isFactoryProvider, isTypeProvider, isValueProvider, SingleProvider} from './provider_collection'; import {ProviderToken} from './provider_token'; import {INJECTOR_SCOPE, InjectorScope} from './scope'; @@ -82,6 +82,13 @@ export abstract class EnvironmentInjector implements Injector { * @returns The instance from the injector if defined, otherwise the `notFoundValue`. * @throws When the `notFoundValue` is `undefined` or `Injector.THROW_IF_NOT_FOUND`. */ + abstract get(token: ProviderToken, notFoundValue?: T, options?: InjectOptions): T; + /** + * Retrieves an instance from the injector based on the provided token. + * @returns The instance from the injector if defined, otherwise the `notFoundValue`. + * @throws When the `notFoundValue` is `undefined` or `Injector.THROW_IF_NOT_FOUND`. + * @deprecated use object-based flags (`InjectOptions`) instead. + */ abstract get(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags): T; /** * @deprecated from v4.0.0 use ProviderToken @@ -207,8 +214,10 @@ export class R3Injector extends EnvironmentInjector { override get( token: ProviderToken, notFoundValue: any = THROW_IF_NOT_FOUND, - flags = InjectFlags.Default): T { + flags: InjectFlags|InjectOptions = InjectFlags.Default): T { this.assertNotDestroyed(); + flags = convertToBitFlags(flags) as InjectFlags; + // Set the injection context. const previousInjector = setCurrentInjector(this); const previousInjectImplementation = setInjectImplementation(undefined); diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 818fa2e170e54..dac35799e2a2d 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -8,7 +8,8 @@ import {ChangeDetectorRef} from '../change_detection/change_detector_ref'; import {Injector} from '../di/injector'; -import {InjectFlags} from '../di/interface/injector'; +import {convertToBitFlags} from '../di/injector_compatibility'; +import {InjectFlags, InjectOptions} from '../di/interface/injector'; import {ProviderToken} from '../di/provider_token'; import {EnvironmentInjector} from '../di/r3_injector'; import {RuntimeError, RuntimeErrorCode} from '../errors'; @@ -83,7 +84,8 @@ function getNamespace(elementName: string): string|null { class ChainedInjector implements Injector { constructor(private injector: Injector, private parentInjector: Injector) {} - get(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags): T { + get(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags|InjectOptions): T { + flags = convertToBitFlags(flags); const value = this.injector.get( token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR, flags); diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 713706727737f..1d7b95ce6a398 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -9,8 +9,9 @@ import {isForwardRef, resolveForwardRef} from '../di/forward_ref'; import {injectRootLimpMode, setInjectImplementation} from '../di/inject_switch'; import {Injector} from '../di/injector'; +import {convertToBitFlags} from '../di/injector_compatibility'; import {InjectorMarkers} from '../di/injector_marker'; -import {InjectFlags} from '../di/interface/injector'; +import {InjectFlags, InjectOptions} from '../di/interface/injector'; import {ProviderToken} from '../di/provider_token'; import {Type} from '../interface/type'; import {assertDefined, assertEqual, assertIndexInRange} from '../util/assert'; @@ -704,8 +705,9 @@ export class NodeInjector implements Injector { private _tNode: TElementNode|TContainerNode|TElementContainerNode|null, private _lView: LView) {} - get(token: any, notFoundValue?: any, flags?: InjectFlags): any { - return getOrCreateInjectable(this._tNode, this._lView, token, flags, notFoundValue); + get(token: any, notFoundValue?: any, flags?: InjectFlags|InjectOptions): any { + return getOrCreateInjectable( + this._tNode, this._lView, token, convertToBitFlags(flags), notFoundValue); } } diff --git a/packages/core/src/render3/ng_module_ref.ts b/packages/core/src/render3/ng_module_ref.ts index eaa98b3d3b97e..5eb7b722157b3 100644 --- a/packages/core/src/render3/ng_module_ref.ts +++ b/packages/core/src/render3/ng_module_ref.ts @@ -8,8 +8,6 @@ import {createInjectorWithoutInjectorInstances} from '../di/create_injector'; import {Injector} from '../di/injector'; -import {INJECTOR} from '../di/injector_token'; -import {InjectFlags} from '../di/interface/injector'; import {ImportedNgModuleProviders, Provider} from '../di/interface/provider'; import {EnvironmentInjector, getNullInjector, R3Injector} from '../di/r3_injector'; import {Type} from '../interface/type'; diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index 642981f24224e..dc33e72548422 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -3444,6 +3444,117 @@ describe('di', () => { }); }); + describe('injection flags', () => { + describe('represented as an options object argument', () => { + it('should be able to optionally inject a service', () => { + const TOKEN = new InjectionToken('TOKEN'); + + @Component({ + standalone: true, + template: '', + }) + class TestCmp { + nodeInjector = inject(Injector); + envInjector = inject(EnvironmentInjector); + } + + const {nodeInjector, envInjector} = TestBed.createComponent(TestCmp).componentInstance; + + expect(nodeInjector.get(TOKEN, undefined, {optional: true})).toBeNull(); + expect(nodeInjector.get(TOKEN, undefined, InjectFlags.Optional)).toBeNull(); + + expect(envInjector.get(TOKEN, undefined, {optional: true})).toBeNull(); + expect(envInjector.get(TOKEN, undefined, InjectFlags.Optional)).toBeNull(); + }); + + it('should be able to use skipSelf injection in NodeInjector', () => { + const TOKEN = new InjectionToken('TOKEN', { + providedIn: 'root', + factory: () => 'from root', + }); + @Component({ + standalone: true, + template: '', + providers: [{provide: TOKEN, useValue: 'from component'}], + }) + class TestCmp { + nodeInjector = inject(Injector); + } + + const {nodeInjector} = TestBed.createComponent(TestCmp).componentInstance; + expect(nodeInjector.get(TOKEN, undefined, {skipSelf: true})).toEqual('from root'); + }); + + it('should be able to use skipSelf injection in EnvironmentInjector', () => { + const TOKEN = new InjectionToken('TOKEN'); + const parent = TestBed.inject(EnvironmentInjector); + const root = createEnvironmentInjector([{provide: TOKEN, useValue: 'from root'}], parent); + const child = createEnvironmentInjector([{provide: TOKEN, useValue: 'from child'}], root); + + expect(child.get(TOKEN)).toEqual('from child'); + expect(child.get(TOKEN, undefined, {skipSelf: true})).toEqual('from root'); + expect(child.get(TOKEN, undefined, InjectFlags.SkipSelf)).toEqual('from root'); + }); + + it('should be able to use self injection in NodeInjector', () => { + const TOKEN = new InjectionToken('TOKEN', { + providedIn: 'root', + factory: () => 'from root', + }); + + @Component({ + standalone: true, + template: '', + }) + class TestCmp { + nodeInjector = inject(Injector); + } + + const {nodeInjector} = TestBed.createComponent(TestCmp).componentInstance; + expect(nodeInjector.get(TOKEN, undefined, {self: true, optional: true})).toBeNull(); + }); + + it('should be able to use self injection in EnvironmentInjector', () => { + const TOKEN = new InjectionToken('TOKEN'); + const parent = TestBed.inject(EnvironmentInjector); + const root = createEnvironmentInjector([{provide: TOKEN, useValue: 'from root'}], parent); + const child = createEnvironmentInjector([], root); + + expect(child.get(TOKEN, undefined, {self: true, optional: true})).toBeNull(); + expect(child.get(TOKEN, undefined, InjectFlags.Self | InjectFlags.Optional)).toBeNull(); + }); + + it('should be able to use host injection', () => { + const TOKEN = new InjectionToken('TOKEN'); + + @Component({ + standalone: true, + selector: 'child', + template: '{{ a }}|{{ b }}', + }) + class ChildCmp { + nodeInjector = inject(Injector); + a = this.nodeInjector.get(TOKEN, 'not found', {host: true, optional: true}); + b = this.nodeInjector.get(TOKEN, 'not found', InjectFlags.Host|InjectFlags.Optional); + } + + @Component({ + standalone: true, + imports: [ChildCmp], + template: '', + providers: [{provide: TOKEN, useValue: 'from parent'}], + encapsulation: ViewEncapsulation.None, + }) + class ParentCmp { + } + + const fixture = TestBed.createComponent(ParentCmp); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('not found|not found'); + }); + }); + }); + it('should be able to use Host in `useFactory` dependency config', () => { // Scenario: // ---------