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/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';

View File

@ -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<any>) => void): void;
@ -503,6 +506,9 @@ export class ApplicationRef_ extends ApplicationRef {
this._zone.onMicrotaskEmpty, (_) => { this._zone.run(() => { this.tick(); }); });
}
/**
* @deprecated
*/
registerBootstrapListener(listener: (ref: ComponentRef<any>) => 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<any>) => void)[]>this._injector.get(APP_BOOTSTRAP_LISTENER, [])
.concat(this._bootstrapListeners);
listeners.forEach((listener) => listener(componentRef));
}
/** @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');
/**
* 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

View File

@ -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 = <any>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 = <any>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<any> = 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<any>[] = [];
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<any>[] = [];
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<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', () => {
ref.registerBootstrapListener((compRef) => capturedCompRefs.push(compRef));
const capturedCompRefs: ComponentRef<any>[] = [];
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', () => {

View File

@ -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.
*

View File

@ -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 ((<any>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()
]
};
}