Skip to content

Commit f8749bf

Browse files
alxhubalexeagle
authored andcommitted
fix(core): export inject() from @angular/core (#22389)
inject() supports the ngInjectableDef-based configuration of the injector (otherwise known as tree-shakeable services). It was missing from the exported API of @angular/core, this PR adds it. The test added here is correct in theory, but may pass accidentally due to the decorator side-effect replacing the inject() call at runtime. An upcoming compiler PR will strip reified decorators from the output entirely. Fixes #22388 PR Close #22389
1 parent 7d65356 commit f8749bf

File tree

8 files changed

+123
-7
lines changed

8 files changed

+123
-7
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Component, Injectable, NgModule} from '@angular/core';
10+
import {BrowserModule} from '@angular/platform-browser';
11+
import {ServerModule} from '@angular/platform-server';
12+
13+
@Injectable()
14+
export class NormalService {
15+
}
16+
17+
@Component({
18+
selector: 'dep-app',
19+
template: '{{found}}',
20+
})
21+
export class AppComponent {
22+
found: boolean;
23+
constructor(service: ShakeableService) { this.found = !!service.normal; }
24+
}
25+
26+
@NgModule({
27+
imports: [
28+
BrowserModule.withServerTransition({appId: 'id-app'}),
29+
ServerModule,
30+
],
31+
declarations: [AppComponent],
32+
bootstrap: [AppComponent],
33+
providers: [NormalService],
34+
})
35+
export class DepAppModule {
36+
}
37+
38+
@Injectable({scope: DepAppModule})
39+
export class ShakeableService {
40+
constructor(readonly normal: NormalService) {}
41+
}

packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/token.ts

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

9-
import {Component, Inject, InjectionToken, NgModule, forwardRef} from '@angular/core';
9+
import {Component, Inject, Injectable, InjectionToken, NgModule, forwardRef, inject} from '@angular/core';
1010
import {BrowserModule} from '@angular/platform-browser';
1111
import {ServerModule} from '@angular/platform-server';
1212

13-
export interface IService { readonly data: string; }
13+
export interface IService { readonly dep: {readonly data: string;}; }
1414

1515
@NgModule({})
1616
export class TokenModule {
1717
}
1818

1919
export const TOKEN = new InjectionToken('test', {
2020
scope: TokenModule,
21-
factory: () => new Service(),
21+
factory: () => new Service(inject(Dep)),
2222
});
2323

2424

@@ -28,7 +28,7 @@ export const TOKEN = new InjectionToken('test', {
2828
})
2929
export class AppComponent {
3030
data: string;
31-
constructor(@Inject(TOKEN) service: IService) { this.data = service.data; }
31+
constructor(@Inject(TOKEN) service: IService) { this.data = service.dep.data; }
3232
}
3333

3434
@NgModule({
@@ -37,10 +37,18 @@ export class AppComponent {
3737
ServerModule,
3838
TokenModule,
3939
],
40+
providers: [forwardRef(() => Dep)],
4041
declarations: [AppComponent],
4142
bootstrap: [AppComponent],
4243
})
4344
export class TokenAppModule {
4445
}
4546

46-
export class Service { readonly data = 'fromToken'; }
47+
@Injectable()
48+
export class Dep {
49+
readonly data = 'fromToken';
50+
}
51+
52+
export class Service {
53+
constructor(readonly dep: Dep) {}
54+
}

packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {enableProdMode} from '@angular/core';
1010
import {renderModuleFactory} from '@angular/platform-server';
1111
import {BasicAppModuleNgFactory} from 'app_built/src/basic.ngfactory';
12+
import {DepAppModuleNgFactory} from 'app_built/src/dep.ngfactory';
1213
import {HierarchyAppModuleNgFactory} from 'app_built/src/hierarchy.ngfactory';
1314
import {RootAppModuleNgFactory} from 'app_built/src/root.ngfactory';
1415
import {SelfAppModuleNgFactory} from 'app_built/src/self.ngfactory';
@@ -66,4 +67,14 @@ describe('ngInjectableDef Bazel Integration', () => {
6667
done();
6768
});
6869
});
70+
71+
it('can inject dependencies', done => {
72+
renderModuleFactory(DepAppModuleNgFactory, {
73+
document: '<dep-app></dep-app>',
74+
url: '/',
75+
}).then(html => {
76+
expect(html).toMatch(/>true<\//);
77+
done();
78+
});
79+
});
6980
});

packages/core/src/core_private_export.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export {devModeEqual as ɵdevModeEqual} from './change_detection/change_detectio
1212
export {isListLikeIterable as ɵisListLikeIterable} from './change_detection/change_detection_util';
1313
export {ChangeDetectorStatus as ɵChangeDetectorStatus, isDefaultChangeDetectionStrategy as ɵisDefaultChangeDetectionStrategy} from './change_detection/constants';
1414
export {Console as ɵConsole} from './console';
15+
export {setCurrentInjector as ɵsetCurrentInjector} from './di/injector';
1516
export {ComponentFactory as ɵComponentFactory} from './linker/component_factory';
1617
export {CodegenComponentFactoryResolver as ɵCodegenComponentFactoryResolver} from './linker/component_factory_resolver';
1718
export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities';

packages/core/src/di.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export {defineInjectable, Injectable, InjectableDecorator, InjectableProvider, I
1717

1818
export {forwardRef, resolveForwardRef, ForwardRefFn} from './di/forward_ref';
1919

20-
export {InjectFlags, Injector} from './di/injector';
20+
export {inject, InjectFlags, Injector} from './di/injector';
2121
export {ReflectiveInjector} from './di/reflective_injector';
2222
export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provider, TypeProvider, ClassProvider} from './di/provider';
2323
export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './di/reflective_provider';

packages/core/src/di/injector.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,20 @@ export function setCurrentInjector(injector: Injector | null): Injector|null {
410410
return former;
411411
}
412412

413+
/**
414+
* Injects a token from the currently active injector.
415+
*
416+
* This function must be used in the context of a factory function such as one defined for an
417+
* `InjectionToken`, and will throw an error if not called from such a context. For example:
418+
*
419+
* {@example core/di/ts/injector_spec.ts region='ShakeableInjectionToken'}
420+
*
421+
* Within such a factory function `inject` is utilized to request injection of a dependency, instead
422+
* of providing an additional array of dependencies as was common to do with `useFactory` providers.
423+
* `inject` is faster and more type-safe.
424+
*
425+
* @experimental
426+
*/
413427
export function inject<T>(
414428
token: Type<T>| InjectionToken<T>, notFoundValue?: undefined, flags?: InjectFlags): T;
415429
export function inject<T>(

packages/examples/core/di/ts/injector_spec.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,25 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {InjectionToken, Injector, ReflectiveInjector} from '@angular/core';
9+
import {APP_ROOT_SCOPE, InjectFlags, InjectionToken, Injector, ReflectiveInjector, Type, inject, ɵsetCurrentInjector as setCurrentInjector} from '@angular/core';
10+
11+
class MockRootScopeInjector implements Injector {
12+
constructor(readonly parent: Injector) {}
13+
14+
get<T>(
15+
token: Type<T>|InjectionToken<T>, defaultValue?: any,
16+
flags: InjectFlags = InjectFlags.Default): T {
17+
if ((token as any).ngInjectableDef && (token as any).ngInjectableDef.scope === APP_ROOT_SCOPE) {
18+
const old = setCurrentInjector(this);
19+
try {
20+
return (token as any).ngInjectableDef.factory();
21+
} finally {
22+
setCurrentInjector(old);
23+
}
24+
}
25+
return this.parent.get(token, defaultValue, flags);
26+
}
27+
}
1028

1129
{
1230
describe('injector metadata examples', () => {
@@ -37,5 +55,25 @@ import {InjectionToken, Injector, ReflectiveInjector} from '@angular/core';
3755
expect(url).toBe('http://localhost');
3856
// #enddocregion
3957
});
58+
59+
it('injects a tree-shaekable InjectionToken', () => {
60+
class MyDep {}
61+
const injector = new MockRootScopeInjector(ReflectiveInjector.resolveAndCreate([MyDep]));
62+
63+
// #docregion ShakeableInjectionToken
64+
class MyService {
65+
constructor(readonly myDep: MyDep) {}
66+
}
67+
68+
const MY_SERVICE_TOKEN = new InjectionToken<MyService>('Manually constructed MyService', {
69+
scope: APP_ROOT_SCOPE,
70+
factory: () => new MyService(inject(MyDep)),
71+
});
72+
73+
const instance = injector.get(MY_SERVICE_TOKEN);
74+
expect(instance instanceof MyService).toBeTruthy();
75+
expect(instance.myDep instanceof MyDep).toBeTruthy();
76+
// #enddocregion
77+
});
4078
});
4179
}

tools/public_api_guard/core/core.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,9 @@ export interface HostDecorator {
447447
/** @stable */
448448
export declare const HostListener: HostListenerDecorator;
449449

450+
/** @experimental */
451+
export declare function inject<T>(token: Type<T> | InjectionToken<T>, notFoundValue?: undefined, flags?: InjectFlags): T;
452+
450453
/** @stable */
451454
export declare const Inject: InjectDecorator;
452455

0 commit comments

Comments
 (0)