From 5df998d086b3d279619d6ed435c72352d7edd8dc Mon Sep 17 00:00:00 2001 From: Victor Savkin Date: Tue, 7 Mar 2017 17:27:20 -0500 Subject: [PATCH] fix(router): do not finish bootstrap until all the routes are resolved (#14762) DEPRECATION: Use `RouterModule.forRoot(routes, {initialNavigation: 'enabled'})` instead of `RouterModule.forRoot(routes, {initialNavigtaion: true})`. Before doing this, move the initialization logic affecting the router from the bootstrapped component to the boostrapped module. Similarly, use `RouterModule.forRoot(routes, {initialNavigation: 'disabled'})` instead of `RouterModule.forRoot(routes, {initialNavigation: false})`. Deprecated options: 'legacy_enabled', `true` (same as 'legacy_enabled'), 'legacy_disabled', `false` (same as 'legacy_disabled'). The "Router Initial Navigation" design document covers this change. Read more here: https://docs.google.com/document/d/1Hlw1fPaVs-PCj5KPeJRKhrQGAvFOxdvTlwAcnZosu5A/edit?usp=sharing --- .../common/src/location/platform_location.ts | 7 + .../web_workers/worker/location_providers.ts | 14 +- .../web_workers/worker/platform_location.ts | 4 + modules/@angular/router/src/router.ts | 55 +++- .../router/src/router_config_loader.ts | 11 +- modules/@angular/router/src/router_module.ts | 141 ++++++++-- .../@angular/router/test/bootstrap.spec.ts | 244 ++++++++++++++++++ .../router/test/router_module.spec.ts | 91 ------- modules/@angular/router/upgrade.ts | 46 ++-- .../src/web_workers/router/index_common.ts | 4 +- .../common/typings/common.d.ts | 3 + .../typings/platform-webworker.d.ts | 4 + .../router/typings/router.d.ts | 2 +- 13 files changed, 468 insertions(+), 158 deletions(-) create mode 100644 modules/@angular/router/test/bootstrap.spec.ts delete mode 100644 modules/@angular/router/test/router_module.spec.ts diff --git a/modules/@angular/common/src/location/platform_location.ts b/modules/@angular/common/src/location/platform_location.ts index 4983c96fe5..985935250f 100644 --- a/modules/@angular/common/src/location/platform_location.ts +++ b/modules/@angular/common/src/location/platform_location.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {InjectionToken} from '@angular/core'; /** * This class should not be used directly by an application developer. Instead, use * {@link Location}. @@ -50,6 +51,12 @@ export abstract class PlatformLocation { abstract back(): void; } +/** + * @whatItDoes indicates when a location is initialized + * @experimental + */ +export const LOCATION_INITIALIZED = new InjectionToken>('Location Initialized'); + /** * A serializable version of the event from onPopState or onHashChange * diff --git a/modules/@angular/platform-webworker/src/web_workers/worker/location_providers.ts b/modules/@angular/platform-webworker/src/web_workers/worker/location_providers.ts index 5eabfd11c4..6db571baa8 100644 --- a/modules/@angular/platform-webworker/src/web_workers/worker/location_providers.ts +++ b/modules/@angular/platform-webworker/src/web_workers/worker/location_providers.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {PlatformLocation} from '@angular/common'; +import {LOCATION_INITIALIZED, PlatformLocation} from '@angular/common'; import {APP_INITIALIZER, InjectionToken, NgZone} from '@angular/core'; import {WebWorkerPlatformLocation} from './platform_location'; @@ -18,16 +18,20 @@ import {WebWorkerPlatformLocation} from './platform_location'; * @experimental */ export const WORKER_APP_LOCATION_PROVIDERS = [ - {provide: PlatformLocation, useClass: WebWorkerPlatformLocation}, - { + {provide: PlatformLocation, useClass: WebWorkerPlatformLocation}, { provide: APP_INITIALIZER, useFactory: appInitFnFactory, multi: true, - deps: [PlatformLocation, NgZone], + deps: [PlatformLocation, NgZone] }, + {provide: LOCATION_INITIALIZED, useFactory: locationInitialized, deps: [PlatformLocation]} ]; -function appInitFnFactory(platformLocation: WebWorkerPlatformLocation, zone: NgZone): () => +export function locationInitialized(platformLocation: WebWorkerPlatformLocation) { + return platformLocation.initialized; +} + +export function appInitFnFactory(platformLocation: WebWorkerPlatformLocation, zone: NgZone): () => Promise { return () => zone.runGuarded(() => platformLocation.init()); } diff --git a/modules/@angular/platform-webworker/src/web_workers/worker/platform_location.ts b/modules/@angular/platform-webworker/src/web_workers/worker/platform_location.ts index 4441d22da5..05c93a47a3 100644 --- a/modules/@angular/platform-webworker/src/web_workers/worker/platform_location.ts +++ b/modules/@angular/platform-webworker/src/web_workers/worker/platform_location.ts @@ -20,6 +20,8 @@ export class WebWorkerPlatformLocation extends PlatformLocation { private _hashChangeListeners: Array = []; private _location: LocationType = null; private _channelSource: EventEmitter; + public initialized: Promise; + private initializedResolve: () => void; constructor( brokerFactory: ClientMessageBrokerFactory, bus: MessageBus, private _serializer: Serializer) { @@ -46,6 +48,7 @@ export class WebWorkerPlatformLocation extends PlatformLocation { } } }); + this.initialized = new Promise(res => this.initializedResolve = res); } /** @internal **/ @@ -56,6 +59,7 @@ export class WebWorkerPlatformLocation extends PlatformLocation { .then( (val: LocationType) => { this._location = val; + this.initializedResolve(); return true; }, err => { throw new Error(err); }); diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index a0bf1fd8d0..3c13e92d82 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -180,6 +180,18 @@ type NavigationParams = { source: NavigationSource, }; +/** + * @internal + */ +export type RouterHook = (snapshot: RouterStateSnapshot) => Observable; + +/** + * @internal + */ +function defaultRouterHook(snapshot: RouterStateSnapshot): Observable { + return of (null); +} + /** * Does not detach any subtrees. Reuses routes as long as their route config is the same. */ @@ -221,11 +233,23 @@ export class Router { */ errorHandler: ErrorHandler = defaultErrorHandler; + + /** * Indicates if at least one navigation happened. */ navigated: boolean = false; + /** + * Used by RouterModule. This allows us to + * pause the navigation either before preactivation or after it. + * @internal + */ + hooks: {beforePreactivation: RouterHook, afterPreactivation: RouterHook} = { + beforePreactivation: defaultRouterHook, + afterPreactivation: defaultRouterHook + }; + /** * Extracts and merges URLs. Used for AngularJS to Angular migrations. */ @@ -602,18 +626,25 @@ export class Router { urlAndSnapshot$ = of ({appliedUrl: url, snapshot: precreatedState}); } + const beforePreactivationDone$ = mergeMap.call( + urlAndSnapshot$, (p: {appliedUrl: string, snapshot: RouterStateSnapshot}) => { + return map.call(this.hooks.beforePreactivation(p.snapshot), () => p); + }); // run preactivation: guards and data resolvers let preActivation: PreActivation; - const preactivationTraverse$ = map.call(urlAndSnapshot$, ({appliedUrl, snapshot}: any) => { - preActivation = - new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector); - preActivation.traverse(this.outletMap); - return {appliedUrl, snapshot}; - }); + const preactivationTraverse$ = map.call( + beforePreactivationDone$, + ({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => { + preActivation = + new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector); + preActivation.traverse(this.outletMap); + return {appliedUrl, snapshot}; + }); - const preactivationCheckGuards = - mergeMap.call(preactivationTraverse$, ({appliedUrl, snapshot}: any) => { + const preactivationCheckGuards$ = mergeMap.call( + preactivationTraverse$, + ({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => { if (this.navigationId !== id) return of (false); return map.call(preActivation.checkGuards(), (shouldActivate: boolean) => { @@ -621,7 +652,7 @@ export class Router { }); }); - const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards, (p: any) => { + const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards$, (p: any) => { if (this.navigationId !== id) return of (false); if (p.shouldActivate) { @@ -631,11 +662,15 @@ export class Router { } }); + const preactivationDone$ = mergeMap.call(preactivationResolveData$, (p: any) => { + return map.call(this.hooks.afterPreactivation(p.snapshot), () => p); + }); + // create router state // this operation has side effects => route state is being affected const routerState$ = - map.call(preactivationResolveData$, ({appliedUrl, snapshot, shouldActivate}: any) => { + map.call(preactivationDone$, ({appliedUrl, snapshot, shouldActivate}: any) => { if (shouldActivate) { const state = createRouterState(this.routeReuseStrategy, snapshot, this.currentRouterState); diff --git a/modules/@angular/router/src/router_config_loader.ts b/modules/@angular/router/src/router_config_loader.ts index bb9bd45313..0e598b8c54 100644 --- a/modules/@angular/router/src/router_config_loader.ts +++ b/modules/@angular/router/src/router_config_loader.ts @@ -58,10 +58,13 @@ export class RouterConfigLoader { if (typeof loadChildren === 'string') { return fromPromise(this.loader.load(loadChildren)); } else { - const offlineMode = this.compiler instanceof Compiler; - return mergeMap.call( - wrapIntoObservable(loadChildren()), - (t: any) => offlineMode ? of (t) : fromPromise(this.compiler.compileModuleAsync(t))); + return mergeMap.call(wrapIntoObservable(loadChildren()), (t: any) => { + if (t instanceof NgModuleFactory) { + return of (t); + } else { + return fromPromise(this.compiler.compileModuleAsync(t)); + } + }); } } } diff --git a/modules/@angular/router/src/router_module.ts b/modules/@angular/router/src/router_module.ts index 53552070e3..9cd30c44b9 100644 --- a/modules/@angular/router/src/router_module.ts +++ b/modules/@angular/router/src/router_module.ts @@ -6,9 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {APP_BASE_HREF, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common'; -import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, ApplicationRef, Compiler, ComponentRef, Inject, InjectionToken, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core'; +import {APP_BASE_HREF, HashLocationStrategy, LOCATION_INITIALIZED, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common'; +import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, Compiler, ComponentRef, Inject, Injectable, InjectionToken, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core'; import {ɵgetDOM as getDOM} from '@angular/platform-browser'; +import {Subject} from 'rxjs/Subject'; +import {of } from 'rxjs/observable/of'; import {Route, Routes} from './config'; import {RouterLink, RouterLinkWithHref} from './directives/router_link'; @@ -19,7 +21,7 @@ import {ErrorHandler, Router} from './router'; import {ROUTES} from './router_config_loader'; import {RouterOutletMap} from './router_outlet_map'; import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader'; -import {ActivatedRoute} from './router_state'; +import {ActivatedRoute, RouterStateSnapshot} from './router_state'; import {UrlHandlingStrategy} from './url_handling_strategy'; import {DefaultUrlSerializer, UrlSerializer} from './url_tree'; import {flatten} from './utils/collection'; @@ -208,6 +210,32 @@ export function provideRoutes(routes: Routes): any { ]; } +/** + * @whatItDoes Represents an option to configure when the initial navigation is performed. + * + * @description + * * 'enabled' - the initial navigation starts before the root component is created. + * The bootstrap is blocked until the initial navigation is complete. + * * 'disabled' - the initial navigation is not performed. The location listener is set up before + * the root component gets created. + * * 'legacy_enabled'- the initial navigation starts after the root component has been created. + * The bootstrap is not blocked until the initial navigation is complete. @deprecated + * * 'legacy_disabled'- the initial navigation is not performed. The location listener is set up + * after @deprecated + * the root component gets created. + * * `true` - same as 'legacy_enabled'. @deprecated + * * `false` - same as 'legacy_disabled'. @deprecated + * + * The 'enabled' option should be used for applications unless there is a reason to have + * more control over when the router starts its initial navigation due to some complex + * initialization logic. In this case, 'disabled' should be used. + * + * The 'legacy_enabled' and 'legacy_disabled' should not be used for new applications. + * + * @experimental + */ +export type InitialNavigation = + true | false | 'enabled' | 'disabled' | 'legacy_enabled' | 'legacy_disabled'; /** * @whatItDoes Represents options to configure the router. @@ -228,7 +256,7 @@ export interface ExtraOptions { /** * Disables the initial navigation. */ - initialNavigation?: boolean; + initialNavigation?: InitialNavigation; /** * A custom error handler. @@ -278,22 +306,100 @@ export function rootRoute(router: Router): ActivatedRoute { return router.routerState.root; } -export function initialRouterNavigation( - router: Router, ref: ApplicationRef, preloader: RouterPreloader, opts: ExtraOptions) { - return (bootstrappedComponentRef: ComponentRef) => { +/** + * To initialize the router properly we need to do in two steps: + * + * We need to start the navigation in a APP_INITIALIZER to block the bootstrap if + * a resolver or a guards executes asynchronously. Second, we need to actually run + * activation in a BOOTSTRAP_LISTENER. We utilize the afterPreactivation + * hook provided by the router to do that. + * + * The router navigation starts, reaches the point when preactivation is done, and then + * pauses. It waits for the hook to be resolved. We then resolve it only in a bootstrap listener. + */ +@Injectable() +export class RouterInitializer { + private initNavigation: boolean = false; + private resultOfPreactivationDone = new Subject(); + + constructor(private injector: Injector) {} + + appInitializer(): Promise { + const p: Promise = this.injector.get(LOCATION_INITIALIZED, Promise.resolve(null)); + return p.then(() => { + let resolve: Function = null; + const res = new Promise(r => resolve = r); + const router = this.injector.get(Router); + const opts = this.injector.get(ROUTER_CONFIGURATION); + + if (this.isLegacyDisabled(opts) || this.isLegacyEnabled(opts)) { + resolve(true); + + } else if (opts.initialNavigation === 'disabled') { + router.setUpLocationChangeListener(); + resolve(true); + + } else if (opts.initialNavigation === 'enabled') { + router.hooks.afterPreactivation = () => { + // only the initial navigation should be delayed + if (!this.initNavigation) { + this.initNavigation = true; + resolve(true); + return this.resultOfPreactivationDone; + + // subsequent navigations should not be delayed + } else { + return of (null); + } + }; + router.initialNavigation(); + + } else { + throw new Error(`Invalid initialNavigation options: '${opts.initialNavigation}'`); + } + + return res; + }); + } + + bootstrapListener(bootstrappedComponentRef: ComponentRef): void { + const opts = this.injector.get(ROUTER_CONFIGURATION); + const preloader = this.injector.get(RouterPreloader); + const router = this.injector.get(Router); + const ref = this.injector.get(ApplicationRef); if (bootstrappedComponentRef !== ref.components[0]) { return; } - router.resetRootComponentType(ref.componentTypes[0]); - preloader.setUpPreloading(); - if (opts.initialNavigation === false) { - router.setUpLocationChangeListener(); - } else { + if (this.isLegacyEnabled(opts)) { router.initialNavigation(); + } else if (this.isLegacyDisabled(opts)) { + router.setUpLocationChangeListener(); } - }; + + preloader.setUpPreloading(); + router.resetRootComponentType(ref.componentTypes[0]); + this.resultOfPreactivationDone.next(null); + this.resultOfPreactivationDone.complete(); + } + + private isLegacyEnabled(opts: ExtraOptions): boolean { + return opts.initialNavigation === 'legacy_enabled' || opts.initialNavigation === true || + opts.initialNavigation === undefined; + } + + private isLegacyDisabled(opts: ExtraOptions): boolean { + return opts.initialNavigation === 'legacy_disabled' || opts.initialNavigation === false; + } +} + +export function getAppInitializer(r: RouterInitializer) { + return r.appInitializer.bind(r); +} + +export function getBootstrapListener(r: RouterInitializer) { + return r.bootstrapListener.bind(r); } /** @@ -306,11 +412,14 @@ export const ROUTER_INITIALIZER = export function provideRouterInitializer() { return [ + RouterInitializer, { - provide: ROUTER_INITIALIZER, - useFactory: initialRouterNavigation, - deps: [Router, ApplicationRef, RouterPreloader, ROUTER_CONFIGURATION] + provide: APP_INITIALIZER, + multi: true, + useFactory: getAppInitializer, + deps: [RouterInitializer] }, + {provide: ROUTER_INITIALIZER, useFactory: getBootstrapListener, deps: [RouterInitializer]}, {provide: APP_BOOTSTRAP_LISTENER, multi: true, useExisting: ROUTER_INITIALIZER}, ]; } diff --git a/modules/@angular/router/test/bootstrap.spec.ts b/modules/@angular/router/test/bootstrap.spec.ts new file mode 100644 index 0000000000..1c6dabfde9 --- /dev/null +++ b/modules/@angular/router/test/bootstrap.spec.ts @@ -0,0 +1,244 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {APP_BASE_HREF} from '@angular/common'; +import {ApplicationRef, CUSTOM_ELEMENTS_SCHEMA, Component, NgModule, destroyPlatform} from '@angular/core'; +import {inject} from '@angular/core/testing'; +import {BrowserModule, DOCUMENT, ɵgetDOM as getDOM} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {NavigationEnd, Resolve, Router, RouterModule} from '@angular/router'; + + +describe('bootstrap', () => { + let log: any[] = []; + let testProviders: any[] = null; + + @Component({selector: 'test-app', template: 'root '}) + class RootCmp { + constructor() { log.push('RootCmp'); } + } + + @Component({selector: 'test-app2', template: 'root '}) + class SecondRootCmp { + } + + class TestResolver implements Resolve { + resolve() { + let resolve: any = null; + const res = new Promise(r => resolve = r); + setTimeout(() => resolve('test-data'), 0); + return res; + } + } + + beforeEach(inject([DOCUMENT], (doc: any) => { + destroyPlatform(); + + const el1 = getDOM().createElement('test-app', doc); + const el2 = getDOM().createElement('test-app2', doc); + getDOM().appendChild(doc.body, el1); + getDOM().appendChild(doc.body, el2); + + log = []; + testProviders = [{provide: APP_BASE_HREF, useValue: ''}]; + })); + + afterEach(inject([DOCUMENT], (doc: any) => { + const oldRoots = getDOM().querySelectorAll(doc, 'test-app,test-app2'); + for (let i = 0; i < oldRoots.length; i++) { + getDOM().remove(oldRoots[i]); + } + })); + + it('should wait for resolvers to complete when initialNavigation = enabled', (done) => { + @Component({selector: 'test', template: 'test'}) + class TestCmpEnabled { + } + + @NgModule({ + imports: [ + BrowserModule, RouterModule.forRoot( + [{path: '**', component: TestCmpEnabled, resolve: {test: TestResolver}}], + {useHash: true, initialNavigation: 'enabled'}) + ], + declarations: [RootCmp, TestCmpEnabled], + bootstrap: [RootCmp], + providers: [...testProviders, TestResolver], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }) + class TestModule { + constructor(router: Router) { + log.push('TestModule'); + router.events.subscribe(e => log.push(e.constructor.name)); + } + } + + platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { + const router = res.injector.get(Router); + const data = router.routerState.snapshot.root.firstChild.data; + expect(data['test']).toEqual('test-data'); + expect(log).toEqual( + ['TestModule', 'NavigationStart', 'RoutesRecognized', 'RootCmp', 'NavigationEnd']); + done(); + }); + }); + + it('should NOT wait for resolvers to complete when initialNavigation = legacy_enabled', + (done) => { + @Component({selector: 'test', template: 'test'}) + class TestCmpLegacyEnabled { + } + + @NgModule({ + imports: [ + BrowserModule, + RouterModule.forRoot( + [{path: '**', component: TestCmpLegacyEnabled, resolve: {test: TestResolver}}], + {useHash: true, initialNavigation: 'legacy_enabled'}) + ], + declarations: [RootCmp, TestCmpLegacyEnabled], + bootstrap: [RootCmp], + providers: [...testProviders, TestResolver], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }) + class TestModule { + constructor(router: Router) { + log.push('TestModule'); + router.events.subscribe(e => log.push(e.constructor.name)); + } + } + + platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { + const router = res.injector.get(Router); + expect(router.routerState.snapshot.root.firstChild).toBeNull(); + // NavigationEnd has not been emitted yet because bootstrap returned too early + expect(log).toEqual(['TestModule', 'RootCmp', 'NavigationStart', 'RoutesRecognized']); + + router.events.subscribe((e) => { + if (e instanceof NavigationEnd) { + done(); + } + }); + }); + }); + + it('should not run navigation when initialNavigation = disabled', (done) => { + @Component({selector: 'test', template: 'test'}) + class TestCmpDiabled { + } + + @NgModule({ + imports: [ + BrowserModule, RouterModule.forRoot( + [{path: '**', component: TestCmpDiabled, resolve: {test: TestResolver}}], + {useHash: true, initialNavigation: 'disabled'}) + ], + declarations: [RootCmp, TestCmpDiabled], + bootstrap: [RootCmp], + providers: [...testProviders, TestResolver], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }) + class TestModule { + constructor(router: Router) { + log.push('TestModule'); + router.events.subscribe(e => log.push(e.constructor.name)); + } + } + + platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { + const router = res.injector.get(Router); + expect(log).toEqual(['TestModule', 'RootCmp']); + done(); + }); + }); + + it('should not run navigation when initialNavigation = legacy_disabled', (done) => { + @Component({selector: 'test', template: 'test'}) + class TestCmpLegacyDisabled { + } + + @NgModule({ + imports: [ + BrowserModule, + RouterModule.forRoot( + [{path: '**', component: TestCmpLegacyDisabled, resolve: {test: TestResolver}}], + {useHash: true, initialNavigation: 'legacy_disabled'}) + ], + declarations: [RootCmp, TestCmpLegacyDisabled], + bootstrap: [RootCmp], + providers: [...testProviders, TestResolver], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }) + class TestModule { + constructor(router: Router) { + log.push('TestModule'); + router.events.subscribe(e => log.push(e.constructor.name)); + } + } + + platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { + const router = res.injector.get(Router); + expect(log).toEqual(['TestModule', 'RootCmp']); + done(); + }); + }); + + it('should not init router navigation listeners if a non root component is bootstrapped', + (done) => { + @NgModule({ + imports: [BrowserModule, RouterModule.forRoot([], {useHash: true})], + declarations: [SecondRootCmp, RootCmp], + entryComponents: [SecondRootCmp], + bootstrap: [RootCmp], + providers: testProviders, + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }) + class TestModule { + } + + platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { + const router = res.injector.get(Router); + spyOn(router, 'resetRootComponentType').and.callThrough(); + + const appRef: ApplicationRef = res.injector.get(ApplicationRef); + appRef.bootstrap(SecondRootCmp); + + expect(router.resetRootComponentType).not.toHaveBeenCalled(); + + done(); + }); + }); + + it('should reinit router navigation listeners if a previously bootstrapped root component is destroyed', + (done) => { + @NgModule({ + imports: [BrowserModule, RouterModule.forRoot([], {useHash: true})], + declarations: [SecondRootCmp, RootCmp], + entryComponents: [SecondRootCmp], + bootstrap: [RootCmp], + providers: testProviders, + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }) + class TestModule { + } + + platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { + const router = res.injector.get(Router); + spyOn(router, 'resetRootComponentType').and.callThrough(); + + const appRef: ApplicationRef = res.injector.get(ApplicationRef); + appRef.components[0].onDestroy(() => { + appRef.bootstrap(SecondRootCmp); + expect(router.resetRootComponentType).toHaveBeenCalled(); + done(); + }); + + appRef.components[0].destroy(); + }); + }); +}); diff --git a/modules/@angular/router/test/router_module.spec.ts b/modules/@angular/router/test/router_module.spec.ts deleted file mode 100644 index 379a3a35d7..0000000000 --- a/modules/@angular/router/test/router_module.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import {APP_BASE_HREF} from '@angular/common'; -import {ApplicationRef, Component, NgModule} from '@angular/core'; -import {TestBed, inject} from '@angular/core/testing'; -import {DOCUMENT} from '@angular/platform-browser'; -import {Router, RouterModule, Routes} from '@angular/router'; - - -@Component({selector: 'app-root', template: ''}) -export class AppRootComponent { -} - -@Component({selector: 'bootstrappable-component', template: ''}) -export class BootstrappableComponent { -} - -export const appRoutes: Routes = [{path: '**', redirectTo: ''}]; - - -@NgModule({ - imports: [RouterModule.forRoot(appRoutes)], - declarations: [AppRootComponent, BootstrappableComponent], - entryComponents: [AppRootComponent, BootstrappableComponent], - providers: [{provide: APP_BASE_HREF, useValue: '/'}] -}) -export class RouterInitTestModule { -} - - -describe('RouterModule', () => { - describe('RouterInitializer', () => { - - beforeEach(() => { TestBed.configureTestingModule({imports: [RouterInitTestModule]}); }); - - beforeEach(inject([DOCUMENT], function(doc: HTMLDocument) { - - const elRootApp = doc.createElement('app-root'); - doc.body.appendChild(elRootApp); - - const elBootComp = doc.createElement('bootstrappable-component'); - doc.body.appendChild(elBootComp); - - })); - it('should not init router navigation listeners if a non root component is bootstrapped', - () => { - - const appRef: ApplicationRef = TestBed.get(ApplicationRef); - const r: Router = TestBed.get(Router); - - const spy = spyOn(r, 'resetRootComponentType').and.callThrough(); - - appRef.bootstrap(AppRootComponent); - expect(r.resetRootComponentType).toHaveBeenCalled(); - - spy.calls.reset(); - - appRef.bootstrap(BootstrappableComponent); - expect(r.resetRootComponentType).not.toHaveBeenCalled(); - }); - it('should reinit router navigation listeners if a previously bootstrapped root component is destroyed', - (done) => { - - const appRef: ApplicationRef = TestBed.get(ApplicationRef); - const r: Router = TestBed.get(Router); - - const spy = spyOn(r, 'resetRootComponentType').and.callThrough(); - - const compRef = appRef.bootstrap(AppRootComponent); - expect(r.resetRootComponentType).toHaveBeenCalled(); - - spy.calls.reset(); - - compRef.onDestroy(() => { - - appRef.bootstrap(BootstrappableComponent); - expect(r.resetRootComponentType).toHaveBeenCalled(); - - done(); - }); - - compRef.destroy(); - }); - }); - -}); diff --git a/modules/@angular/router/upgrade.ts b/modules/@angular/router/upgrade.ts index 17f5bdd1e9..723d8fa28c 100644 --- a/modules/@angular/router/upgrade.ts +++ b/modules/@angular/router/upgrade.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {APP_BOOTSTRAP_LISTENER, ApplicationRef, ComponentRef, InjectionToken} from '@angular/core'; -import {ExtraOptions, ROUTER_CONFIGURATION, ROUTER_INITIALIZER, Router, RouterPreloader} from '@angular/router'; +import {APP_BOOTSTRAP_LISTENER, ComponentRef, InjectionToken} from '@angular/core'; +import {Router} from '@angular/router'; import {UpgradeModule} from '@angular/upgrade/static'; @@ -36,38 +36,17 @@ import {UpgradeModule} from '@angular/upgrade/static'; * @experimental */ export const RouterUpgradeInitializer = { - provide: ROUTER_INITIALIZER, - useFactory: initialRouterNavigation, - deps: [UpgradeModule, ApplicationRef, RouterPreloader, ROUTER_CONFIGURATION] + provide: APP_BOOTSTRAP_LISTENER, + multi: true, + useFactory: locationSyncBootstrapListener, + deps: [UpgradeModule] }; /** * @internal */ -export function initialRouterNavigation( - ngUpgrade: UpgradeModule, ref: ApplicationRef, preloader: RouterPreloader, - opts: ExtraOptions): Function { - return () => { - if (!ngUpgrade.$injector) { - throw new Error(` - RouterUpgradeInitializer can be used only after UpgradeModule.bootstrap has been called. - Remove RouterUpgradeInitializer and call setUpLocationSync after UpgradeModule.bootstrap. - `); - } - - const router = ngUpgrade.injector.get(Router); - const ref = ngUpgrade.injector.get(ApplicationRef); - - (router as any).resetRootComponentType(ref.componentTypes[0]); - preloader.setUpPreloading(); - if (opts.initialNavigation === false) { - router.setUpLocationChangeListener(); - } else { - router.initialNavigation(); - } - - setUpLocationSync(ngUpgrade); - }; +export function locationSyncBootstrapListener(ngUpgrade: UpgradeModule) { + return () => { setUpLocationSync(ngUpgrade); }; } /** @@ -78,7 +57,14 @@ export function initialRouterNavigation( * * @experimental */ -export function setUpLocationSync(ngUpgrade: UpgradeModule): void { +export function setUpLocationSync(ngUpgrade: UpgradeModule) { + if (!ngUpgrade.$injector) { + throw new Error(` + RouterUpgradeInitializer can be used only after UpgradeModule.bootstrap has been called. + Remove RouterUpgradeInitializer and call setUpLocationSync after UpgradeModule.bootstrap. + `); + } + const router: Router = ngUpgrade.injector.get(Router); const url = document.createElement('a'); diff --git a/modules/playground/src/web_workers/router/index_common.ts b/modules/playground/src/web_workers/router/index_common.ts index 0311bf5f17..7c927c1d20 100644 --- a/modules/playground/src/web_workers/router/index_common.ts +++ b/modules/playground/src/web_workers/router/index_common.ts @@ -25,7 +25,9 @@ export const ROUTES = [ @NgModule({ imports: [WorkerAppModule, RouterModule.forRoot(ROUTES, {useHash: true})], - providers: [WORKER_APP_LOCATION_PROVIDERS], + providers: [ + WORKER_APP_LOCATION_PROVIDERS, + ], bootstrap: [App], declarations: [App, Start, Contact, About] }) diff --git a/tools/public_api_guard/common/typings/common.d.ts b/tools/public_api_guard/common/typings/common.d.ts index bd91a25b67..8cfc4a2126 100644 --- a/tools/public_api_guard/common/typings/common.d.ts +++ b/tools/public_api_guard/common/typings/common.d.ts @@ -94,6 +94,9 @@ export declare class Location { static stripTrailingSlash(url: string): string; } +/** @experimental */ +export declare const LOCATION_INITIALIZED: InjectionToken>; + /** @experimental */ export interface LocationChangeEvent { type: string; diff --git a/tools/public_api_guard/platform-webworker/typings/platform-webworker.d.ts b/tools/public_api_guard/platform-webworker/typings/platform-webworker.d.ts index f536decef1..24ce49968a 100644 --- a/tools/public_api_guard/platform-webworker/typings/platform-webworker.d.ts +++ b/tools/public_api_guard/platform-webworker/typings/platform-webworker.d.ts @@ -93,6 +93,10 @@ export declare const WORKER_APP_LOCATION_PROVIDERS: ({ useFactory: (platformLocation: WebWorkerPlatformLocation, zone: NgZone) => () => Promise; multi: boolean; deps: (typeof NgZone | typeof PlatformLocation)[]; +} | { + provide: InjectionToken>; + useFactory: (platformLocation: WebWorkerPlatformLocation) => Promise; + deps: typeof PlatformLocation[]; })[]; /** @experimental */ diff --git a/tools/public_api_guard/router/typings/router.d.ts b/tools/public_api_guard/router/typings/router.d.ts index 87b6a1a921..cdefc4795f 100644 --- a/tools/public_api_guard/router/typings/router.d.ts +++ b/tools/public_api_guard/router/typings/router.d.ts @@ -76,7 +76,7 @@ export declare type Event = NavigationStart | NavigationEnd | NavigationCancel | export interface ExtraOptions { enableTracing?: boolean; errorHandler?: ErrorHandler; - initialNavigation?: boolean; + initialNavigation?: InitialNavigation; preloadingStrategy?: any; useHash?: boolean; }