refactor(core): introduce `@NgModule.bootstrap` and `ngDoBootstrap` method

If a `@NgModule` has a `bootstrap` property, `PlatformRef.bootstrapModule` /
`PlatformRef.bootstrapModuleFactory` will automatically bootstrap the components
listed in there.
If such a property does not exist, `PlatformRef.bootstrapModule` /
`PlatformRef.bootstrapModuleFactory` will try to call the method `ngDoBootstrap(appRef: ApplicationRef)` on the module class.
Otherwise an error is reported.
This commit is contained in:
Tobias Bosch 2016-08-02 06:54:08 -07:00
parent af2e80e068
commit 7e4fd7d7da
14 changed files with 156 additions and 37 deletions

View File

@ -34,4 +34,6 @@ import {CompWithChildQuery, CompWithDirectiveChild} from './queries';
})
export class MainModule {
constructor(public appRef: ApplicationRef) {}
ngDoBootstrap() {}
}

View File

@ -633,6 +633,7 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier {
exportedPipes: CompilePipeMetadata[];
// Note: See CompileDirectiveMetadata.entryComponents why this has to be a type.
entryComponents: CompileTypeMetadata[];
bootstrapComponents: CompileTypeMetadata[];
providers: CompileProviderMetadata[];
importedModules: CompileNgModuleMetadata[];
@ -643,7 +644,8 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier {
constructor(
{type, providers, declaredDirectives, exportedDirectives, declaredPipes, exportedPipes,
entryComponents, importedModules, exportedModules, schemas, transitiveModule}: {
entryComponents, bootstrapComponents, importedModules, exportedModules, schemas,
transitiveModule}: {
type?: CompileTypeMetadata,
providers?:
Array<CompileProviderMetadata|CompileTypeMetadata|CompileIdentifierMetadata|any[]>,
@ -652,6 +654,7 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier {
declaredPipes?: CompilePipeMetadata[],
exportedPipes?: CompilePipeMetadata[],
entryComponents?: CompileTypeMetadata[],
bootstrapComponents?: CompileTypeMetadata[],
importedModules?: CompileNgModuleMetadata[],
exportedModules?: CompileNgModuleMetadata[],
transitiveModule?: TransitiveCompileNgModuleMetadata,
@ -664,6 +667,7 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier {
this.exportedPipes = _normalizeArray(exportedPipes);
this.providers = _normalizeArray(providers);
this.entryComponents = _normalizeArray(entryComponents);
this.bootstrapComponents = _normalizeArray(bootstrapComponents);
this.importedModules = _normalizeArray(importedModules);
this.exportedModules = _normalizeArray(exportedModules);
this.schemas = _normalizeArray(schemas);

View File

@ -237,6 +237,7 @@ export class CompileMetadataResolver {
const exportedModules: cpl.CompileNgModuleMetadata[] = [];
const providers: any[] = [];
const entryComponents: cpl.CompileTypeMetadata[] = [];
const bootstrapComponents: cpl.CompileTypeMetadata[] = [];
const schemas: SchemaMetadata[] = [];
if (meta.imports) {
@ -318,6 +319,12 @@ export class CompileMetadataResolver {
...flattenArray(meta.entryComponents)
.map(type => this.getTypeMetadata(type, staticTypeModuleUrl(type))));
}
if (meta.bootstrap) {
bootstrapComponents.push(
...flattenArray(meta.bootstrap)
.map(type => this.getTypeMetadata(type, staticTypeModuleUrl(type))));
}
entryComponents.push(...bootstrapComponents);
if (meta.schemas) {
schemas.push(...flattenArray(meta.schemas));
}
@ -329,6 +336,7 @@ export class CompileMetadataResolver {
type: this.getTypeMetadata(moduleType, staticTypeModuleUrl(moduleType)),
providers: providers,
entryComponents: entryComponents,
bootstrapComponents: bootstrapComponents,
schemas: schemas,
declaredDirectives: declaredDirectives,
exportedDirectives: exportedDirectives,

View File

@ -43,12 +43,18 @@ export class NgModuleCompiler {
new ParseLocation(sourceFile, null, null, null),
new ParseLocation(sourceFile, null, null, null));
var deps: ComponentFactoryDependency[] = [];
var entryComponents = ngModuleMeta.transitiveModule.entryComponents.map((entryComponent) => {
var id = new CompileIdentifierMetadata({name: entryComponent.name});
deps.push(new ComponentFactoryDependency(entryComponent, id));
return id;
});
var builder = new _InjectorBuilder(ngModuleMeta, entryComponents, sourceSpan);
var bootstrapComponentFactories: CompileIdentifierMetadata[] = [];
var entryComponentFactories =
ngModuleMeta.transitiveModule.entryComponents.map((entryComponent) => {
var id = new CompileIdentifierMetadata({name: entryComponent.name});
if (ngModuleMeta.bootstrapComponents.indexOf(entryComponent) > -1) {
bootstrapComponentFactories.push(id);
}
deps.push(new ComponentFactoryDependency(entryComponent, id));
return id;
});
var builder = new _InjectorBuilder(
ngModuleMeta, entryComponentFactories, bootstrapComponentFactories, sourceSpan);
var providerParser = new NgModuleProviderAnalyzer(ngModuleMeta, extraProviders, sourceSpan);
providerParser.parse().forEach((provider) => builder.addProvider(provider));
@ -78,8 +84,9 @@ class _InjectorBuilder {
constructor(
private _ngModuleMeta: CompileNgModuleMetadata,
private _entryComponents: CompileIdentifierMetadata[], private _sourceSpan: ParseSourceSpan) {
}
private _entryComponentFactories: CompileIdentifierMetadata[],
private _bootstrapComponentFactories: CompileIdentifierMetadata[],
private _sourceSpan: ParseSourceSpan) {}
addProvider(resolvedProvider: ProviderAst) {
var providerValueExpressions =
@ -125,8 +132,10 @@ class _InjectorBuilder {
[o.SUPER_EXPR
.callFn([
o.variable(InjectorProps.parent.name),
o.literalArr(
this._entryComponents.map((entryComponent) => o.importExpr(entryComponent)))
o.literalArr(this._entryComponentFactories.map(
(componentFactory) => o.importExpr(componentFactory))),
o.literalArr(this._bootstrapComponentFactories.map(
(componentFactory) => o.importExpr(componentFactory)))
])
.toStmt()]);

View File

@ -9,7 +9,7 @@
import {ObservableWrapper, PromiseCompleter, PromiseWrapper} from '../src/facade/async';
import {ListWrapper} from '../src/facade/collection';
import {BaseException, ExceptionHandler, unimplemented} from '../src/facade/exceptions';
import {ConcreteType, Type, isBlank, isPresent, isPromise} from '../src/facade/lang';
import {ConcreteType, Type, isBlank, isPresent, isPromise, stringify} from '../src/facade/lang';
import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, PLATFORM_INITIALIZER} from './application_tokens';
import {ChangeDetectorRef} from './change_detection/change_detector_ref';
@ -19,7 +19,7 @@ import {Compiler, CompilerFactory, CompilerOptions} from './linker/compiler';
import {ComponentFactory, ComponentRef} from './linker/component_factory';
import {ComponentFactoryResolver} from './linker/component_factory_resolver';
import {ComponentResolver} from './linker/component_resolver';
import {NgModuleFactory, NgModuleRef} from './linker/ng_module_factory';
import {NgModuleFactory, NgModuleInjector, NgModuleRef} from './linker/ng_module_factory';
import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile';
import {Testability, TestabilityRegistry} from './testability/testability';
import {NgZone, NgZoneError} from './zone/ng_zone';
@ -349,7 +349,7 @@ export class PlatformRef_ extends PlatformRef {
return ngZone.run(() => {
const ngZoneInjector =
ReflectiveInjector.resolveAndCreate([{provide: NgZone, useValue: ngZone}], this.injector);
const moduleRef = moduleFactory.create(ngZoneInjector);
const moduleRef = <NgModuleInjector<M>>moduleFactory.create(ngZoneInjector);
const exceptionHandler: ExceptionHandler = moduleRef.injector.get(ExceptionHandler, null);
if (!exceptionHandler) {
throw new Error('No ExceptionHandler. Is platform module (BrowserModule) included?');
@ -372,6 +372,7 @@ export class PlatformRef_ extends PlatformRef {
const appRef: ApplicationRef_ = moduleRef.injector.get(ApplicationRef);
return Promise.all(asyncInitPromises).then(() => {
appRef.asyncInitDone();
this._moduleDoBootstrap(moduleRef);
return moduleRef;
});
});
@ -387,6 +388,19 @@ export class PlatformRef_ extends PlatformRef {
return compiler.compileModuleAsync(moduleType)
.then((moduleFactory) => this.bootstrapModuleFactory(moduleFactory));
}
private _moduleDoBootstrap(moduleRef: NgModuleInjector<any>) {
const appRef = moduleRef.injector.get(ApplicationRef);
if (moduleRef.bootstrapFactories.length > 0) {
moduleRef.bootstrapFactories.forEach((compFactory) => appRef.bootstrap(compFactory));
} else if (moduleRef.instance.ngDoBootstrap) {
moduleRef.instance.ngDoBootstrap(appRef);
} else {
throw new BaseException(
`The module ${stringify(moduleRef.instance.constructor)} was bootstrapped, but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. ` +
`Please define one of these.`);
}
}
}
/**
@ -473,6 +487,10 @@ export abstract class ApplicationRef {
* Get a list of component types registered to this application.
*/
get componentTypes(): Type[] { return <Type[]>unimplemented(); };
/**
* Get a list of components registered to this application.
*/
get components(): ComponentRef<any>[] { return <ComponentRef<any>[]>unimplemented(); };
}
@Injectable()
@ -620,4 +638,6 @@ export class ApplicationRef_ extends ApplicationRef {
dispose(): void { this.ngOnDestroy(); }
get componentTypes(): Type[] { return this._rootComponentTypes; }
get components(): ComponentRef<any>[] { return this._rootComponents; }
}

View File

@ -81,7 +81,9 @@ export abstract class NgModuleInjector<T> extends CodegenComponentFactoryResolve
public instance: T;
constructor(public parent: Injector, factories: ComponentFactory<any>[]) {
constructor(
public parent: Injector, factories: ComponentFactory<any>[],
public bootstrapFactories: ComponentFactory<any>[]) {
super(factories, parent.get(ComponentFactoryResolver, ComponentFactoryResolver.NULL));
}

View File

@ -46,6 +46,7 @@ export interface NgModuleMetadataType {
imports?: Array<Type|ModuleWithProviders|any[]>;
exports?: Array<Type|any[]>;
entryComponents?: Array<Type|any[]>;
bootstrap?: Array<Type|any[]>;
schemas?: Array<SchemaMetadata|any[]>;
}
@ -144,7 +145,14 @@ export class NgModuleMetadata extends InjectableMetadata implements NgModuleMeta
*/
entryComponents: Array<Type|any[]>;
schemas: Array<SchemaMetadata|any[]>;
/**
* Defines the components that should be bootstrapped when
* this module is bootstrapped. The components listed here
* will automatically be added to `entryComponents`.
*/
bootstrap: Array<Type|any[]>
schemas: Array<SchemaMetadata|any[]>;
constructor(options: NgModuleMetadataType = {}) {
// We cannot use destructuring of the constructor argument because `exports` is a
@ -155,6 +163,7 @@ export class NgModuleMetadata extends InjectableMetadata implements NgModuleMeta
this.imports = options.imports;
this.exports = options.exports;
this.entryComponents = options.entryComponents;
this.bootstrap = options.bootstrap;
this.schemas = options.schemas;
}
}

View File

@ -39,20 +39,34 @@ export function main() {
errorLogger = new _ArrayLogger();
});
function createModule(providers: any[] = []): ConcreteType<any> {
type CreateModuleOptions = {providers?: any[], ngDoBootstrap?: any, bootstrap?: any[]};
function createModule(providers?: any[]): ConcreteType<any>;
function createModule(options: CreateModuleOptions): ConcreteType<any>;
function createModule(providersOrOptions: any[] | CreateModuleOptions): ConcreteType<any> {
let options: CreateModuleOptions = {};
if (providersOrOptions instanceof Array) {
options = {providers: providersOrOptions};
} else {
options = providersOrOptions || {};
}
@NgModule({
providers: [
{provide: Console, useValue: new _MockConsole()},
{provide: ExceptionHandler, useValue: new ExceptionHandler(errorLogger, false)},
{provide: DOCUMENT, useValue: fakeDoc}, providers
{provide: DOCUMENT, useValue: fakeDoc}, options.providers || []
],
imports: [BrowserModule],
declarations: [SomeComponent],
entryComponents: [SomeComponent]
entryComponents: [SomeComponent],
bootstrap: options.bootstrap || []
})
class MyModule {
}
if (options.ngDoBootstrap !== false) {
(<any>MyModule.prototype).ngDoBootstrap = options.ngDoBootstrap || (() => {});
}
return MyModule;
}
@ -176,6 +190,35 @@ export function main() {
.toEqual('No ExceptionHandler. Is platform module (BrowserModule) included?');
});
}));
it('should call the `ngDoBootstrap` method with `ApplicationRef` on the main module',
async(() => {
const ngDoBootstrap = jasmine.createSpy('ngDoBootstrap');
defaultPlatform.bootstrapModule(createModule({ngDoBootstrap: ngDoBootstrap}))
.then((moduleRef) => {
const appRef = moduleRef.injector.get(ApplicationRef);
expect(ngDoBootstrap).toHaveBeenCalledWith(appRef);
});
}));
it('should auto bootstrap components listed in @NgModule.bootstrap', async(() => {
defaultPlatform.bootstrapModule(createModule({bootstrap: [SomeComponent]}))
.then((moduleRef) => {
const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
expect(appRef.componentTypes).toEqual([SomeComponent]);
});
}));
it('should error if neither `ngDoBootstrap` nor @NgModule.bootstrap was specified',
async(() => {
defaultPlatform.bootstrapModule(createModule({ngDoBootstrap: false}))
.then(() => expect(false).toBe(true), (e) => {
const expectedErrMsg =
`The module MyModule was bootstrapped, but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. Please define one of these.`;
expect(e.message).toEqual(expectedErrMsg);
expect(errorLogger.res).toEqual(['EXCEPTION: ' + expectedErrMsg]);
});
}));
});
describe('bootstrapModuleFactory', () => {

View File

@ -17,6 +17,7 @@ import {expect} from '@angular/platform-browser/testing/matchers';
import {BaseException} from '../../src/facade/exceptions';
import {ConcreteType, Type, stringify} from '../../src/facade/lang';
import {NgModuleInjector} from '../../src/linker/ng_module_factory';
class Engine {}
@ -253,7 +254,7 @@ function declareTests({useJit}: {useJit: boolean}) {
});
describe('entryComponents', () => {
it('should entryComponents ComponentFactories in root modules', () => {
it('should create ComponentFactories in root modules', () => {
@NgModule({declarations: [SomeComp], entryComponents: [SomeComp]})
class SomeModule {
}
@ -289,7 +290,7 @@ function declareTests({useJit}: {useJit: boolean}) {
]);
});
it('should entryComponents ComponentFactories via ANALYZE_FOR_ENTRY_COMPONENTS', () => {
it('should create ComponentFactories via ANALYZE_FOR_ENTRY_COMPONENTS', () => {
@NgModule({
declarations: [SomeComp],
providers: [{
@ -310,7 +311,7 @@ function declareTests({useJit}: {useJit: boolean}) {
.toBe(SomeComp);
});
it('should entryComponents ComponentFactories in imported modules', () => {
it('should crate ComponentFactories in imported modules', () => {
@NgModule({declarations: [SomeComp], entryComponents: [SomeComp]})
class SomeImportedModule {
}
@ -328,7 +329,7 @@ function declareTests({useJit}: {useJit: boolean}) {
.toBe(SomeComp);
});
it('should entryComponents ComponentFactories if the component was imported', () => {
it('should create ComponentFactories if the component was imported', () => {
@NgModule({declarations: [SomeComp], exports: [SomeComp]})
class SomeImportedModule {
}
@ -348,6 +349,29 @@ function declareTests({useJit}: {useJit: boolean}) {
});
describe('bootstrap components', () => {
it('should create ComponentFactories', () => {
@NgModule({declarations: [SomeComp], bootstrap: [SomeComp]})
class SomeModule {
}
const ngModule = createModule(SomeModule);
expect(ngModule.componentFactoryResolver.resolveComponentFactory(SomeComp).componentType)
.toBe(SomeComp);
});
it('should store the ComponentFactories in the NgModuleInjector', () => {
@NgModule({declarations: [SomeComp], bootstrap: [SomeComp]})
class SomeModule {
}
const ngModule = <NgModuleInjector<any>>createModule(SomeModule);
expect(ngModule.bootstrapFactories.length).toBe(1);
expect(ngModule.bootstrapFactories[0].componentType).toBe(SomeComp);
});
});
describe('directives and pipes', () => {
describe('declarations', () => {
it('should be supported in root modules', () => {

View File

@ -169,7 +169,8 @@ export function bootstrap<C>(
providers: providers,
declarations: declarations.concat([appComponentType]),
imports: [BrowserModule, imports],
entryComponents: entryComponents.concat([appComponentType]),
entryComponents: entryComponents,
bootstrap: [appComponentType],
schemas: schemas
})
class DynamicModule {
@ -181,7 +182,7 @@ export function bootstrap<C>(
const console = moduleRef.injector.get(Console);
deprecationMessages.forEach((msg) => console.warn(msg));
const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
return appRef.bootstrap(appComponentType);
return appRef.components[0];
});
}
@ -233,7 +234,7 @@ export function bootstrapWorkerApp<T>(
providers: customProviders,
declarations: declarations,
imports: [WorkerAppModule],
entryComponents: [appComponentType]
bootstrap: [appComponentType]
})
class DynamicModule {
}
@ -244,7 +245,7 @@ export function bootstrapWorkerApp<T>(
const console = moduleRef.injector.get(Console);
deprecatedConfiguration.deprecationMessages.forEach((msg) => console.warn(msg));
const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
return appRef.bootstrap(appComponentType);
return appRef.components[0];
});
}

View File

@ -269,6 +269,7 @@ export function main() {
]
})
class SomeModule {
ngDoBootstrap() {}
}
expect(log.result()).toEqual('platform_init1; platform_init2');

View File

@ -110,7 +110,7 @@ export function serverBootstrap<T>(
providers: customProviders,
declarations: declarations,
imports: [BrowserModule],
entryComponents: [appComponentType]
bootstrap: [appComponentType]
})
class DynamicModule {
}
@ -121,6 +121,6 @@ export function serverBootstrap<T>(
const console = moduleRef.injector.get(Console);
deprecatedConfiguration.deprecationMessages.forEach((msg) => console.warn(msg));
const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
return appRef.bootstrap(appComponentType);
return appRef.components[0];
});
}

View File

@ -286,6 +286,7 @@ export class UpgradeAdapter {
@NgModule({providers: providers, imports: [BrowserModule]})
class DynamicModule {
ngDoBootstrap() {}
}
platformRef.bootstrapModule(DynamicModule).then((moduleRef) => {

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, NgModule, ApplicationRef} from '@angular/core';
import {Component, NgModule} from '@angular/core';
import {Start} from './components/start';
import {About} from './components/about';
import {Contact} from './components/contact';
@ -27,13 +27,8 @@ export const ROUTES = [
@NgModule({
imports: [WorkerAppModule, RouterModule.forRoot(ROUTES, {useHash: true})],
providers: [WORKER_APP_LOCATION_PROVIDERS],
entryComponents: [App],
bootstrap: [App],
declarations: [App, Start, Contact, About]
})
export class AppModule {
constructor(appRef: ApplicationRef) {
appRef.waitForAsyncInitializers().then( () => {
appRef.bootstrap(App);
});
}
}