Skip to content

Commit

Permalink
feat(core): support object-based DI flags in Injector.get() (#46761)
Browse files Browse the repository at this point in the history
This commit applies the changes similar to the ones performed for the `inject()` function in df246bb.

The `Injector.get` function is updated to use previously added object-based API for options: now the flags argument supports passing an object which configures injection flags.

DEPRECATED:

The bit field signature of `Injector.get()` has been deprecated, in favor of the new options object.

PR Close #46761
  • Loading branch information
AndrewKushnir authored and alxhub committed Sep 27, 2022
1 parent 85b5d12 commit 841c8e5
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 53 deletions.
4 changes: 4 additions & 0 deletions goldens/public-api/core/index.md
Expand Up @@ -492,6 +492,8 @@ export const ENVIRONMENT_INITIALIZER: InjectionToken<() => void>;
export abstract class EnvironmentInjector implements Injector {
// (undocumented)
abstract destroy(): void;
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 +733,8 @@ export abstract class Injector {
parent?: Injector;
name?: string;
}): Injector;
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
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
10 changes: 9 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 @@ -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<T>(token: ProviderToken<T>, 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<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
/**
* @deprecated from v4.0.0 use ProviderToken<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|
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;
}
17 changes: 13 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,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<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 +214,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
111 changes: 111 additions & 0 deletions packages/core/test/acceptance/di_spec.ts
Expand Up @@ -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<string>('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<string>('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<string>('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<string>('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<string>('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<string>('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: '<child></child>',
providers: [{provide: TOKEN, useValue: 'from parent'}],
encapsulation: ViewEncapsulation.None,
})
class ParentCmp {
}

const fixture = TestBed.createComponent(ParentCmp);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('<child>not found|not found</child>');
});
});
});

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

0 comments on commit 841c8e5

Please sign in to comment.