diff --git a/modules/@angular/core/index.ts b/modules/@angular/core/index.ts index bf0a3f27a5..555f067afb 100644 --- a/modules/@angular/core/index.ts +++ b/modules/@angular/core/index.ts @@ -15,7 +15,7 @@ export * from './src/metadata'; export * from './src/util'; export * from './src/di'; export {createPlatform, assertPlatform, disposePlatform, getPlatform, coreBootstrap, coreLoadAndBootstrap, PlatformRef, ApplicationRef, enableProdMode, lockRunMode, isDevMode, createPlatformFactory} from './src/application_ref'; -export {APP_ID, APP_INITIALIZER, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER} from './src/application_tokens'; +export {APP_ID, APP_INITIALIZER, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER, APP_BOOTSTRAP_LISTENER} from './src/application_tokens'; export * from './src/zone'; export * from './src/render'; export * from './src/linker'; diff --git a/modules/@angular/core/src/application_ref.ts b/modules/@angular/core/src/application_ref.ts index 2db0c4dab3..b0160efc80 100644 --- a/modules/@angular/core/src/application_ref.ts +++ b/modules/@angular/core/src/application_ref.ts @@ -11,7 +11,7 @@ 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 {APP_INITIALIZER, PLATFORM_INITIALIZER} from './application_tokens'; +import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, PLATFORM_INITIALIZER} from './application_tokens'; import {ChangeDetectorRef} from './change_detection/change_detector_ref'; import {Console} from './console'; import {Inject, Injectable, Injector, OpaqueToken, Optional, ReflectiveInjector, SkipSelf, forwardRef} from './di'; @@ -400,6 +400,9 @@ export abstract class ApplicationRef { /** * Register a listener to be called each time `bootstrap()` is called to bootstrap * a new root component. + * + * @deprecated Provide a callback via a multi provider for {@link APP_BOOTSTRAP_LISTENER} + * instead. */ abstract registerBootstrapListener(listener: (ref: ComponentRef) => void): void; @@ -503,6 +506,9 @@ export class ApplicationRef_ extends ApplicationRef { this._zone.onMicrotaskEmpty, (_) => { this._zone.run(() => { this.tick(); }); }); } + /** + * @deprecated + */ registerBootstrapListener(listener: (ref: ComponentRef) => void): void { this._bootstrapListeners.push(listener); } @@ -564,7 +570,11 @@ export class ApplicationRef_ extends ApplicationRef { this._changeDetectorRefs.push(componentRef.changeDetectorRef); this.tick(); this._rootComponents.push(componentRef); - this._bootstrapListeners.forEach((listener) => listener(componentRef)); + // Get the listeners lazily to prevent DI cycles. + const listeners = + <((compRef: ComponentRef) => void)[]>this._injector.get(APP_BOOTSTRAP_LISTENER, []) + .concat(this._bootstrapListeners); + listeners.forEach((listener) => listener(componentRef)); } /** @internal */ diff --git a/modules/@angular/core/src/application_tokens.ts b/modules/@angular/core/src/application_tokens.ts index 776d9ca508..a5f5cb00b5 100644 --- a/modules/@angular/core/src/application_tokens.ts +++ b/modules/@angular/core/src/application_tokens.ts @@ -53,6 +53,13 @@ export const PLATFORM_INITIALIZER: any = new OpaqueToken('Platform Initializer') */ export const APP_INITIALIZER: any = new OpaqueToken('Application Initializer'); +/** + * All callbacks provided via this token will be called when a component has been bootstrapped. + * + * @experimental + */ +export const APP_BOOTSTRAP_LISTENER = new OpaqueToken('appBootstrapListener'); + /** * A token which indicates the root directory of the application * @experimental diff --git a/modules/@angular/core/test/application_ref_spec.ts b/modules/@angular/core/test/application_ref_spec.ts index daed5942e6..65bfdb693a 100644 --- a/modules/@angular/core/test/application_ref_spec.ts +++ b/modules/@angular/core/test/application_ref_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {APP_INITIALIZER, ChangeDetectorRef, CompilerFactory, Component, Injector, NgModule, PlatformRef} from '@angular/core'; +import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ChangeDetectorRef, CompilerFactory, Component, Injector, NgModule, PlatformRef} from '@angular/core'; import {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref'; import {Console} from '@angular/core/src/console'; import {ComponentRef} from '@angular/core/src/linker/component_factory'; @@ -57,48 +57,71 @@ export function main() { } describe('ApplicationRef', () => { - var ref: ApplicationRef_; beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); }); - beforeEach(inject([ApplicationRef], (_ref: ApplicationRef_) => { ref = _ref; })); - it('should throw when reentering tick', () => { - var cdRef = new SpyChangeDetectorRef(); - try { - ref.registerChangeDetector(cdRef); - cdRef.spy('detectChanges').andCallFake(() => ref.tick()); - expect(() => ref.tick()).toThrowError('ApplicationRef.tick is called recursively'); - } finally { - ref.unregisterChangeDetector(cdRef); - } - }); + it('should throw when reentering tick', inject([ApplicationRef], (ref: ApplicationRef_) => { + var cdRef = new SpyChangeDetectorRef(); + try { + ref.registerChangeDetector(cdRef); + cdRef.spy('detectChanges').andCallFake(() => ref.tick()); + expect(() => ref.tick()).toThrowError('ApplicationRef.tick is called recursively'); + } finally { + ref.unregisterChangeDetector(cdRef); + } + })); describe('run', () => { - it('should rethrow errors even if the exceptionHandler is not rethrowing', () => { - expect(() => ref.run(() => { throw new BaseException('Test'); })).toThrowError('Test'); - }); + it('should rethrow errors even if the exceptionHandler is not rethrowing', + inject([ApplicationRef], (ref: ApplicationRef_) => { + expect(() => ref.run(() => { throw new BaseException('Test'); })).toThrowError('Test'); + })); it('should return a promise with rejected errors even if the exceptionHandler is not rethrowing', - async(() => { + async(inject([ApplicationRef], (ref: ApplicationRef_) => { var promise: Promise = ref.run(() => Promise.reject('Test')); promise.then(() => expect(false).toBe(true), (e) => { expect(e).toEqual('Test'); }); - })); + }))); }); describe('registerBootstrapListener', () => { - it('should be called when a component is bootstrapped', () => { - const capturedCompRefs: ComponentRef[] = []; - ref.registerBootstrapListener((compRef) => capturedCompRefs.push(compRef)); - const compRef = ref.bootstrap(SomeComponent); - expect(capturedCompRefs).toEqual([compRef]); + it('should be called when a component is bootstrapped', + inject([ApplicationRef], (ref: ApplicationRef_) => { + const capturedCompRefs: ComponentRef[] = []; + ref.registerBootstrapListener((compRef) => capturedCompRefs.push(compRef)); + const compRef = ref.bootstrap(SomeComponent); + expect(capturedCompRefs).toEqual([compRef]); + })); + + it('should be called immediately when a component was bootstrapped before', + inject([ApplicationRef], (ref: ApplicationRef_) => { + ref.registerBootstrapListener((compRef) => capturedCompRefs.push(compRef)); + const capturedCompRefs: ComponentRef[] = []; + const compRef = ref.bootstrap(SomeComponent); + expect(capturedCompRefs).toEqual([compRef]); + })); + }); + + describe('APP_BOOTSTRAP_LISTENER', () => { + let capturedCompRefs: ComponentRef[]; + beforeEach(() => { + capturedCompRefs = []; + TestBed.configureTestingModule({ + providers: [{ + provide: APP_BOOTSTRAP_LISTENER, + multi: true, + useValue: (compRef: any) => { capturedCompRefs.push(compRef); } + }] + }); }); - it('should be called immediately when a component was bootstrapped before', () => { - ref.registerBootstrapListener((compRef) => capturedCompRefs.push(compRef)); - const capturedCompRefs: ComponentRef[] = []; - const compRef = ref.bootstrap(SomeComponent); - expect(capturedCompRefs).toEqual([compRef]); - }); + it('should be called when a component is bootstrapped', + inject([ApplicationRef], (ref: ApplicationRef_) => { + const compRef = ref.bootstrap(SomeComponent); + expect(capturedCompRefs).toEqual([compRef]); + })); + }); + }); describe('bootstrapModule', () => { diff --git a/modules/@angular/router/src/common_router_providers.ts b/modules/@angular/router/src/common_router_providers.ts index ba4e0068ec..e41ec965cb 100644 --- a/modules/@angular/router/src/common_router_providers.ts +++ b/modules/@angular/router/src/common_router_providers.ts @@ -7,7 +7,7 @@ */ import {Location, LocationStrategy, PathLocationStrategy} from '@angular/common'; -import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_INITIALIZER, ApplicationRef, ComponentResolver, Injector, NgModuleFactoryLoader, OpaqueToken, SystemJsNgModuleLoader} from '@angular/core'; +import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, ComponentResolver, Injector, NgModuleFactoryLoader, OpaqueToken, SystemJsNgModuleLoader} from '@angular/core'; import {Routes} from './config'; import {Router} from './router'; @@ -53,12 +53,8 @@ export function rootRoute(router: Router): ActivatedRoute { return router.routerState.root; } -export function setupRouterInitializer(injector: Injector) { - return () => { - injector.get(ApplicationRef).registerBootstrapListener(() => { - injector.get(Router).initialNavigation(); - }); - }; +export function initialRouterNavigation(router: Router) { + return () => { router.initialNavigation(); }; } /** @@ -102,11 +98,19 @@ export function provideRouter(routes: Routes, config: ExtraOptions = {}): any[] RouterOutletMap, {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]}, // Trigger initial navigation - {provide: APP_INITIALIZER, multi: true, useFactory: setupRouterInitializer, deps: [Injector]}, - {provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader} + provideRouterInitializer(), {provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader} ]; } +export function provideRouterInitializer() { + return { + provide: APP_BOOTSTRAP_LISTENER, + multi: true, + useFactory: initialRouterNavigation, + deps: [Router] + }; +} + /** * Router configuration. * diff --git a/modules/@angular/router/src/router_module.ts b/modules/@angular/router/src/router_module.ts index d856e8c93b..646d47f545 100644 --- a/modules/@angular/router/src/router_module.ts +++ b/modules/@angular/router/src/router_module.ts @@ -9,7 +9,7 @@ import {APP_BASE_HREF, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common'; import {ApplicationRef, ComponentResolver, Inject, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, OpaqueToken, Optional, SystemJsNgModuleLoader} from '@angular/core'; -import {ExtraOptions, ROUTER_CONFIGURATION, provideRouterConfig, provideRoutes, rootRoute, setupRouter} from './common_router_providers'; +import {ExtraOptions, ROUTER_CONFIGURATION, provideRouterConfig, provideRouterInitializer, provideRoutes, rootRoute, setupRouter} from './common_router_providers'; import {Routes} from './config'; import {RouterLink, RouterLinkWithHref} from './directives/router_link'; import {RouterLinkActive} from './directives/router_link_active'; @@ -76,13 +76,6 @@ export const ROUTER_PROVIDERS: any[] = [ */ @NgModule({declarations: ROUTER_DIRECTIVES, exports: ROUTER_DIRECTIVES}) export class RouterModule { - constructor(private injector: Injector, appRef: ApplicationRef) { - // do the initialization only once - if ((injector).parent.get(RouterModule, null)) return; - - appRef.registerBootstrapListener(() => { injector.get(Router).initialNavigation(); }); - } - static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders { return { ngModule: RouterModule, @@ -94,7 +87,8 @@ export class RouterModule { deps: [ PlatformLocation, [new Inject(APP_BASE_HREF), new Optional()], ROUTER_CONFIGURATION ] - } + }, + provideRouterInitializer() ] }; }