diff --git a/modules/@angular/router/src/index.ts b/modules/@angular/router/src/index.ts index 85000e9477..f19b56456b 100644 --- a/modules/@angular/router/src/index.ts +++ b/modules/@angular/router/src/index.ts @@ -1,4 +1,4 @@ -export { Router } from './router'; +export { Router, Event, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from './router'; export { UrlSerializer, DefaultUrlSerializer } from './url_serializer'; export { RouterState, ActivatedRoute, RouterStateSnapshot, ActivatedRouteSnapshot } from './router_state'; export { UrlTree, UrlSegment} from './url_tree'; diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index 2a815d592a..7467aada57 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -26,6 +26,12 @@ import {forkJoin} from 'rxjs/observable/forkJoin'; export interface NavigationExtras { relativeTo?: ActivatedRoute; queryParameters?: Params; fragment?: string; } +export class NavigationStart { constructor(public id:number, public url:UrlTree) {} } +export class NavigationEnd { constructor(public id:number, public url:UrlTree) {} } +export class NavigationCancel { constructor(public id:number, public url:UrlTree) {} } +export class NavigationError { constructor(public id:number, public url:UrlTree, public error:any) {} } +export type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError; + /** * The `Router` is responsible for mapping URLs to components. */ @@ -34,12 +40,14 @@ export class Router { private currentRouterState: RouterState; private config: RouterConfig; private locationSubscription: Subscription; + private routerEvents: Subject; private navigationId: number = 0; /** * @internal */ constructor(private rootComponentType:Type, private resolver: ComponentResolver, private urlSerializer: UrlSerializer, private outletMap: RouterOutletMap, private location: Location, private injector: Injector) { + this.routerEvents = new Subject(); this.currentUrlTree = createEmptyUrlTree(); this.currentRouterState = createEmptyState(rootComponentType); this.setUpLocationChangeListener(); @@ -60,6 +68,10 @@ export class Router { return this.currentUrlTree; } + get events(): Observable { + return this.routerEvents; + } + /** * Navigate based on the provided url. This navigation is always absolute. * @@ -160,6 +172,7 @@ export class Router { private scheduleNavigation(url: UrlTree, pop: boolean):Promise { const id = ++ this.navigationId; + this.routerEvents.next(new NavigationStart(id, url)); return Promise.resolve().then((_) => this.runNavigate(url, false, id)); } @@ -171,6 +184,7 @@ export class Router { private runNavigate(url: UrlTree, pop: boolean, id: number):Promise { if (id !== this.navigationId) { + this.routerEvents.next(new NavigationCancel(id, url)); return Promise.resolve(false); } @@ -190,7 +204,8 @@ export class Router { }).forEach((shouldActivate) => { if (!shouldActivate || id !== this.navigationId) { - return; + this.routerEvents.next(new NavigationCancel(id, url)); + return Promise.resolve(false); } new ActivateRoutes(state, this.currentRouterState).activate(this.outletMap); @@ -200,7 +215,14 @@ export class Router { if (!pop) { this.location.go(this.urlSerializer.serialize(url)); } - }).then(() => resolvePromise(true), e => rejectPromise(e)); + }).then(() => { + this.routerEvents.next(new NavigationEnd(id, url)); + resolvePromise(true); + + }, e => { + this.routerEvents.next(new NavigationError(id, url, e)); + rejectPromise(e); + }); }); } } diff --git a/modules/@angular/router/test/router.spec.ts b/modules/@angular/router/test/router.spec.ts index 4a86646889..b0f012f74c 100644 --- a/modules/@angular/router/test/router.spec.ts +++ b/modules/@angular/router/test/router.spec.ts @@ -18,7 +18,7 @@ import {TestComponentBuilder, ComponentFixture} from '@angular/compiler/testing' import { ComponentResolver } from '@angular/core'; import { SpyLocation } from '@angular/common/testing'; import { UrlSerializer, DefaultUrlSerializer, RouterOutletMap, Router, ActivatedRoute, ROUTER_DIRECTIVES, Params, - RouterStateSnapshot, ActivatedRouteSnapshot, CanActivate, CanDeactivate } from '../src/index'; + RouterStateSnapshot, ActivatedRouteSnapshot, CanActivate, CanDeactivate, Event, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '../src/index'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; @@ -228,6 +228,9 @@ describe("Integration", () => { { path: '/user/:name', component: UserCmp } ]); + const recordedEvents = []; + router.events.forEach(e => recordedEvents.push(e)); + const fixture = tcb.createFakeAsync(RootCmp); router.navigateByUrl('/user/init'); advance(fixture); @@ -244,6 +247,17 @@ describe("Integration", () => { expect(fixture.debugElement.nativeElement).toHaveText('user fedor'); expect(user.recordedParams).toEqual([{name: 'init'}, {name: 'fedor'}]); + + expectEvents(router, recordedEvents.slice(1), [ + [NavigationStart, '/user/init'], + [NavigationEnd, '/user/init'], + + [NavigationStart, '/user/victor'], + [NavigationStart, '/user/fedor'], + + [NavigationCancel, '/user/victor'], + [NavigationEnd, '/user/fedor'] + ]); }))); it("should handle failed navigations gracefully", @@ -252,6 +266,9 @@ describe("Integration", () => { { path: '/user/:name', component: UserCmp } ]); + const recordedEvents = []; + router.events.forEach(e => recordedEvents.push(e)); + const fixture = tcb.createFakeAsync(RootCmp); advance(fixture); @@ -264,6 +281,13 @@ describe("Integration", () => { advance(fixture); expect(fixture.debugElement.nativeElement).toHaveText('user fedor'); + expectEvents(router, recordedEvents.slice(1), [ + [NavigationStart, '/invalid'], + [NavigationError, '/invalid'], + + [NavigationStart, '/user/fedor'], + [NavigationEnd, '/user/fedor'] + ]); }))); describe("router links", () => { @@ -482,6 +506,13 @@ describe("Integration", () => { }); }); +function expectEvents(router: Router, events:Event[], pairs: any[]) { + for (let i = 0; i < events.length; ++i) { + expect((events[i].constructor).name).toBe(pairs[i][0].name); + expect(router.serializeUrl((events[i]).url)).toBe(pairs[i][1]); + } +} + @Component({ selector: 'link-cmp', template: `link`,