feat(router): implement cancelation

This commit is contained in:
vsavkin
2016-06-03 14:07:01 -07:00
parent 5d386dc426
commit 2717bcc3af
2 changed files with 87 additions and 28 deletions

View File

@ -15,9 +15,12 @@ import { createUrlTree } from './create_url_tree';
import { forEach, and, shallowEqual } from './utils/collection'; import { forEach, and, shallowEqual } from './utils/collection';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map'; import 'rxjs/add/operator/map';
import 'rxjs/add/operator/scan'; import 'rxjs/add/operator/scan';
import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/concat';
import 'rxjs/add/operator/concatMap';
import {of} from 'rxjs/observable/of'; import {of} from 'rxjs/observable/of';
import {forkJoin} from 'rxjs/observable/forkJoin'; import {forkJoin} from 'rxjs/observable/forkJoin';
@ -31,6 +34,7 @@ export class Router {
private currentRouterState: RouterState; private currentRouterState: RouterState;
private config: RouterConfig; private config: RouterConfig;
private locationSubscription: Subscription; private locationSubscription: Subscription;
private navigationId: number = 0;
/** /**
* @internal * @internal
@ -65,9 +69,9 @@ export class Router {
* router.navigateByUrl("/team/33/user/11"); * router.navigateByUrl("/team/33/user/11");
* ``` * ```
*/ */
navigateByUrl(url: string): Observable<void> { navigateByUrl(url: string): Promise<boolean> {
const urlTree = this.urlSerializer.parse(url); const urlTree = this.urlSerializer.parse(url);
return this.runNavigate(urlTree, false); return this.scheduleNavigation(urlTree, false);
} }
/** /**
@ -140,8 +144,8 @@ export class Router {
* router.navigate(['team', 33, 'team', '11], {relativeTo: route}); * router.navigate(['team', 33, 'team', '11], {relativeTo: route});
* ``` * ```
*/ */
navigate(commands: any[], extras: NavigationExtras = {}): Observable<void> { navigate(commands: any[], extras: NavigationExtras = {}): Promise<boolean> {
return this.runNavigate(this.createUrlTree(commands, extras)); return this.scheduleNavigation(this.createUrlTree(commands, extras), false);
} }
/** /**
@ -154,39 +158,50 @@ export class Router {
*/ */
parseUrl(url: string): UrlTree { return this.urlSerializer.parse(url); } parseUrl(url: string): UrlTree { return this.urlSerializer.parse(url); }
private scheduleNavigation(url: UrlTree, pop: boolean):Promise<boolean> {
const id = ++ this.navigationId;
return Promise.resolve().then((_) => this.runNavigate(url, false, id));
}
private setUpLocationChangeListener(): void { private setUpLocationChangeListener(): void {
this.locationSubscription = <any>this.location.subscribe((change) => { this.locationSubscription = <any>this.location.subscribe((change) => {
this.runNavigate(this.urlSerializer.parse(change['url']), change['pop']) return this.scheduleNavigation(this.urlSerializer.parse(change['url']), change['pop']);
}); });
} }
private runNavigate(url:UrlTree, pop?:boolean):Observable<any> { private runNavigate(url: UrlTree, pop: boolean, id: number):Promise<boolean> {
let state; if (id !== this.navigationId) {
const r = recognize(this.rootComponentType, this.config, url).mergeMap((newRouterStateSnapshot) => { return Promise.resolve(false);
return resolve(this.resolver, newRouterStateSnapshot); }
}).map((routerStateSnapshot) => { return new Promise((resolvePromise, rejectPromise) => {
return createRouterState(routerStateSnapshot, this.currentRouterState); let state;
recognize(this.rootComponentType, this.config, url).mergeMap((newRouterStateSnapshot) => {
return resolve(this.resolver, newRouterStateSnapshot);
}).map((newState:RouterState) => { }).map((routerStateSnapshot) => {
state = newState; return createRouterState(routerStateSnapshot, this.currentRouterState);
}).mergeMap(_ => { }).map((newState:RouterState) => {
return new GuardChecks(state.snapshot, this.currentRouterState.snapshot, this.injector).check(this.outletMap); state = newState;
}).mergeMap(_ => {
return new GuardChecks(state.snapshot, this.currentRouterState.snapshot, this.injector).check(this.outletMap);
}).forEach((shouldActivate) => {
if (!shouldActivate || id !== this.navigationId) {
return;
}
new ActivateRoutes(state, this.currentRouterState).activate(this.outletMap);
this.currentUrlTree = url;
this.currentRouterState = state;
if (!pop) {
this.location.go(this.urlSerializer.serialize(url));
}
}).then(() => resolvePromise(true), e => rejectPromise(e));
}); });
r.subscribe((shouldActivate) => {
if (!shouldActivate) return;
new ActivateRoutes(state, this.currentRouterState).activate(this.outletMap);
this.currentUrlTree = url;
this.currentRouterState = state;
if (!pop) {
this.location.go(this.urlSerializer.serialize(url));
}
});
return r;
} }
} }

View File

@ -222,6 +222,50 @@ describe("Integration", () => {
expect(fixture.debugElement.nativeElement).toHaveText('simple'); expect(fixture.debugElement.nativeElement).toHaveText('simple');
}))); })));
it("should cancel in-flight navigations",
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => {
router.resetConfig([
{ path: '/user/:name', component: UserCmp }
]);
const fixture = tcb.createFakeAsync(RootCmp);
router.navigateByUrl('/user/init');
advance(fixture);
const user = fixture.debugElement.children[1].componentInstance;
let r1, r2;
router.navigateByUrl('/user/victor').then(_ => r1 = _);
router.navigateByUrl('/user/fedor').then(_ => r2 = _);
advance(fixture);
expect(r1).toEqual(false); // returns false because it was canceled
expect(r2).toEqual(true); // returns true because it was successful
expect(fixture.debugElement.nativeElement).toHaveText('user fedor');
expect(user.recordedParams).toEqual([{name: 'init'}, {name: 'fedor'}]);
})));
it("should handle failed navigations gracefully",
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => {
router.resetConfig([
{ path: '/user/:name', component: UserCmp }
]);
const fixture = tcb.createFakeAsync(RootCmp);
advance(fixture);
let e;
router.navigateByUrl('/invalid').catch(_ => e = _);
advance(fixture);
expect(e.message).toContain("Cannot match any routes");
router.navigateByUrl('/user/fedor');
advance(fixture);
expect(fixture.debugElement.nativeElement).toHaveText('user fedor');
})));
describe("router links", () => { describe("router links", () => {
it("should support string router links", it("should support string router links",
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {