Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): update Injector.get and TestBed.inject to support object-based flags #46761

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions aio/tests/e2e/src/api-pages.e2e-spec.ts
Expand Up @@ -76,11 +76,11 @@ describe('Api pages', () => {

it('should show all overloads of interface methods', async () => {
await page.navigateTo('api/core/testing/TestBed');
expect(await (await page.getInstanceMethodOverloads('inject')).length).toEqual(2);
expect(await (await page.getInstanceMethodOverloads('inject')).length).toEqual(4);
});

it('should show all overloads of pseudo-class methods', async () => {
await page.navigateTo('api/core/testing/TestBed');
expect(await (await page.getInstanceMethodOverloads('inject')).length).toEqual(2);
expect(await (await page.getInstanceMethodOverloads('inject')).length).toEqual(4);
});
});
12 changes: 12 additions & 0 deletions goldens/public-api/core/index.md
Expand Up @@ -492,6 +492,12 @@ export const ENVIRONMENT_INITIALIZER: InjectionToken<() => void>;
export abstract class EnvironmentInjector implements Injector {
// (undocumented)
abstract destroy(): void;
abstract get<T>(token: ProviderToken<T>, notFoundValue: undefined, options: InjectOptions & {
optional?: false;
}): T;
abstract get<T>(token: ProviderToken<T>, notFoundValue: null | undefined, options: InjectOptions): T | null;
abstract get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions): T;
// @deprecated
abstract get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
// @deprecated (undocumented)
abstract get(token: any, notFoundValue?: any): any;
Expand Down Expand Up @@ -731,6 +737,12 @@ export abstract class Injector {
parent?: Injector;
name?: string;
}): Injector;
abstract get<T>(token: ProviderToken<T>, notFoundValue: undefined, options: InjectOptions & {
optional?: false;
}): T;
abstract get<T>(token: ProviderToken<T>, notFoundValue: null | undefined, options: InjectOptions): T | null;
abstract get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions | InjectFlags): T;
// @deprecated
abstract get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
// @deprecated (undocumented)
abstract get(token: any, notFoundValue?: any): any;
Expand Down
11 changes: 10 additions & 1 deletion goldens/public-api/core/testing/index.md
Expand Up @@ -12,6 +12,7 @@ import { Directive } from '@angular/core';
import { ElementRef } from '@angular/core';
import { InjectFlags } from '@angular/core';
import { InjectionToken } from '@angular/core';
import { InjectOptions } from '@angular/core';
import { NgModule } from '@angular/core';
import { NgZone } from '@angular/core';
import { Pipe } from '@angular/core';
Expand Down Expand Up @@ -115,8 +116,16 @@ export interface TestBed {
get(token: any, notFoundValue?: any): any;
initTestEnvironment(ngModule: Type<any> | Type<any>[], platform: PlatformRef, options?: TestEnvironmentOptions): void;
// (undocumented)
inject<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
inject<T>(token: ProviderToken<T>, notFoundValue: undefined, options: InjectOptions & {
optional?: false;
}): T;
// (undocumented)
inject<T>(token: ProviderToken<T>, notFoundValue: null | undefined, options: InjectOptions): T | null;
// (undocumented)
inject<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions): T;
// @deprecated (undocumented)
inject<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
// @deprecated (undocumented)
inject<T>(token: ProviderToken<T>, notFoundValue: null, flags?: InjectFlags): T | null;
// (undocumented)
get ngModule(): Type<any> | Type<any>[];
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core_private_export.ts
Expand Up @@ -12,7 +12,7 @@ export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffe
export {ChangeDetectorStatus as ɵChangeDetectorStatus, isDefaultChangeDetectionStrategy as ɵisDefaultChangeDetectionStrategy} from './change_detection/constants';
export {Console as ɵConsole} from './console';
export {getDebugNodeR2 as ɵgetDebugNodeR2} from './debug/debug_node';
export {setCurrentInjector as ɵsetCurrentInjector} from './di/injector_compatibility';
export {convertToBitFlags as ɵconvertToBitFlags, setCurrentInjector as ɵsetCurrentInjector} from './di/injector_compatibility';
export {getInjectableDef as ɵgetInjectableDef, ɵɵInjectableDeclaration, ɵɵInjectorDef} from './di/interface/defs';
export {INJECTOR_SCOPE as ɵINJECTOR_SCOPE} from './di/scope';
export {formatRuntimeError as ɵformatRuntimeError, RuntimeError as ɵRuntimeError} from './errors';
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/di/index.ts
Expand Up @@ -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';
Expand Down
32 changes: 31 additions & 1 deletion packages/core/src/di/injector.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -45,10 +45,40 @@ export abstract class Injector {
static THROW_IF_NOT_FOUND = THROW_IF_NOT_FOUND;
static NULL: Injector = (/* @__PURE__ */ new NullInjector());

/**
* Internal note on the `options?: InjectOptions|InjectFlags` override of the `get`
* method: consider dropping the `InjectFlags` part in one of the major versions.
* It can **not** be done in minor/patch, since it's breaking for custom injectors
* that only implement the old `InjectorFlags` interface.
*/

/**
* 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`.
*/
abstract get<T>(token: ProviderToken<T>, notFoundValue: undefined, options: InjectOptions&{
optional?: false;
}): 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`.
*/
abstract get<T>(token: ProviderToken<T>, notFoundValue: null|undefined, options: InjectOptions): T
|null;
/**
* 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`.
*/
abstract get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions|InjectFlags):
AndrewKushnir marked this conversation as resolved.
Show resolved Hide resolved
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<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
/**
Expand Down
58 changes: 18 additions & 40 deletions packages/core/src/di/injector_compatibility.ts
Expand Up @@ -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';


Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -240,17 +211,24 @@ export function inject<T>(token: ProviderToken<T>, options: InjectOptions): T|nu
*/
export function inject<T>(
token: ProviderToken<T>, 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|
AndrewKushnir marked this conversation as resolved.
Show resolved Hide resolved
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[])[]): any[] {
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/di/interface/injector.ts
Expand Up @@ -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;
}
32 changes: 28 additions & 4 deletions packages/core/src/di/r3_injector.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -82,6 +82,28 @@ 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<T>(token: ProviderToken<T>, notFoundValue: undefined, options: InjectOptions&{
optional?: false;
}): 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`.
*/
abstract get<T>(token: ProviderToken<T>, notFoundValue: null|undefined, options: InjectOptions): T
|null;
/**
* 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`.
*/
abstract get<T>(token: ProviderToken<T>, 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<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
/**
* @deprecated from v4.0.0 use ProviderToken<T>
Expand Down Expand Up @@ -207,8 +229,10 @@ export class R3Injector extends EnvironmentInjector {

override get<T>(
token: ProviderToken<T>, 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);
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/render3/component_ref.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -83,7 +84,8 @@ function getNamespace(elementName: string): string|null {
class ChainedInjector implements Injector {
constructor(private injector: Injector, private parentInjector: Injector) {}

get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T {
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags|InjectOptions): T {
flags = convertToBitFlags(flags);
const value = this.injector.get<T|typeof NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR>(
token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR, flags);

Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/render3/di.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
}

Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/render3/ng_module_ref.ts
Expand Up @@ -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';
Expand Down