Skip to content

Commit 7ac34e4

Browse files
alxhubvicb
authored andcommitted
feat: allow direct scoping of @Injectables to the root injector (#22185)
@Injectable() supports a scope parameter which specifies the target module. However, it's still difficult to specify that a particular service belongs in the root injector. A developer attempting to ensure that must either also provide a module intended for placement in the root injector or target a module known to already be in the root injector (e.g. BrowserModule). Both of these strategies are cumbersome and brittle. Instead, this commit adds a token APP_ROOT_SCOPE which provides a straightforward way of targeting the root injector directly, without requiring special knowledge of modules within it. PR Close #22185
1 parent 029dbf0 commit 7ac34e4

File tree

12 files changed

+152
-3
lines changed

12 files changed

+152
-3
lines changed

packages/compiler-cli/integrationtest/bazel/injectable_def/app/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ ng_module(
1616
"//packages/core",
1717
"//packages/platform-browser",
1818
"//packages/platform-server",
19+
"//packages/router",
1920
"@rxjs",
2021
],
2122
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 {APP_ROOT_SCOPE, Component, Injectable, NgModule, Optional, Self} from '@angular/core';
10+
import {BrowserModule} from '@angular/platform-browser';
11+
import {ServerModule} from '@angular/platform-server';
12+
import {RouterModule} from '@angular/router';
13+
14+
import {LazyModuleNgFactory} from './root_lazy.ngfactory';
15+
16+
@Component({
17+
selector: 'root-app',
18+
template: '<router-outlet></router-outlet>',
19+
})
20+
export class AppComponent {
21+
}
22+
23+
export function children(): any {
24+
console.error('children', LazyModuleNgFactory);
25+
return LazyModuleNgFactory;
26+
}
27+
28+
29+
@NgModule({
30+
imports: [
31+
BrowserModule.withServerTransition({appId: 'id-app'}),
32+
ServerModule,
33+
RouterModule.forRoot(
34+
[
35+
{path: '', pathMatch: 'prefix', loadChildren: children},
36+
],
37+
{initialNavigation: 'enabled'}),
38+
],
39+
declarations: [AppComponent],
40+
bootstrap: [AppComponent],
41+
})
42+
export class RootAppModule {
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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, NgModule, Optional, Self} from '@angular/core';
10+
import {RouterModule} from '@angular/router';
11+
import {Service} from './root_service';
12+
13+
@Component({
14+
selector: 'lazy-route',
15+
template: '{{service}}:{{serviceInLazyInjector}}',
16+
})
17+
export class RouteComponent {
18+
service: boolean;
19+
serviceInLazyInjector: boolean;
20+
constructor(@Optional() service: Service, @Optional() @Self() lazyService: Service) {
21+
this.service = !!service;
22+
this.serviceInLazyInjector = !!lazyService;
23+
}
24+
}
25+
26+
@NgModule({
27+
declarations: [RouteComponent],
28+
imports: [
29+
RouterModule.forChild([
30+
{path: '', pathMatch: 'prefix', component: RouteComponent},
31+
]),
32+
],
33+
})
34+
export class LazyModule {
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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 {APP_ROOT_SCOPE, Injectable} from '@angular/core';
10+
11+
@Injectable({
12+
scope: APP_ROOT_SCOPE,
13+
})
14+
export class Service {
15+
}

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

+11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {enableProdMode} from '@angular/core';
1010
import {renderModuleFactory} from '@angular/platform-server';
1111
import {BasicAppModuleNgFactory} from 'app_built/src/basic.ngfactory';
1212
import {HierarchyAppModuleNgFactory} from 'app_built/src/hierarchy.ngfactory';
13+
import {RootAppModuleNgFactory} from 'app_built/src/root.ngfactory';
1314
import {SelfAppModuleNgFactory} from 'app_built/src/self.ngfactory';
1415
import {TokenAppModuleNgFactory} from 'app_built/src/token.ngfactory';
1516

@@ -55,4 +56,14 @@ describe('ngInjectableDef Bazel Integration', () => {
5556
done();
5657
});
5758
});
59+
60+
it('APP_ROOT_SCOPE works', done => {
61+
renderModuleFactory(RootAppModuleNgFactory, {
62+
document: '<root-app></root-app>',
63+
url: '/',
64+
}).then(html => {
65+
expect(html).toMatch(/>true:false<\//);
66+
done();
67+
});
68+
});
5869
});

packages/core/src/di.ts

+1
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provid
2323
export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './di/reflective_provider';
2424
export {ReflectiveKey} from './di/reflective_key';
2525
export {InjectionToken} from './di/injection_token';
26+
export {APP_ROOT_SCOPE} from './di/scope';

packages/core/src/di/scope.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 {Type} from '../type';
10+
import {InjectionToken} from './injection_token';
11+
12+
13+
// APP_ROOT_SCOPE is cast as a Type to allow for its usage as the scope parameter of @Injectable().
14+
15+
/**
16+
* A scope which targets the root injector.
17+
*
18+
* When specified as the `scope` parameter to `@Injectable` or `InjectionToken`, this special
19+
* scope indicates the provider for the service or token being configured belongs in the root
20+
* injector. This is loosely equivalent to the convention of having a `forRoot()` static
21+
* function within a module that configures the provider, and expecting users to only import that
22+
* module via its `forRoot()` function in the root injector.
23+
*
24+
* @experimental
25+
*/
26+
export const APP_ROOT_SCOPE: Type<any> = new InjectionToken<boolean>(
27+
'The presence of this token marks an injector as being the root injector.') as any;

packages/core/src/view/ng_module.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {resolveForwardRef} from '../di/forward_ref';
1010
import {InjectFlags, Injector, setCurrentInjector} from '../di/injector';
11+
import {APP_ROOT_SCOPE} from '../di/scope';
1112
import {NgModuleRef} from '../linker/ng_module_factory';
1213
import {stringify} from '../util';
1314

@@ -43,8 +44,12 @@ export function moduleProvideDef(
4344
export function moduleDef(providers: NgModuleProviderDef[]): NgModuleDefinition {
4445
const providersByKey: {[key: string]: NgModuleProviderDef} = {};
4546
const modules = [];
47+
let isRoot: boolean = false;
4648
for (let i = 0; i < providers.length; i++) {
4749
const provider = providers[i];
50+
if (provider.token === APP_ROOT_SCOPE) {
51+
isRoot = true;
52+
}
4853
if (provider.flags & NodeFlags.TypeNgModule) {
4954
modules.push(provider.token);
5055
}
@@ -57,6 +62,7 @@ export function moduleDef(providers: NgModuleProviderDef[]): NgModuleDefinition
5762
providersByKey,
5863
providers,
5964
modules,
65+
isRoot,
6066
};
6167
}
6268

@@ -119,8 +125,13 @@ export function resolveNgModuleDep(
119125
return data._parent.get(depDef.token, notFoundValue);
120126
}
121127

128+
function moduleTransitivelyPresent(ngModule: NgModuleData, scope: any): boolean {
129+
return ngModule._def.modules.indexOf(scope) > -1;
130+
}
131+
122132
function targetsModule(ngModule: NgModuleData, def: InjectableDef): boolean {
123-
return def.scope != null && ngModule._def.modules.indexOf(def.scope) > -1;
133+
return def.scope != null && (moduleTransitivelyPresent(ngModule, def.scope) ||
134+
def.scope === APP_ROOT_SCOPE && ngModule._def.isRoot);
124135
}
125136

126137
function _createProviderInstance(ngModule: NgModuleData, providerDef: NgModuleProviderDef): any {

packages/core/src/view/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface NgModuleDefinition extends Definition<NgModuleDefinitionFactory
4343
providers: NgModuleProviderDef[];
4444
providersByKey: {[tokenKey: string]: NgModuleProviderDef};
4545
modules: any[];
46+
isRoot: boolean;
4647
}
4748

4849
export interface NgModuleDefinitionFactory extends DefinitionFactory<NgModuleDefinition> {}

packages/core/test/view/ng_module_spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ function makeProviders(classes: any[], modules: any[]): NgModuleDefinition {
9797
}));
9898
const providersByKey: {[key: string]: NgModuleProviderDef} = {};
9999
providers.forEach(provider => providersByKey[tokenKey(provider.token)] = provider);
100-
return {factory: null, providers, providersByKey, modules};
100+
return {factory: null, providers, providersByKey, modules, isRoot: true};
101101
}
102102

103103
describe('NgModuleRef_ injector', () => {

packages/platform-browser/src/browser.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {CommonModule, PlatformLocation, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common';
10-
import {APP_ID, ApplicationModule, ErrorHandler, ModuleWithProviders, NgModule, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, RendererFactory2, RootRenderer, Sanitizer, SkipSelf, StaticProvider, Testability, createPlatformFactory, platformCore} from '@angular/core';
10+
import {APP_ID, APP_ROOT_SCOPE, ApplicationModule, ErrorHandler, ModuleWithProviders, NgModule, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, RendererFactory2, RootRenderer, Sanitizer, SkipSelf, StaticProvider, Testability, createPlatformFactory, platformCore} from '@angular/core';
1111

1212
import {BrowserDomAdapter} from './browser/browser_adapter';
1313
import {BrowserPlatformLocation} from './browser/location/browser_platform_location';
@@ -71,6 +71,7 @@ export function _document(): any {
7171
@NgModule({
7272
providers: [
7373
BROWSER_SANITIZATION_PROVIDERS,
74+
{provide: APP_ROOT_SCOPE, useValue: true},
7475
{provide: ErrorHandler, useFactory: errorHandler, deps: []},
7576
{provide: EVENT_MANAGER_PLUGINS, useClass: DomEventsPlugin, multi: true},
7677
{provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true},

tools/public_api_guard/core/core.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ export declare const APP_ID: InjectionToken<string>;
112112
/** @experimental */
113113
export declare const APP_INITIALIZER: InjectionToken<(() => void)[]>;
114114

115+
/** @experimental */
116+
export declare const APP_ROOT_SCOPE: Type<any>;
117+
115118
/** @experimental */
116119
export declare class ApplicationInitStatus {
117120
readonly done: boolean;

0 commit comments

Comments
 (0)