Skip to content

Commit 89d291c

Browse files
alxhubatscott
authored andcommitted
feat(core): add assertInInjectionContext (#49529)
This commit adds an assertion function to the public API, which allows authors of functions which rely on `inject` to validate that they're being called with the right context. This mostly produces a nicer error message than calling `inject()` and relying on Angular's default error message for that. PR Close #49529
1 parent 84a0fa3 commit 89d291c

File tree

5 files changed

+66
-5
lines changed

5 files changed

+66
-5
lines changed

goldens/public-api/core/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ export class ApplicationRef {
9898
// @public (undocumented)
9999
export function asNativeElements(debugEls: DebugElement[]): any;
100100

101+
// @public
102+
export function assertInInjectionContext(debugFn: Function): void;
103+
101104
// @public
102105
export function assertPlatform(requiredToken: any): PlatformRef;
103106

packages/core/src/di/contextual.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {RuntimeError, RuntimeErrorCode} from '../errors';
910
import type {Injector} from './injector';
10-
import {setCurrentInjector} from './injector_compatibility';
11-
import {setInjectImplementation} from './inject_switch';
11+
import {getCurrentInjector, setCurrentInjector} from './injector_compatibility';
12+
import {getInjectImplementation, setInjectImplementation} from './inject_switch';
1213
import {R3Injector} from './r3_injector';
1314

1415
/**
@@ -37,3 +38,22 @@ export function runInInjectionContext<ReturnT>(injector: Injector, fn: () => Ret
3738
setInjectImplementation(previousInjectImplementation);
3839
}
3940
}
41+
42+
/**
43+
* Asserts that the current stack frame is within an injection context and has access to `inject`.
44+
*
45+
* @param debugFn a reference to the function making the assertion (used for the error message).
46+
*
47+
* @publicApi
48+
*/
49+
export function assertInInjectionContext(debugFn: Function): void {
50+
// Taking a `Function` instead of a string name here prevents the unminified name of the function
51+
// from being retained in the bundle regardless of minification.
52+
if (!getInjectImplementation() && !getCurrentInjector()) {
53+
throw new RuntimeError(
54+
RuntimeErrorCode.MISSING_INJECTION_CONTEXT,
55+
ngDevMode &&
56+
(debugFn.name +
57+
'() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`'));
58+
}
59+
}

packages/core/src/di/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*/
1414

1515
export * from './metadata';
16-
export {runInInjectionContext} from './contextual';
16+
export {assertInInjectionContext, runInInjectionContext} from './contextual';
1717
export {InjectFlags} from './interface/injector';
1818
export {ɵɵdefineInjectable, defineInjectable, ɵɵdefineInjector, InjectableType, InjectorType} from './interface/defs';
1919
export {forwardRef, resolveForwardRef, ForwardRefFn} from './forward_ref';

packages/core/src/di/injector_compatibility.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ export const SOURCE = '__source';
4343
*/
4444
let _currentInjector: Injector|undefined|null = undefined;
4545

46+
export function getCurrentInjector(): Injector|undefined|null {
47+
return _currentInjector;
48+
}
49+
4650
export function setCurrentInjector(injector: Injector|null|undefined): Injector|undefined|null {
4751
const former = _currentInjector;
4852
_currentInjector = injector;
@@ -57,7 +61,7 @@ export function injectInjectorOnly<T>(token: ProviderToken<T>, flags = InjectFla
5761
throw new RuntimeError(
5862
RuntimeErrorCode.MISSING_INJECTION_CONTEXT,
5963
ngDevMode &&
60-
`inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with \`EnvironmentInjector#runInContext\`.`);
64+
`inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with \`runInInjectionContext\`.`);
6165
} else if (_currentInjector === null) {
6266
return injectRootLimpMode(token, undefined, flags);
6367
} else {

packages/core/test/acceptance/di_spec.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
*/
88

99
import {CommonModule} from '@angular/common';
10-
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';
10+
import {assertInInjectionContext, 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';
11+
import {RuntimeError, RuntimeErrorCode} from '@angular/core/src/errors';
1112
import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref';
1213
import {TestBed} from '@angular/core/testing';
1314
import {By} from '@angular/platform-browser';
@@ -3810,6 +3811,39 @@ describe('di', () => {
38103811
});
38113812
});
38123813

3814+
describe('assertInInjectionContext', () => {
3815+
function placeholder() {}
3816+
3817+
it('should throw if not in an injection context', () => {
3818+
expect(() => assertInInjectionContext(placeholder))
3819+
.toThrowMatching(
3820+
(e: Error) => e instanceof RuntimeError &&
3821+
e.code === RuntimeErrorCode.MISSING_INJECTION_CONTEXT);
3822+
});
3823+
3824+
it('should not throw if in an EnvironmentInjector context', () => {
3825+
expect(() => {
3826+
TestBed.runInInjectionContext(() => {
3827+
assertInInjectionContext(placeholder);
3828+
});
3829+
}).not.toThrow();
3830+
});
3831+
3832+
3833+
it('should not throw if in an element injector context', () => {
3834+
expect(() => {
3835+
@Component({template: ''})
3836+
class EmptyCmp {
3837+
}
3838+
3839+
const fixture = TestBed.createComponent(EmptyCmp);
3840+
runInInjectionContext(fixture.componentRef.injector, () => {
3841+
assertInInjectionContext(placeholder);
3842+
});
3843+
}).not.toThrow();
3844+
});
3845+
});
3846+
38133847
it('should be able to use Host in `useFactory` dependency config', () => {
38143848
// Scenario:
38153849
// ---------

0 commit comments

Comments
 (0)