refactor(core): introduce `APP_BOOTSTRAP_LISTENER` multi provider

Using the `registerBootstrapListener` easily lead to race condition
and needed dependencies on `ApplicationRef`.

BREAKING CHANGE:
- `ApplicationRef.registerBootstrapListener` is deprecated. Provide a multi
  provider for the new token `APP_BOOTSTRAP_LISTENER` instead.
This commit is contained in:
Tobias Bosch 2016-08-02 05:22:44 -07:00
parent 8e6091de6c
commit af2e80e068
6 changed files with 88 additions and 50 deletions

View File

@ -15,7 +15,7 @@ export * from './src/metadata';
export * from './src/util'; export * from './src/util';
export * from './src/di'; export * from './src/di';
export {createPlatform, assertPlatform, disposePlatform, getPlatform, coreBootstrap, coreLoadAndBootstrap, PlatformRef, ApplicationRef, enableProdMode, lockRunMode, isDevMode, createPlatformFactory} from './src/application_ref'; 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/zone';
export * from './src/render'; export * from './src/render';
export * from './src/linker'; export * from './src/linker';

View File

@ -11,7 +11,7 @@ import {ListWrapper} from '../src/facade/collection';
import {BaseException, ExceptionHandler, unimplemented} from '../src/facade/exceptions'; import {BaseException, ExceptionHandler, unimplemented} from '../src/facade/exceptions';
import {ConcreteType, Type, isBlank, isPresent, isPromise} from '../src/facade/lang'; 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 {ChangeDetectorRef} from './change_detection/change_detector_ref';
import {Console} from './console'; import {Console} from './console';
import {Inject, Injectable, Injector, OpaqueToken, Optional, ReflectiveInjector, SkipSelf, forwardRef} from './di'; 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 * Register a listener to be called each time `bootstrap()` is called to bootstrap
* a new root component. * a new root component.
*
* @deprecated Provide a callback via a multi provider for {@link APP_BOOTSTRAP_LISTENER}
* instead.
*/ */
abstract registerBootstrapListener(listener: (ref: ComponentRef<any>) => void): void; abstract registerBootstrapListener(listener: (ref: ComponentRef<any>) => void): void;
@ -503,6 +506,9 @@ export class ApplicationRef_ extends ApplicationRef {
this._zone.onMicrotaskEmpty, (_) => { this._zone.run(() => { this.tick(); }); }); this._zone.onMicrotaskEmpty, (_) => { this._zone.run(() => { this.tick(); }); });
} }
/**
* @deprecated
*/
registerBootstrapListener(listener: (ref: ComponentRef<any>) => void): void { registerBootstrapListener(listener: (ref: ComponentRef<any>) => void): void {
this._bootstrapListeners.push(listener); this._bootstrapListeners.push(listener);
} }
@ -564,7 +570,11 @@ export class ApplicationRef_ extends ApplicationRef {
this._changeDetectorRefs.push(componentRef.changeDetectorRef); this._changeDetectorRefs.push(componentRef.changeDetectorRef);
this.tick(); this.tick();
this._rootComponents.push(componentRef); this._rootComponents.push(componentRef);
this._bootstrapListeners.forEach((listener) => listener(componentRef)); // Get the listeners lazily to prevent DI cycles.
const listeners =
<((compRef: ComponentRef<any>) => void)[]>this._injector.get(APP_BOOTSTRAP_LISTENER, [])
.concat(this._bootstrapListeners);
listeners.forEach((listener) => listener(componentRef));
} }
/** @internal */ /** @internal */

View File

@ -53,6 +53,13 @@ export const PLATFORM_INITIALIZER: any = new OpaqueToken('Platform Initializer')
*/ */
export const APP_INITIALIZER: any = new OpaqueToken('Application 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 * A token which indicates the root directory of the application
* @experimental * @experimental

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * 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 {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref';
import {Console} from '@angular/core/src/console'; import {Console} from '@angular/core/src/console';
import {ComponentRef} from '@angular/core/src/linker/component_factory'; import {ComponentRef} from '@angular/core/src/linker/component_factory';
@ -57,48 +57,71 @@ export function main() {
} }
describe('ApplicationRef', () => { describe('ApplicationRef', () => {
var ref: ApplicationRef_;
beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); }); beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); });
beforeEach(inject([ApplicationRef], (_ref: ApplicationRef_) => { ref = _ref; }));
it('should throw when reentering tick', () => { it('should throw when reentering tick', inject([ApplicationRef], (ref: ApplicationRef_) => {
var cdRef = <any>new SpyChangeDetectorRef(); var cdRef = <any>new SpyChangeDetectorRef();
try { try {
ref.registerChangeDetector(cdRef); ref.registerChangeDetector(cdRef);
cdRef.spy('detectChanges').andCallFake(() => ref.tick()); cdRef.spy('detectChanges').andCallFake(() => ref.tick());
expect(() => ref.tick()).toThrowError('ApplicationRef.tick is called recursively'); expect(() => ref.tick()).toThrowError('ApplicationRef.tick is called recursively');
} finally { } finally {
ref.unregisterChangeDetector(cdRef); ref.unregisterChangeDetector(cdRef);
} }
}); }));
describe('run', () => { describe('run', () => {
it('should rethrow errors even if the exceptionHandler is not rethrowing', () => { it('should rethrow errors even if the exceptionHandler is not rethrowing',
expect(() => ref.run(() => { throw new BaseException('Test'); })).toThrowError('Test'); 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', it('should return a promise with rejected errors even if the exceptionHandler is not rethrowing',
async(() => { async(inject([ApplicationRef], (ref: ApplicationRef_) => {
var promise: Promise<any> = ref.run(() => Promise.reject('Test')); var promise: Promise<any> = ref.run(() => Promise.reject('Test'));
promise.then(() => expect(false).toBe(true), (e) => { expect(e).toEqual('Test'); }); promise.then(() => expect(false).toBe(true), (e) => { expect(e).toEqual('Test'); });
})); })));
}); });
describe('registerBootstrapListener', () => { describe('registerBootstrapListener', () => {
it('should be called when a component is bootstrapped', () => { it('should be called when a component is bootstrapped',
const capturedCompRefs: ComponentRef<any>[] = []; inject([ApplicationRef], (ref: ApplicationRef_) => {
ref.registerBootstrapListener((compRef) => capturedCompRefs.push(compRef)); const capturedCompRefs: ComponentRef<any>[] = [];
const compRef = ref.bootstrap(SomeComponent); ref.registerBootstrapListener((compRef) => capturedCompRefs.push(compRef));
expect(capturedCompRefs).toEqual([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<any>[] = [];
const compRef = ref.bootstrap(SomeComponent);
expect(capturedCompRefs).toEqual([compRef]);
}));
});
describe('APP_BOOTSTRAP_LISTENER', () => {
let capturedCompRefs: ComponentRef<any>[];
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', () => { it('should be called when a component is bootstrapped',
ref.registerBootstrapListener((compRef) => capturedCompRefs.push(compRef)); inject([ApplicationRef], (ref: ApplicationRef_) => {
const capturedCompRefs: ComponentRef<any>[] = []; const compRef = ref.bootstrap(SomeComponent);
const compRef = ref.bootstrap(SomeComponent); expect(capturedCompRefs).toEqual([compRef]);
expect(capturedCompRefs).toEqual([compRef]); }));
});
}); });
}); });
describe('bootstrapModule', () => { describe('bootstrapModule', () => {

View File

@ -7,7 +7,7 @@
*/ */
import {Location, LocationStrategy, PathLocationStrategy} from '@angular/common'; 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 {Routes} from './config';
import {Router} from './router'; import {Router} from './router';
@ -53,12 +53,8 @@ export function rootRoute(router: Router): ActivatedRoute {
return router.routerState.root; return router.routerState.root;
} }
export function setupRouterInitializer(injector: Injector) { export function initialRouterNavigation(router: Router) {
return () => { return () => { router.initialNavigation(); };
injector.get(ApplicationRef).registerBootstrapListener(() => {
injector.get(Router).initialNavigation();
});
};
} }
/** /**
@ -102,11 +98,19 @@ export function provideRouter(routes: Routes, config: ExtraOptions = {}): any[]
RouterOutletMap, {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]}, RouterOutletMap, {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]},
// Trigger initial navigation // Trigger initial navigation
{provide: APP_INITIALIZER, multi: true, useFactory: setupRouterInitializer, deps: [Injector]}, provideRouterInitializer(), {provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader}
{provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader}
]; ];
} }
export function provideRouterInitializer() {
return {
provide: APP_BOOTSTRAP_LISTENER,
multi: true,
useFactory: initialRouterNavigation,
deps: [Router]
};
}
/** /**
* Router configuration. * Router configuration.
* *

View File

@ -9,7 +9,7 @@
import {APP_BASE_HREF, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common'; 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 {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 {Routes} from './config';
import {RouterLink, RouterLinkWithHref} from './directives/router_link'; import {RouterLink, RouterLinkWithHref} from './directives/router_link';
import {RouterLinkActive} from './directives/router_link_active'; import {RouterLinkActive} from './directives/router_link_active';
@ -76,13 +76,6 @@ export const ROUTER_PROVIDERS: any[] = [
*/ */
@NgModule({declarations: ROUTER_DIRECTIVES, exports: ROUTER_DIRECTIVES}) @NgModule({declarations: ROUTER_DIRECTIVES, exports: ROUTER_DIRECTIVES})
export class RouterModule { export class RouterModule {
constructor(private injector: Injector, appRef: ApplicationRef) {
// do the initialization only once
if ((<any>injector).parent.get(RouterModule, null)) return;
appRef.registerBootstrapListener(() => { injector.get(Router).initialNavigation(); });
}
static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders { static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders {
return { return {
ngModule: RouterModule, ngModule: RouterModule,
@ -94,7 +87,8 @@ export class RouterModule {
deps: [ deps: [
PlatformLocation, [new Inject(APP_BASE_HREF), new Optional()], ROUTER_CONFIGURATION PlatformLocation, [new Inject(APP_BASE_HREF), new Optional()], ROUTER_CONFIGURATION
] ]
} },
provideRouterInitializer()
] ]
}; };
} }