diff --git a/modules/@angular/compiler-cli/integrationtest/src/module.ts b/modules/@angular/compiler-cli/integrationtest/src/module.ts index a4173fa1c0..6deb6c1821 100644 --- a/modules/@angular/compiler-cli/integrationtest/src/module.ts +++ b/modules/@angular/compiler-cli/integrationtest/src/module.ts @@ -34,4 +34,6 @@ import {CompWithChildQuery, CompWithDirectiveChild} from './queries'; }) export class MainModule { constructor(public appRef: ApplicationRef) {} + + ngDoBootstrap() {} } diff --git a/modules/@angular/compiler/src/compile_metadata.ts b/modules/@angular/compiler/src/compile_metadata.ts index bb6e094cc9..8d20a0048f 100644 --- a/modules/@angular/compiler/src/compile_metadata.ts +++ b/modules/@angular/compiler/src/compile_metadata.ts @@ -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, @@ -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); diff --git a/modules/@angular/compiler/src/metadata_resolver.ts b/modules/@angular/compiler/src/metadata_resolver.ts index 60d54abaf2..22fee162ad 100644 --- a/modules/@angular/compiler/src/metadata_resolver.ts +++ b/modules/@angular/compiler/src/metadata_resolver.ts @@ -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, diff --git a/modules/@angular/compiler/src/ng_module_compiler.ts b/modules/@angular/compiler/src/ng_module_compiler.ts index 5a113508c0..3df44e03ff 100644 --- a/modules/@angular/compiler/src/ng_module_compiler.ts +++ b/modules/@angular/compiler/src/ng_module_compiler.ts @@ -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()]); diff --git a/modules/@angular/core/src/application_ref.ts b/modules/@angular/core/src/application_ref.ts index b0160efc80..01b5432a38 100644 --- a/modules/@angular/core/src/application_ref.ts +++ b/modules/@angular/core/src/application_ref.ts @@ -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 = >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) { + 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 unimplemented(); }; + /** + * Get a list of components registered to this application. + */ + get components(): ComponentRef[] { return []>unimplemented(); }; } @Injectable() @@ -620,4 +638,6 @@ export class ApplicationRef_ extends ApplicationRef { dispose(): void { this.ngOnDestroy(); } get componentTypes(): Type[] { return this._rootComponentTypes; } + + get components(): ComponentRef[] { return this._rootComponents; } } diff --git a/modules/@angular/core/src/linker/ng_module_factory.ts b/modules/@angular/core/src/linker/ng_module_factory.ts index d07a9ebeae..fc08933fcd 100644 --- a/modules/@angular/core/src/linker/ng_module_factory.ts +++ b/modules/@angular/core/src/linker/ng_module_factory.ts @@ -81,7 +81,9 @@ export abstract class NgModuleInjector extends CodegenComponentFactoryResolve public instance: T; - constructor(public parent: Injector, factories: ComponentFactory[]) { + constructor( + public parent: Injector, factories: ComponentFactory[], + public bootstrapFactories: ComponentFactory[]) { super(factories, parent.get(ComponentFactoryResolver, ComponentFactoryResolver.NULL)); } diff --git a/modules/@angular/core/src/metadata/ng_module.ts b/modules/@angular/core/src/metadata/ng_module.ts index 896808ddf4..744d52adc7 100644 --- a/modules/@angular/core/src/metadata/ng_module.ts +++ b/modules/@angular/core/src/metadata/ng_module.ts @@ -46,6 +46,7 @@ export interface NgModuleMetadataType { imports?: Array; exports?: Array; entryComponents?: Array; + bootstrap?: Array; schemas?: Array; } @@ -144,7 +145,14 @@ export class NgModuleMetadata extends InjectableMetadata implements NgModuleMeta */ entryComponents: Array; - schemas: Array; + /** + * Defines the components that should be bootstrapped when + * this module is bootstrapped. The components listed here + * will automatically be added to `entryComponents`. + */ + bootstrap: Array + + schemas: Array; 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; } } diff --git a/modules/@angular/core/test/application_ref_spec.ts b/modules/@angular/core/test/application_ref_spec.ts index 65bfdb693a..cb6cb1dd13 100644 --- a/modules/@angular/core/test/application_ref_spec.ts +++ b/modules/@angular/core/test/application_ref_spec.ts @@ -39,20 +39,34 @@ export function main() { errorLogger = new _ArrayLogger(); }); - function createModule(providers: any[] = []): ConcreteType { + type CreateModuleOptions = {providers?: any[], ngDoBootstrap?: any, bootstrap?: any[]}; + + function createModule(providers?: any[]): ConcreteType; + function createModule(options: CreateModuleOptions): ConcreteType; + function createModule(providersOrOptions: any[] | CreateModuleOptions): ConcreteType { + 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) { + (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', () => { diff --git a/modules/@angular/core/test/linker/ng_module_integration_spec.ts b/modules/@angular/core/test/linker/ng_module_integration_spec.ts index d666f6c1a5..b03ae13fc3 100644 --- a/modules/@angular/core/test/linker/ng_module_integration_spec.ts +++ b/modules/@angular/core/test/linker/ng_module_integration_spec.ts @@ -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 = >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', () => { diff --git a/modules/@angular/platform-browser-dynamic/index.ts b/modules/@angular/platform-browser-dynamic/index.ts index 65c0287dcf..7ed999fa33 100644 --- a/modules/@angular/platform-browser-dynamic/index.ts +++ b/modules/@angular/platform-browser-dynamic/index.ts @@ -169,7 +169,8 @@ export function bootstrap( 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( 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( providers: customProviders, declarations: declarations, imports: [WorkerAppModule], - entryComponents: [appComponentType] + bootstrap: [appComponentType] }) class DynamicModule { } @@ -244,7 +245,7 @@ export function bootstrapWorkerApp( 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]; }); } diff --git a/modules/@angular/platform-browser/test/browser/bootstrap_spec.ts b/modules/@angular/platform-browser/test/browser/bootstrap_spec.ts index 9bb8c62537..bb6471d01a 100644 --- a/modules/@angular/platform-browser/test/browser/bootstrap_spec.ts +++ b/modules/@angular/platform-browser/test/browser/bootstrap_spec.ts @@ -269,6 +269,7 @@ export function main() { ] }) class SomeModule { + ngDoBootstrap() {} } expect(log.result()).toEqual('platform_init1; platform_init2'); diff --git a/modules/@angular/platform-server/src/server.ts b/modules/@angular/platform-server/src/server.ts index e34dc45179..d816be10ca 100644 --- a/modules/@angular/platform-server/src/server.ts +++ b/modules/@angular/platform-server/src/server.ts @@ -110,7 +110,7 @@ export function serverBootstrap( providers: customProviders, declarations: declarations, imports: [BrowserModule], - entryComponents: [appComponentType] + bootstrap: [appComponentType] }) class DynamicModule { } @@ -121,6 +121,6 @@ export function serverBootstrap( 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]; }); } diff --git a/modules/@angular/upgrade/src/upgrade_adapter.ts b/modules/@angular/upgrade/src/upgrade_adapter.ts index df49f6c9fc..e80924bc21 100644 --- a/modules/@angular/upgrade/src/upgrade_adapter.ts +++ b/modules/@angular/upgrade/src/upgrade_adapter.ts @@ -286,6 +286,7 @@ export class UpgradeAdapter { @NgModule({providers: providers, imports: [BrowserModule]}) class DynamicModule { + ngDoBootstrap() {} } platformRef.bootstrapModule(DynamicModule).then((moduleRef) => { diff --git a/modules/playground/src/web_workers/router/index_common.ts b/modules/playground/src/web_workers/router/index_common.ts index d8072d0fc6..6f150d995a 100644 --- a/modules/playground/src/web_workers/router/index_common.ts +++ b/modules/playground/src/web_workers/router/index_common.ts @@ -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); - }); - } }