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 { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/scan';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/concat';
import 'rxjs/add/operator/concatMap';
import {of} from 'rxjs/observable/of';
import {forkJoin} from 'rxjs/observable/forkJoin';
@ -31,6 +34,7 @@ export class Router {
private currentRouterState: RouterState;
private config: RouterConfig;
private locationSubscription: Subscription;
private navigationId: number = 0;
/**
* @internal
@ -65,9 +69,9 @@ export class Router {
* router.navigateByUrl("/team/33/user/11");
* ```
*/
navigateByUrl(url: string): Observable<void> {
navigateByUrl(url: string): Promise<boolean> {
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});
* ```
*/
navigate(commands: any[], extras: NavigationExtras = {}): Observable<void> {
return this.runNavigate(this.createUrlTree(commands, extras));
navigate(commands: any[], extras: NavigationExtras = {}): Promise<boolean> {
return this.scheduleNavigation(this.createUrlTree(commands, extras), false);
}
/**
@ -154,39 +158,50 @@ export class Router {
*/
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 {
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> {
let state;
const r = recognize(this.rootComponentType, this.config, url).mergeMap((newRouterStateSnapshot) => {
return resolve(this.resolver, newRouterStateSnapshot);
private runNavigate(url: UrlTree, pop: boolean, id: number):Promise<boolean> {
if (id !== this.navigationId) {
return Promise.resolve(false);
}
}).map((routerStateSnapshot) => {
return createRouterState(routerStateSnapshot, this.currentRouterState);
return new Promise((resolvePromise, rejectPromise) => {
let state;
recognize(this.rootComponentType, this.config, url).mergeMap((newRouterStateSnapshot) => {
return resolve(this.resolver, newRouterStateSnapshot);
}).map((newState:RouterState) => {
state = newState;
}).map((routerStateSnapshot) => {
return createRouterState(routerStateSnapshot, this.currentRouterState);
}).mergeMap(_ => {
return new GuardChecks(state.snapshot, this.currentRouterState.snapshot, this.injector).check(this.outletMap);
}).map((newState:RouterState) => {
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');
})));
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", () => {
it("should support string router links",
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {