Skip to content

Commit

Permalink
feat(router): add navigationSource and restoredState to NavigationSta…
Browse files Browse the repository at this point in the history
…rt event

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).
  • Loading branch information
vsavkin committed Jan 23, 2018
1 parent 95fbb7d commit c77ffd9
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 38 deletions.
10 changes: 6 additions & 4 deletions packages/common/src/location/location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {LocationStrategy} from './location_strategy';
/** @experimental */
export interface PopStateEvent {
pop?: boolean;
state?: any;
type?: string;
url?: string;
}
Expand Down Expand Up @@ -56,6 +57,7 @@ export class Location {
this._subject.emit({
'url': this.path(true),
'pop': true,
'state': ev.state,
'type': ev.type,
});
});
Expand Down Expand Up @@ -103,16 +105,16 @@ export class Location {
* Changes the browsers URL to the normalized version of the given URL, and pushes a
* new item onto the platform's history.
*/
go(path: string, query: string = ''): void {
this._platformStrategy.pushState(null, '', path, query);
go(path: string, query: string = '', state: any = null): void {
this._platformStrategy.pushState(state, '', path, query);
}

/**
* Changes the browsers URL to the normalized version of the given URL, and replaces
* the top item on the platform's history stack.
*/
replaceState(path: string, query: string = ''): void {
this._platformStrategy.replaceState(null, '', path, query);
replaceState(path: string, query: string = '', state: any = null): void {
this._platformStrategy.replaceState(state, '', path, query);
}

/**
Expand Down
5 changes: 4 additions & 1 deletion packages/common/src/location/platform_location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ export const LOCATION_INITIALIZED = new InjectionToken<Promise<any>>('Location I
*
* @experimental
*/
export interface LocationChangeEvent { type: string; }
export interface LocationChangeEvent {
type: string;
state: any;
}

/**
* @experimental
Expand Down
22 changes: 10 additions & 12 deletions packages/common/testing/src/location_mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {ISubscription} from 'rxjs/Subscription';
@Injectable()
export class SpyLocation implements Location {
urlChanges: string[] = [];
private _history: LocationState[] = [new LocationState('', '')];
private _history: LocationState[] = [new LocationState('', '', null)];
private _historyIndex: number = 0;
/** @internal */
_subject: EventEmitter<any> = new EventEmitter();
Expand All @@ -34,6 +34,8 @@ export class SpyLocation implements Location {

path(): string { return this._history[this._historyIndex].path; }

private state(): string { return this._history[this._historyIndex].state; }

isCurrentPathEqualTo(path: string, query: string = ''): boolean {
const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;
const currPath =
Expand All @@ -60,13 +62,13 @@ export class SpyLocation implements Location {
return this._baseHref + url;
}

go(path: string, query: string = '') {
go(path: string, query: string = '', state: any = null) {
path = this.prepareExternalUrl(path);

if (this._historyIndex > 0) {
this._history.splice(this._historyIndex + 1);
}
this._history.push(new LocationState(path, query));
this._history.push(new LocationState(path, query, state));
this._historyIndex = this._history.length - 1;

const locationState = this._history[this._historyIndex - 1];
Expand All @@ -79,7 +81,7 @@ export class SpyLocation implements Location {
this._subject.emit({'url': url, 'pop': false});
}

replaceState(path: string, query: string = '') {
replaceState(path: string, query: string = '', state: any = null) {
path = this.prepareExternalUrl(path);

const history = this._history[this._historyIndex];
Expand All @@ -89,6 +91,7 @@ export class SpyLocation implements Location {

history.path = path;
history.query = query;
history.state = state;

const url = path + (query.length > 0 ? ('?' + query) : '');
this.urlChanges.push('replace: ' + url);
Expand All @@ -97,14 +100,14 @@ export class SpyLocation implements Location {
forward() {
if (this._historyIndex < (this._history.length - 1)) {
this._historyIndex++;
this._subject.emit({'url': this.path(), 'pop': true});
this._subject.emit({'url': this.path(), 'state': this.state(), 'pop': true});
}
}

back() {
if (this._historyIndex > 0) {
this._historyIndex--;
this._subject.emit({'url': this.path(), 'pop': true});
this._subject.emit({'url': this.path(), 'state': this.state(), 'pop': true});
}
}

Expand All @@ -118,10 +121,5 @@ export class SpyLocation implements Location {
}

class LocationState {
path: string;
query: string;
constructor(path: string, query: string) {
this.path = path;
this.query = query;
}
constructor(public path: string, public query: string, public state: any) {}
}
5 changes: 3 additions & 2 deletions packages/platform-server/src/location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ export class ServerPlatformLocation implements PlatformLocation {
}
(this as{hash: string}).hash = value;
const newUrl = this.url;
scheduleMicroTask(
() => this._hashUpdate.next({ type: 'hashchange', oldUrl, newUrl } as LocationChangeEvent));
scheduleMicroTask(() => this._hashUpdate.next({
type: 'hashchange', state: null, oldUrl, newUrl
} as LocationChangeEvent));
}

replaceState(state: any, title: string, newUrl: string): void {
Expand Down
25 changes: 25 additions & 0 deletions packages/router/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
import {Route} from './config';
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';

/**
* @whatItDoes what triggered the navigation
* @experimental
*/
export type NavigationSource = 'imperative' | 'popstate' | 'hashchange';

/**
* @whatItDoes Base for events the Router goes through, as opposed to events tied to a specific
Expand Down Expand Up @@ -42,6 +47,26 @@ export class RouterEvent {
* @stable
*/
export class NavigationStart extends RouterEvent {
/** @docsNotRequired */
navigationSource?: NavigationSource;

/** @docsNotRequired */
restoredState?: {navigationId: number}|null;

constructor(
/** @docsNotRequired */
id: number,
/** @docsNotRequired */
url: string,
/** @docsNotRequired */
navigationSource: NavigationSource = 'imperative',
/** @docsNotRequired */
restoredState: {navigationId: number}|null = null) {
super(id, url);
this.navigationSource = navigationSource;
this.restoredState = restoredState;
}

/** @docsNotRequired */
toString(): string { return `NavigationStart(id: ${this.id}, url: '${this.url}')`; }
}
Expand Down
40 changes: 25 additions & 15 deletions packages/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, NavigationSource, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
import {PreActivation} from './pre_activation';
import {recognize} from './recognize';
import {DefaultRouteReuseStrategy, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
Expand Down Expand Up @@ -164,8 +164,6 @@ function defaultErrorHandler(error: any): any {
throw error;
}

type NavigationSource = 'imperative' | 'popstate' | 'hashchange';

type NavigationParams = {
id: number,
rawUrl: UrlTree,
Expand All @@ -174,6 +172,7 @@ type NavigationParams = {
reject: any,
promise: Promise<boolean>,
source: NavigationSource,
state: {navigationId: number} | null
};

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -312,7 +312,11 @@ export class Router {
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 state = change.state && change.state.navigationId ?
{navigationId: change.state.navigationId} :
null;
setTimeout(
() => { this.scheduleNavigation(rawUrlTree, source, state, {replaceUrl: true}); }, 0);
}));
}
}
Expand Down Expand Up @@ -341,6 +345,7 @@ export class Router {
validateConfig(config);
this.config = config;
this.navigated = false;
this.lastSuccessfulId = -1;
}

/** @docsNotRequired */
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -522,8 +527,9 @@ export class Router {
.subscribe(() => {});
}

private scheduleNavigation(rawUrl: UrlTree, source: NavigationSource, extras: NavigationExtras):
Promise<boolean> {
private scheduleNavigation(
rawUrl: UrlTree, source: NavigationSource, 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),
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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});
}
}

Expand All @@ -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)));
Expand Down Expand Up @@ -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});
}
}

Expand Down
Loading

0 comments on commit c77ffd9

Please sign in to comment.