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
This commit is contained in:
parent
029dbf0e18
commit
7ac34e42a0
|
@ -16,6 +16,7 @@ ng_module(
|
||||||
"//packages/core",
|
"//packages/core",
|
||||||
"//packages/platform-browser",
|
"//packages/platform-browser",
|
||||||
"//packages/platform-server",
|
"//packages/platform-server",
|
||||||
|
"//packages/router",
|
||||||
"@rxjs",
|
"@rxjs",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {APP_ROOT_SCOPE, Component, Injectable, NgModule, Optional, Self} from '@angular/core';
|
||||||
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
|
import {ServerModule} from '@angular/platform-server';
|
||||||
|
import {RouterModule} from '@angular/router';
|
||||||
|
|
||||||
|
import {LazyModuleNgFactory} from './root_lazy.ngfactory';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'root-app',
|
||||||
|
template: '<router-outlet></router-outlet>',
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function children(): any {
|
||||||
|
console.error('children', LazyModuleNgFactory);
|
||||||
|
return LazyModuleNgFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
BrowserModule.withServerTransition({appId: 'id-app'}),
|
||||||
|
ServerModule,
|
||||||
|
RouterModule.forRoot(
|
||||||
|
[
|
||||||
|
{path: '', pathMatch: 'prefix', loadChildren: children},
|
||||||
|
],
|
||||||
|
{initialNavigation: 'enabled'}),
|
||||||
|
],
|
||||||
|
declarations: [AppComponent],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
|
})
|
||||||
|
export class RootAppModule {
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Component, NgModule, Optional, Self} from '@angular/core';
|
||||||
|
import {RouterModule} from '@angular/router';
|
||||||
|
import {Service} from './root_service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'lazy-route',
|
||||||
|
template: '{{service}}:{{serviceInLazyInjector}}',
|
||||||
|
})
|
||||||
|
export class RouteComponent {
|
||||||
|
service: boolean;
|
||||||
|
serviceInLazyInjector: boolean;
|
||||||
|
constructor(@Optional() service: Service, @Optional() @Self() lazyService: Service) {
|
||||||
|
this.service = !!service;
|
||||||
|
this.serviceInLazyInjector = !!lazyService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [RouteComponent],
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{path: '', pathMatch: 'prefix', component: RouteComponent},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class LazyModule {
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {APP_ROOT_SCOPE, Injectable} from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
scope: APP_ROOT_SCOPE,
|
||||||
|
})
|
||||||
|
export class Service {
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import {enableProdMode} from '@angular/core';
|
||||||
import {renderModuleFactory} from '@angular/platform-server';
|
import {renderModuleFactory} from '@angular/platform-server';
|
||||||
import {BasicAppModuleNgFactory} from 'app_built/src/basic.ngfactory';
|
import {BasicAppModuleNgFactory} from 'app_built/src/basic.ngfactory';
|
||||||
import {HierarchyAppModuleNgFactory} from 'app_built/src/hierarchy.ngfactory';
|
import {HierarchyAppModuleNgFactory} from 'app_built/src/hierarchy.ngfactory';
|
||||||
|
import {RootAppModuleNgFactory} from 'app_built/src/root.ngfactory';
|
||||||
import {SelfAppModuleNgFactory} from 'app_built/src/self.ngfactory';
|
import {SelfAppModuleNgFactory} from 'app_built/src/self.ngfactory';
|
||||||
import {TokenAppModuleNgFactory} from 'app_built/src/token.ngfactory';
|
import {TokenAppModuleNgFactory} from 'app_built/src/token.ngfactory';
|
||||||
|
|
||||||
|
@ -55,4 +56,14 @@ describe('ngInjectableDef Bazel Integration', () => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('APP_ROOT_SCOPE works', done => {
|
||||||
|
renderModuleFactory(RootAppModuleNgFactory, {
|
||||||
|
document: '<root-app></root-app>',
|
||||||
|
url: '/',
|
||||||
|
}).then(html => {
|
||||||
|
expect(html).toMatch(/>true:false<\//);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,3 +23,4 @@ export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provid
|
||||||
export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './di/reflective_provider';
|
export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './di/reflective_provider';
|
||||||
export {ReflectiveKey} from './di/reflective_key';
|
export {ReflectiveKey} from './di/reflective_key';
|
||||||
export {InjectionToken} from './di/injection_token';
|
export {InjectionToken} from './di/injection_token';
|
||||||
|
export {APP_ROOT_SCOPE} from './di/scope';
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Type} from '../type';
|
||||||
|
import {InjectionToken} from './injection_token';
|
||||||
|
|
||||||
|
|
||||||
|
// APP_ROOT_SCOPE is cast as a Type to allow for its usage as the scope parameter of @Injectable().
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A scope which targets the root injector.
|
||||||
|
*
|
||||||
|
* When specified as the `scope` parameter to `@Injectable` or `InjectionToken`, this special
|
||||||
|
* scope indicates the provider for the service or token being configured belongs in the root
|
||||||
|
* injector. This is loosely equivalent to the convention of having a `forRoot()` static
|
||||||
|
* function within a module that configures the provider, and expecting users to only import that
|
||||||
|
* module via its `forRoot()` function in the root injector.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export const APP_ROOT_SCOPE: Type<any> = new InjectionToken<boolean>(
|
||||||
|
'The presence of this token marks an injector as being the root injector.') as any;
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import {resolveForwardRef} from '../di/forward_ref';
|
import {resolveForwardRef} from '../di/forward_ref';
|
||||||
import {InjectFlags, Injector, setCurrentInjector} from '../di/injector';
|
import {InjectFlags, Injector, setCurrentInjector} from '../di/injector';
|
||||||
|
import {APP_ROOT_SCOPE} from '../di/scope';
|
||||||
import {NgModuleRef} from '../linker/ng_module_factory';
|
import {NgModuleRef} from '../linker/ng_module_factory';
|
||||||
import {stringify} from '../util';
|
import {stringify} from '../util';
|
||||||
|
|
||||||
|
@ -43,8 +44,12 @@ export function moduleProvideDef(
|
||||||
export function moduleDef(providers: NgModuleProviderDef[]): NgModuleDefinition {
|
export function moduleDef(providers: NgModuleProviderDef[]): NgModuleDefinition {
|
||||||
const providersByKey: {[key: string]: NgModuleProviderDef} = {};
|
const providersByKey: {[key: string]: NgModuleProviderDef} = {};
|
||||||
const modules = [];
|
const modules = [];
|
||||||
|
let isRoot: boolean = false;
|
||||||
for (let i = 0; i < providers.length; i++) {
|
for (let i = 0; i < providers.length; i++) {
|
||||||
const provider = providers[i];
|
const provider = providers[i];
|
||||||
|
if (provider.token === APP_ROOT_SCOPE) {
|
||||||
|
isRoot = true;
|
||||||
|
}
|
||||||
if (provider.flags & NodeFlags.TypeNgModule) {
|
if (provider.flags & NodeFlags.TypeNgModule) {
|
||||||
modules.push(provider.token);
|
modules.push(provider.token);
|
||||||
}
|
}
|
||||||
|
@ -57,6 +62,7 @@ export function moduleDef(providers: NgModuleProviderDef[]): NgModuleDefinition
|
||||||
providersByKey,
|
providersByKey,
|
||||||
providers,
|
providers,
|
||||||
modules,
|
modules,
|
||||||
|
isRoot,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,8 +125,13 @@ export function resolveNgModuleDep(
|
||||||
return data._parent.get(depDef.token, notFoundValue);
|
return data._parent.get(depDef.token, notFoundValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moduleTransitivelyPresent(ngModule: NgModuleData, scope: any): boolean {
|
||||||
|
return ngModule._def.modules.indexOf(scope) > -1;
|
||||||
|
}
|
||||||
|
|
||||||
function targetsModule(ngModule: NgModuleData, def: InjectableDef): boolean {
|
function targetsModule(ngModule: NgModuleData, def: InjectableDef): boolean {
|
||||||
return def.scope != null && ngModule._def.modules.indexOf(def.scope) > -1;
|
return def.scope != null && (moduleTransitivelyPresent(ngModule, def.scope) ||
|
||||||
|
def.scope === APP_ROOT_SCOPE && ngModule._def.isRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _createProviderInstance(ngModule: NgModuleData, providerDef: NgModuleProviderDef): any {
|
function _createProviderInstance(ngModule: NgModuleData, providerDef: NgModuleProviderDef): any {
|
||||||
|
|
|
@ -43,6 +43,7 @@ export interface NgModuleDefinition extends Definition<NgModuleDefinitionFactory
|
||||||
providers: NgModuleProviderDef[];
|
providers: NgModuleProviderDef[];
|
||||||
providersByKey: {[tokenKey: string]: NgModuleProviderDef};
|
providersByKey: {[tokenKey: string]: NgModuleProviderDef};
|
||||||
modules: any[];
|
modules: any[];
|
||||||
|
isRoot: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NgModuleDefinitionFactory extends DefinitionFactory<NgModuleDefinition> {}
|
export interface NgModuleDefinitionFactory extends DefinitionFactory<NgModuleDefinition> {}
|
||||||
|
|
|
@ -97,7 +97,7 @@ function makeProviders(classes: any[], modules: any[]): NgModuleDefinition {
|
||||||
}));
|
}));
|
||||||
const providersByKey: {[key: string]: NgModuleProviderDef} = {};
|
const providersByKey: {[key: string]: NgModuleProviderDef} = {};
|
||||||
providers.forEach(provider => providersByKey[tokenKey(provider.token)] = provider);
|
providers.forEach(provider => providersByKey[tokenKey(provider.token)] = provider);
|
||||||
return {factory: null, providers, providersByKey, modules};
|
return {factory: null, providers, providersByKey, modules, isRoot: true};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('NgModuleRef_ injector', () => {
|
describe('NgModuleRef_ injector', () => {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {CommonModule, PlatformLocation, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common';
|
import {CommonModule, PlatformLocation, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common';
|
||||||
import {APP_ID, ApplicationModule, ErrorHandler, ModuleWithProviders, NgModule, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, RendererFactory2, RootRenderer, Sanitizer, SkipSelf, StaticProvider, Testability, createPlatformFactory, platformCore} from '@angular/core';
|
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';
|
||||||
|
|
||||||
import {BrowserDomAdapter} from './browser/browser_adapter';
|
import {BrowserDomAdapter} from './browser/browser_adapter';
|
||||||
import {BrowserPlatformLocation} from './browser/location/browser_platform_location';
|
import {BrowserPlatformLocation} from './browser/location/browser_platform_location';
|
||||||
|
@ -71,6 +71,7 @@ export function _document(): any {
|
||||||
@NgModule({
|
@NgModule({
|
||||||
providers: [
|
providers: [
|
||||||
BROWSER_SANITIZATION_PROVIDERS,
|
BROWSER_SANITIZATION_PROVIDERS,
|
||||||
|
{provide: APP_ROOT_SCOPE, useValue: true},
|
||||||
{provide: ErrorHandler, useFactory: errorHandler, deps: []},
|
{provide: ErrorHandler, useFactory: errorHandler, deps: []},
|
||||||
{provide: EVENT_MANAGER_PLUGINS, useClass: DomEventsPlugin, multi: true},
|
{provide: EVENT_MANAGER_PLUGINS, useClass: DomEventsPlugin, multi: true},
|
||||||
{provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true},
|
{provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true},
|
||||||
|
|
|
@ -112,6 +112,9 @@ export declare const APP_ID: InjectionToken<string>;
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export declare const APP_INITIALIZER: InjectionToken<(() => void)[]>;
|
export declare const APP_INITIALIZER: InjectionToken<(() => void)[]>;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export declare const APP_ROOT_SCOPE: Type<any>;
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export declare class ApplicationInitStatus {
|
export declare class ApplicationInitStatus {
|
||||||
readonly done: boolean;
|
readonly done: boolean;
|
||||||
|
|
Loading…
Reference in New Issue