From 78e88141036fdc41b38506fc1d245b5b79cc17c3 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Wed, 15 Feb 2017 10:57:03 -0800 Subject: [PATCH] feat(router): add `RouteConfigLoadStart` and `RouteConfigLoadEnd` events --- modules/@angular/router/src/events.ts | 34 ++++++++++----- modules/@angular/router/src/index.ts | 2 +- modules/@angular/router/src/router.ts | 15 ++++--- .../router/src/router_config_loader.ts | 14 ++++++- .../@angular/router/src/router_preloader.ts | 8 ++-- .../@angular/router/test/integration.spec.ts | 41 +++++++++++-------- .../router/test/router_preloader.spec.ts | 27 +++++++----- tools/public_api_guard/router/index.d.ts | 11 ++++- 8 files changed, 100 insertions(+), 52 deletions(-) diff --git a/modules/@angular/router/src/events.ts b/modules/@angular/router/src/events.ts index f5071c92ad..c4b223c17e 100644 --- a/modules/@angular/router/src/events.ts +++ b/modules/@angular/router/src/events.ts @@ -15,7 +15,6 @@ import {RouterStateSnapshot} from './router_state'; * @stable */ export class NavigationStart { - // TODO: vsavkin: make internal constructor( /** @docsNotRequired */ public id: number, @@ -32,7 +31,6 @@ export class NavigationStart { * @stable */ export class NavigationEnd { - // TODO: vsavkin: make internal constructor( /** @docsNotRequired */ public id: number, @@ -53,7 +51,6 @@ export class NavigationEnd { * @stable */ export class NavigationCancel { - // TODO: vsavkin: make internal constructor( /** @docsNotRequired */ public id: number, @@ -72,7 +69,6 @@ export class NavigationCancel { * @stable */ export class NavigationError { - // TODO: vsavkin: make internal constructor( /** @docsNotRequired */ public id: number, @@ -93,7 +89,6 @@ export class NavigationError { * @stable */ export class RoutesRecognized { - // TODO: vsavkin: make internal constructor( /** @docsNotRequired */ public id: number, @@ -111,23 +106,40 @@ export class RoutesRecognized { } /** - * @whatItDoes Represents an event triggered when route is lazy loaded. + * @whatItDoes Represents an event triggered before lazy loading a route config. * * @experimental */ -export class RouteConfigLoaded { +export class RouteConfigLoadStart { constructor(public route: Route) {} - toString(): string { return `RouteConfigLoaded(path: ${this.route.path})`; } + toString(): string { return `RouteConfigLoadStart(path: ${this.route.path})`; } +} + +/** + * @whatItDoes Represents an event triggered when a route has been lazy loaded. + * + * @experimental + */ +export class RouteConfigLoadEnd { + constructor(public route: Route) {} + + toString(): string { return `RouteConfigLoadEnd(path: ${this.route.path})`; } } /** * @whatItDoes Represents a router event. * - * Please see {@link NavigationStart}, {@link NavigationEnd}, {@link NavigationCancel}, {@link - * NavigationError}, {@link RoutesRecognized}, {@link RouteConfigLoaded} for more information. + * One of: + * - {@link NavigationStart}, + * - {@link NavigationEnd}, + * - {@link NavigationCancel}, + * - {@link NavigationError}, + * - {@link RoutesRecognized}, + * - {@link RouteConfigLoadStart}, + * - {@link RouteConfigLoadEnd} * * @stable */ export type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | - RoutesRecognized | RouteConfigLoaded; + RoutesRecognized | RouteConfigLoadStart | RouteConfigLoadEnd; diff --git a/modules/@angular/router/src/index.ts b/modules/@angular/router/src/index.ts index 86d8e8a0e1..96d5c790c0 100644 --- a/modules/@angular/router/src/index.ts +++ b/modules/@angular/router/src/index.ts @@ -11,7 +11,7 @@ export {Data, LoadChildren, LoadChildrenCallback, ResolveData, Route, Routes} fr export {RouterLink, RouterLinkWithHref} from './directives/router_link'; export {RouterLinkActive} from './directives/router_link_active'; export {RouterOutlet} from './directives/router_outlet'; -export {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, RouteConfigLoaded, RoutesRecognized} from './events'; +export {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events'; export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces'; export {DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy'; export {ROUTES} from './router_config_loader'; diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index 7e9bff311f..fae1e132be 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -26,7 +26,7 @@ import {QueryParamsHandling, ResolveData, Route, Routes, validateConfig} from '. import {createRouterState} from './create_router_state'; import {createUrlTree} from './create_url_tree'; import {RouterOutlet} from './directives/router_outlet'; -import {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, RouteConfigLoaded, RoutesRecognized} from './events'; +import {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events'; import {recognize} from './recognize'; import {DetachedRouteHandle, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy'; import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader'; @@ -208,8 +208,7 @@ export class Router { private rawUrlTree: UrlTree; private navigations = new BehaviorSubject(null); - /** @internal */ - routerEvents = new Subject(); + private routerEvents = new Subject(); private currentRouterState: RouterState; private locationSubscription: Subscription; @@ -243,11 +242,14 @@ export class Router { private rootComponentType: Type, private urlSerializer: UrlSerializer, private outletMap: RouterOutletMap, private location: Location, private injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler, public config: Routes) { + const onLoadStart = (r: Route) => this.triggerEvent(new RouteConfigLoadStart(r)); + const onLoadEnd = (r: Route) => this.triggerEvent(new RouteConfigLoadEnd(r)); + this.resetConfig(config); this.currentUrlTree = createEmptyUrlTree(); this.rawUrlTree = this.currentUrlTree; - this.configLoader = new RouterConfigLoader( - loader, compiler, (r: Route) => this.routerEvents.next(new RouteConfigLoaded(r))); + + this.configLoader = new RouterConfigLoader(loader, compiler, onLoadStart, onLoadEnd); this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType); this.processNavigations(); } @@ -297,6 +299,9 @@ export class Router { /** An observable of router events */ get events(): Observable { return this.routerEvents; } + /** @internal */ + triggerEvent(e: Event) { this.routerEvents.next(e); } + /** * Resets the configuration used for navigation and generating links. * diff --git a/modules/@angular/router/src/router_config_loader.ts b/modules/@angular/router/src/router_config_loader.ts index 36f2a77c84..bb9bd45313 100644 --- a/modules/@angular/router/src/router_config_loader.ts +++ b/modules/@angular/router/src/router_config_loader.ts @@ -30,14 +30,24 @@ export class LoadedRouterConfig { export class RouterConfigLoader { constructor( private loader: NgModuleFactoryLoader, private compiler: Compiler, - private onLoadListener: (r: Route) => void) {} + private onLoadStartListener?: (r: Route) => void, + private onLoadEndListener?: (r: Route) => void) {} load(parentInjector: Injector, route: Route): Observable { + if (this.onLoadStartListener) { + this.onLoadStartListener(route); + } + const moduleFactory$ = this.loadModuleFactory(route.loadChildren); + return map.call(moduleFactory$, (factory: NgModuleFactory) => { + if (this.onLoadEndListener) { + this.onLoadEndListener(route); + } + const module = factory.create(parentInjector); const injectorFactory = (parent: Injector) => factory.create(parent).injector; - this.onLoadListener(route); + return new LoadedRouterConfig( flatten(module.injector.get(ROUTES)), module.injector, module.componentFactoryResolver, injectorFactory); diff --git a/modules/@angular/router/src/router_preloader.ts b/modules/@angular/router/src/router_preloader.ts index 1457f685d0..15af8a87be 100644 --- a/modules/@angular/router/src/router_preloader.ts +++ b/modules/@angular/router/src/router_preloader.ts @@ -17,7 +17,7 @@ import {filter} from 'rxjs/operator/filter'; import {mergeAll} from 'rxjs/operator/mergeAll'; import {mergeMap} from 'rxjs/operator/mergeMap'; import {Route, Routes} from './config'; -import {NavigationEnd, RouteConfigLoaded} from './events'; +import {NavigationEnd, RouteConfigLoadEnd, RouteConfigLoadStart} from './events'; import {Router} from './router'; import {RouterConfigLoader} from './router_config_loader'; @@ -80,8 +80,10 @@ export class RouterPreloader { constructor( private router: Router, moduleLoader: NgModuleFactoryLoader, compiler: Compiler, private injector: Injector, private preloadingStrategy: PreloadingStrategy) { - this.loader = new RouterConfigLoader( - moduleLoader, compiler, (r: Route) => router.routerEvents.next(new RouteConfigLoaded(r))); + const onStartLoad = (r: Route) => router.triggerEvent(new RouteConfigLoadStart(r)); + const onEndLoad = (r: Route) => router.triggerEvent(new RouteConfigLoadEnd(r)); + + this.loader = new RouterConfigLoader(moduleLoader, compiler, onStartLoad, onEndLoad); }; setUpPreloading(): void { diff --git a/modules/@angular/router/test/integration.spec.ts b/modules/@angular/router/test/integration.spec.ts index 5f3d9682f3..6d28640331 100644 --- a/modules/@angular/router/test/integration.spec.ts +++ b/modules/@angular/router/test/integration.spec.ts @@ -14,7 +14,7 @@ import {expect} from '@angular/platform-browser/testing/matchers'; import {Observable} from 'rxjs/Observable'; import {map} from 'rxjs/operator/map'; -import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DetachedRouteHandle, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, Params, PreloadAllModules, PreloadingStrategy, Resolve, RouteConfigLoaded, RouteReuseStrategy, Router, RouterModule, RouterStateSnapshot, RoutesRecognized, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '../index'; +import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DetachedRouteHandle, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, Params, PreloadAllModules, PreloadingStrategy, Resolve, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterModule, RouterStateSnapshot, RoutesRecognized, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '../index'; import {RouterPreloader} from '../src/router_preloader'; import {forEach} from '../src/utils/collection'; import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing'; @@ -1949,15 +1949,16 @@ describe('Integration', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ - {provide: 'alwaysFalse', useValue: (a: any) => false}, { + {provide: 'alwaysFalse', useValue: (a: any) => false}, + { provide: 'returnFalseAndNavigate', useFactory: (router: any) => (a: any) => { router.navigate(['blank']); return false; }, - deps: [Router] + deps: [Router], }, - {provide: 'alwaysTrue', useValue: (a: any) => true} + {provide: 'alwaysTrue', useValue: (a: any) => true}, ] }); }); @@ -1999,7 +2000,7 @@ describe('Integration', () => { expectEvents(recordedEvents, [ [NavigationStart, '/lazyFalse/loaded'], - [NavigationCancel, '/lazyFalse/loaded'] + [NavigationCancel, '/lazyFalse/loaded'], ]); recordedEvents.splice(0); @@ -2011,8 +2012,11 @@ describe('Integration', () => { expect(location.path()).toEqual('/lazyTrue/loaded'); expectEvents(recordedEvents, [ - [NavigationStart, '/lazyTrue/loaded'], [RouteConfigLoaded, undefined], - [RoutesRecognized, '/lazyTrue/loaded'], [NavigationEnd, '/lazyTrue/loaded'] + [NavigationStart, '/lazyTrue/loaded'], + [RouteConfigLoadStart], + [RouteConfigLoadEnd], + [RoutesRecognized, '/lazyTrue/loaded'], + [NavigationEnd, '/lazyTrue/loaded'], ]); }))); @@ -2299,13 +2303,13 @@ describe('Integration', () => { expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]'); }))); - it('should emit RouteConfigLoaded event when route is lazy loaded', + it('should emit RouteConfigLoadStart and RouteConfigLoadEnd event when route is lazy loaded', fakeAsync(inject( [Router, Location, NgModuleFactoryLoader], (router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => { @Component({ selector: 'lazy', - template: 'lazy-loaded-parent []' + template: 'lazy-loaded-parent []', }) class ParentLazyLoadedComponent { } @@ -2319,16 +2323,16 @@ describe('Integration', () => { imports: [RouterModule.forChild([{ path: 'loaded', component: ParentLazyLoadedComponent, - children: [{path: 'child', component: ChildLazyLoadedComponent}] + children: [{path: 'child', component: ChildLazyLoadedComponent}], }])] }) class LoadedModule { } - const events: RouteConfigLoaded[] = []; + const events: Array = []; router.events.subscribe(e => { - if (e instanceof RouteConfigLoaded) { + if (e instanceof RouteConfigLoadStart || e instanceof RouteConfigLoadEnd) { events.push(e); } }); @@ -2340,8 +2344,9 @@ describe('Integration', () => { router.navigateByUrl('/lazy/loaded/child'); advance(fixture); - expect(events.length).toEqual(1); - expect(events[0].route.path).toEqual('lazy'); + expect(events.length).toEqual(2); + expect(events[0].toString()).toEqual('RouteConfigLoadStart(path: lazy)'); + expect(events[1].toString()).toEqual('RouteConfigLoadEnd(path: lazy)'); }))); it('throws an error when forRoot() is used in a lazy context', @@ -2535,9 +2540,11 @@ describe('Integration', () => { expect(location.path()).toEqual('/'); - expectEvents( - recordedEvents, - [[NavigationStart, '/lazy/loaded'], [NavigationError, '/lazy/loaded']]); + expectEvents(recordedEvents, [ + [NavigationStart, '/lazy/loaded'], + [RouteConfigLoadStart], + [NavigationError, '/lazy/loaded'], + ]); }))); it('should work with complex redirect rules', diff --git a/modules/@angular/router/test/router_preloader.spec.ts b/modules/@angular/router/test/router_preloader.spec.ts index 14392d2e38..fce0b699de 100644 --- a/modules/@angular/router/test/router_preloader.spec.ts +++ b/modules/@angular/router/test/router_preloader.spec.ts @@ -9,7 +9,7 @@ import {Component, NgModule, NgModuleFactoryLoader} from '@angular/core'; import {TestBed, fakeAsync, inject, tick} from '@angular/core/testing'; -import {RouteConfigLoaded, Router, RouterModule} from '../index'; +import {RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterModule} from '../index'; import {PreloadAllModules, PreloadingStrategy, RouterPreloader} from '../src/router_preloader'; import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing'; @@ -18,10 +18,6 @@ describe('RouterPreloader', () => { class LazyLoadedCmp { } - @Component({}) - class BlankCmp { - } - describe('should preload configurations', () => { @NgModule({ declarations: [LazyLoadedCmp], @@ -46,13 +42,18 @@ describe('RouterPreloader', () => { fakeAsync(inject( [NgModuleFactoryLoader, RouterPreloader, Router], (loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router) => { - const events: RouteConfigLoaded[] = []; + const events: Array = []; + router.events.subscribe(e => { - if (e instanceof RouteConfigLoaded) { + if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { events.push(e); } }); - loader.stubbedModules = {expected: LoadedModule1, expected2: LoadedModule2}; + + loader.stubbedModules = { + expected: LoadedModule1, + expected2: LoadedModule2, + }; preloader.preload().subscribe(() => {}); @@ -66,9 +67,13 @@ describe('RouterPreloader', () => { const loaded2: any = (loaded[0])._loadedConfig.routes; expect(loaded2[0].path).toEqual('LoadedModule2'); - expect(events.length).toEqual(2); - expect(events[0].route.path).toEqual('lazy'); - expect(events[1].route.path).toEqual('LoadedModule1'); + + expect(events.map(e => e.toString())).toEqual([ + 'RouteConfigLoadStart(path: lazy)', + 'RouteConfigLoadEnd(path: lazy)', + 'RouteConfigLoadStart(path: LoadedModule1)', + 'RouteConfigLoadEnd(path: LoadedModule1)', + ]); }))); }); diff --git a/tools/public_api_guard/router/index.d.ts b/tools/public_api_guard/router/index.d.ts index 4b54855779..244b1f1f26 100644 --- a/tools/public_api_guard/router/index.d.ts +++ b/tools/public_api_guard/router/index.d.ts @@ -70,7 +70,7 @@ export declare class DefaultUrlSerializer implements UrlSerializer { export declare type DetachedRouteHandle = {}; /** @stable */ -export declare type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized | RouteConfigLoaded; +export declare type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized | RouteConfigLoadStart | RouteConfigLoadEnd; /** @stable */ export interface ExtraOptions { @@ -200,7 +200,14 @@ export interface Route { } /** @experimental */ -export declare class RouteConfigLoaded { +export declare class RouteConfigLoadEnd { + route: Route; + constructor(route: Route); + toString(): string; +} + +/** @experimental */ +export declare class RouteConfigLoadStart { route: Route; constructor(route: Route); toString(): string;