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
This commit is contained in:
parent
1cff1250ba
commit
5df998d086
|
@ -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<Promise<any>>('Location Initialized');
|
||||
|
||||
/**
|
||||
* A serializable version of the event from onPopState or onHashChange
|
||||
*
|
||||
|
|
|
@ -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<boolean> {
|
||||
return () => zone.runGuarded(() => platformLocation.init());
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
|
|||
private _hashChangeListeners: Array<Function> = [];
|
||||
private _location: LocationType = null;
|
||||
private _channelSource: EventEmitter<Object>;
|
||||
public initialized: Promise<any>;
|
||||
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); });
|
||||
|
|
|
@ -180,6 +180,18 @@ type NavigationParams = {
|
|||
source: NavigationSource,
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type RouterHook = (snapshot: RouterStateSnapshot) => Observable<void>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
function defaultRouterHook(snapshot: RouterStateSnapshot): Observable<void> {
|
||||
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);
|
||||
|
|
|
@ -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 (<any>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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<any>) => {
|
||||
/**
|
||||
* 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<void>();
|
||||
|
||||
constructor(private injector: Injector) {}
|
||||
|
||||
appInitializer(): Promise<any> {
|
||||
const p: Promise<any> = 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<any>): 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},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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 <router-outlet></router-outlet>'})
|
||||
class RootCmp {
|
||||
constructor() { log.push('RootCmp'); }
|
||||
}
|
||||
|
||||
@Component({selector: 'test-app2', template: 'root <router-outlet></router-outlet>'})
|
||||
class SecondRootCmp {
|
||||
}
|
||||
|
||||
class TestResolver implements Resolve<any> {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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]
|
||||
})
|
||||
|
|
|
@ -94,6 +94,9 @@ export declare class Location {
|
|||
static stripTrailingSlash(url: string): string;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export declare const LOCATION_INITIALIZED: InjectionToken<Promise<any>>;
|
||||
|
||||
/** @experimental */
|
||||
export interface LocationChangeEvent {
|
||||
type: string;
|
||||
|
|
|
@ -93,6 +93,10 @@ export declare const WORKER_APP_LOCATION_PROVIDERS: ({
|
|||
useFactory: (platformLocation: WebWorkerPlatformLocation, zone: NgZone) => () => Promise<boolean>;
|
||||
multi: boolean;
|
||||
deps: (typeof NgZone | typeof PlatformLocation)[];
|
||||
} | {
|
||||
provide: InjectionToken<Promise<any>>;
|
||||
useFactory: (platformLocation: WebWorkerPlatformLocation) => Promise<any>;
|
||||
deps: typeof PlatformLocation[];
|
||||
})[];
|
||||
|
||||
/** @experimental */
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue