feat(router): add navigationSource and restoredState to NavigationStart event (#21728)

Currently, NavigationStart there is no way to know if an navigation was triggered imperatively or via the location change. These two use cases should be handled differently for a variety of use cases (e.g., scroll position restoration). This PR adds a navigation source field and restored navigation id (passed to navigations triggered by a URL change).

PR Close #21728
This commit is contained in:
vsavkin
2018-01-24 12:19:59 -05:00
committed by Jason Aden
parent 5bd93b1f0f
commit c40ae7f7cf
10 changed files with 183 additions and 40 deletions

View File

@ -9,6 +9,16 @@
import {Route} from './config';
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
/**
* @whatItDoes Identifies the trigger of the navigation.
*
* * 'imperative'--triggered by `router.navigateByUrl` or `router.navigate`.
* * 'popstate'--triggered by a popstate event
* * 'hashchange'--triggered by a hashchange event
*
* @experimental
*/
export type NavigationTrigger = 'imperative' | 'popstate' | 'hashchange';
/**
* @whatItDoes Base for events the Router goes through, as opposed to events tied to a specific
@ -42,6 +52,43 @@ export class RouterEvent {
* @stable
*/
export class NavigationStart extends RouterEvent {
/**
* Identifies the trigger of the navigation.
*
* * 'imperative'--triggered by `router.navigateByUrl` or `router.navigate`.
* * 'popstate'--triggered by a popstate event
* * 'hashchange'--triggered by a hashchange event
*/
navigationTrigger?: 'imperative'|'popstate'|'hashchange';
/**
* This contains the navigation id that pushed the history record that the router navigates
* back to. This is not null only when the navigation is triggered by a popstate event.
*
* The router assigns a navigationId to every router transition/navigation. Even when the user
* clicks on the back button in the browser, a new navigation id will be created. So from
* the perspective of the router, the router never "goes back". By using the `restoredState`
* and its navigationId, you can implement behavior that differentiates between creating new
* states
* and popstate events. In the latter case you can restore some remembered state (e.g., scroll
* position).
*/
restoredState?: {navigationId: number}|null;
constructor(
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
navigationTrigger: 'imperative'|'popstate'|'hashchange' = 'imperative',
/** @docsNotRequired */
restoredState: {navigationId: number}|null = null) {
super(id, url);
this.navigationTrigger = navigationTrigger;
this.restoredState = restoredState;
}
/** @docsNotRequired */
toString(): string { return `NavigationStart(id: ${this.id}, url: '${this.url}')`; }
}

View File

@ -21,7 +21,7 @@ import {applyRedirects} from './apply_redirects';
import {LoadedRouterConfig, QueryParamsHandling, Route, Routes, validateConfig} from './config';
import {createRouterState} from './create_router_state';
import {createUrlTree} from './create_url_tree';
import {ActivationEnd, ChildActivationEnd, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
import {ActivationEnd, ChildActivationEnd, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
import {PreActivation} from './pre_activation';
import {recognize} from './recognize';
import {DefaultRouteReuseStrategy, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
@ -164,8 +164,6 @@ function defaultErrorHandler(error: any): any {
throw error;
}
type NavigationSource = 'imperative' | 'popstate' | 'hashchange';
type NavigationParams = {
id: number,
rawUrl: UrlTree,
@ -173,7 +171,8 @@ type NavigationParams = {
resolve: any,
reject: any,
promise: Promise<boolean>,
source: NavigationSource,
source: NavigationTrigger,
state: {navigationId: number} | null
};
/**
@ -223,6 +222,7 @@ export class Router {
* Indicates if at least one navigation happened.
*/
navigated: boolean = false;
private lastSuccessfulId: number = -1;
/**
* Used by RouterModule. This allows us to
@ -311,8 +311,12 @@ export class Router {
if (!this.locationSubscription) {
this.locationSubscription = <any>this.location.subscribe(Zone.current.wrap((change: any) => {
const rawUrlTree = this.urlSerializer.parse(change['url']);
const source: NavigationSource = change['type'] === 'popstate' ? 'popstate' : 'hashchange';
setTimeout(() => { this.scheduleNavigation(rawUrlTree, source, {replaceUrl: true}); }, 0);
const source: NavigationTrigger = change['type'] === 'popstate' ? 'popstate' : 'hashchange';
const state = change.state && change.state.navigationId ?
{navigationId: change.state.navigationId} :
null;
setTimeout(
() => { this.scheduleNavigation(rawUrlTree, source, state, {replaceUrl: true}); }, 0);
}));
}
}
@ -341,6 +345,7 @@ export class Router {
validateConfig(config);
this.config = config;
this.navigated = false;
this.lastSuccessfulId = -1;
}
/** @docsNotRequired */
@ -449,7 +454,7 @@ export class Router {
const urlTree = url instanceof UrlTree ? url : this.parseUrl(url);
const mergedTree = this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree);
return this.scheduleNavigation(mergedTree, 'imperative', extras);
return this.scheduleNavigation(mergedTree, 'imperative', null, extras);
}
/**
@ -522,8 +527,9 @@ export class Router {
.subscribe(() => {});
}
private scheduleNavigation(rawUrl: UrlTree, source: NavigationSource, extras: NavigationExtras):
Promise<boolean> {
private scheduleNavigation(
rawUrl: UrlTree, source: NavigationTrigger, state: {navigationId: number}|null,
extras: NavigationExtras): Promise<boolean> {
const lastNavigation = this.navigations.value;
// If the user triggers a navigation imperatively (e.g., by using navigateByUrl),
@ -558,21 +564,22 @@ export class Router {
});
const id = ++this.navigationId;
this.navigations.next({id, source, rawUrl, extras, resolve, reject, promise});
this.navigations.next({id, source, state, rawUrl, extras, resolve, reject, promise});
// Make sure that the error is propagated even though `processNavigations` catch
// handler does not rethrow
return promise.catch((e: any) => Promise.reject(e));
}
private executeScheduledNavigation({id, rawUrl, extras, resolve, reject}: NavigationParams):
void {
private executeScheduledNavigation({id, rawUrl, extras, resolve, reject, source,
state}: NavigationParams): void {
const url = this.urlHandlingStrategy.extract(rawUrl);
const urlTransition = !this.navigated || url.toString() !== this.currentUrlTree.toString();
if ((this.onSameUrlNavigation === 'reload' ? true : urlTransition) &&
this.urlHandlingStrategy.shouldProcessUrl(rawUrl)) {
(this.events as Subject<Event>).next(new NavigationStart(id, this.serializeUrl(url)));
(this.events as Subject<Event>)
.next(new NavigationStart(id, this.serializeUrl(url), source, state));
Promise.resolve()
.then(
(_) => this.runNavigate(
@ -584,7 +591,8 @@ export class Router {
} else if (
urlTransition && this.rawUrlTree &&
this.urlHandlingStrategy.shouldProcessUrl(this.rawUrlTree)) {
(this.events as Subject<Event>).next(new NavigationStart(id, this.serializeUrl(url)));
(this.events as Subject<Event>)
.next(new NavigationStart(id, this.serializeUrl(url), source, state));
Promise.resolve()
.then(
(_) => this.runNavigate(
@ -727,9 +735,9 @@ export class Router {
if (!skipLocationChange) {
const path = this.urlSerializer.serialize(this.rawUrlTree);
if (this.location.isCurrentPathEqualTo(path) || replaceUrl) {
this.location.replaceState(path);
this.location.replaceState(path, '', {navigationId: id});
} else {
this.location.go(path);
this.location.go(path, '', {navigationId: id});
}
}
@ -743,6 +751,7 @@ export class Router {
() => {
if (navigationIsSuccessful) {
this.navigated = true;
this.lastSuccessfulId = id;
(this.events as Subject<Event>)
.next(new NavigationEnd(
id, this.serializeUrl(url), this.serializeUrl(this.currentUrlTree)));
@ -784,7 +793,8 @@ export class Router {
}
private resetUrlToCurrentUrlTree(): void {
this.location.replaceState(this.urlSerializer.serialize(this.rawUrlTree));
this.location.replaceState(
this.urlSerializer.serialize(this.rawUrlTree), '', {navigationId: this.lastSuccessfulId});
}
}