feat(router): add `RouteConfigLoadStart` and `RouteConfigLoadEnd` events

This commit is contained in:
Victor Berchet 2017-02-15 10:57:03 -08:00
parent 7df6f46c1c
commit 78e8814103
8 changed files with 100 additions and 52 deletions

View File

@ -15,7 +15,6 @@ import {RouterStateSnapshot} from './router_state';
* @stable * @stable
*/ */
export class NavigationStart { export class NavigationStart {
// TODO: vsavkin: make internal
constructor( constructor(
/** @docsNotRequired */ /** @docsNotRequired */
public id: number, public id: number,
@ -32,7 +31,6 @@ export class NavigationStart {
* @stable * @stable
*/ */
export class NavigationEnd { export class NavigationEnd {
// TODO: vsavkin: make internal
constructor( constructor(
/** @docsNotRequired */ /** @docsNotRequired */
public id: number, public id: number,
@ -53,7 +51,6 @@ export class NavigationEnd {
* @stable * @stable
*/ */
export class NavigationCancel { export class NavigationCancel {
// TODO: vsavkin: make internal
constructor( constructor(
/** @docsNotRequired */ /** @docsNotRequired */
public id: number, public id: number,
@ -72,7 +69,6 @@ export class NavigationCancel {
* @stable * @stable
*/ */
export class NavigationError { export class NavigationError {
// TODO: vsavkin: make internal
constructor( constructor(
/** @docsNotRequired */ /** @docsNotRequired */
public id: number, public id: number,
@ -93,7 +89,6 @@ export class NavigationError {
* @stable * @stable
*/ */
export class RoutesRecognized { export class RoutesRecognized {
// TODO: vsavkin: make internal
constructor( constructor(
/** @docsNotRequired */ /** @docsNotRequired */
public id: number, 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 * @experimental
*/ */
export class RouteConfigLoaded { export class RouteConfigLoadStart {
constructor(public route: Route) {} 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. * @whatItDoes Represents a router event.
* *
* Please see {@link NavigationStart}, {@link NavigationEnd}, {@link NavigationCancel}, {@link * One of:
* NavigationError}, {@link RoutesRecognized}, {@link RouteConfigLoaded} for more information. * - {@link NavigationStart},
* - {@link NavigationEnd},
* - {@link NavigationCancel},
* - {@link NavigationError},
* - {@link RoutesRecognized},
* - {@link RouteConfigLoadStart},
* - {@link RouteConfigLoadEnd}
* *
* @stable * @stable
*/ */
export type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | export type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError |
RoutesRecognized | RouteConfigLoaded; RoutesRecognized | RouteConfigLoadStart | RouteConfigLoadEnd;

View File

@ -11,7 +11,7 @@ export {Data, LoadChildren, LoadChildrenCallback, ResolveData, Route, Routes} fr
export {RouterLink, RouterLinkWithHref} from './directives/router_link'; export {RouterLink, RouterLinkWithHref} from './directives/router_link';
export {RouterLinkActive} from './directives/router_link_active'; export {RouterLinkActive} from './directives/router_link_active';
export {RouterOutlet} from './directives/router_outlet'; 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 {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces';
export {DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy'; export {DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
export {ROUTES} from './router_config_loader'; export {ROUTES} from './router_config_loader';

View File

@ -26,7 +26,7 @@ import {QueryParamsHandling, ResolveData, Route, Routes, validateConfig} from '.
import {createRouterState} from './create_router_state'; import {createRouterState} from './create_router_state';
import {createUrlTree} from './create_url_tree'; import {createUrlTree} from './create_url_tree';
import {RouterOutlet} from './directives/router_outlet'; 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 {recognize} from './recognize';
import {DetachedRouteHandle, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy'; import {DetachedRouteHandle, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader'; import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader';
@ -208,8 +208,7 @@ export class Router {
private rawUrlTree: UrlTree; private rawUrlTree: UrlTree;
private navigations = new BehaviorSubject<NavigationParams>(null); private navigations = new BehaviorSubject<NavigationParams>(null);
/** @internal */ private routerEvents = new Subject<Event>();
routerEvents = new Subject<Event>();
private currentRouterState: RouterState; private currentRouterState: RouterState;
private locationSubscription: Subscription; private locationSubscription: Subscription;
@ -243,11 +242,14 @@ export class Router {
private rootComponentType: Type<any>, private urlSerializer: UrlSerializer, private rootComponentType: Type<any>, private urlSerializer: UrlSerializer,
private outletMap: RouterOutletMap, private location: Location, private injector: Injector, private outletMap: RouterOutletMap, private location: Location, private injector: Injector,
loader: NgModuleFactoryLoader, compiler: Compiler, public config: Routes) { 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.resetConfig(config);
this.currentUrlTree = createEmptyUrlTree(); this.currentUrlTree = createEmptyUrlTree();
this.rawUrlTree = this.currentUrlTree; 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.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType);
this.processNavigations(); this.processNavigations();
} }
@ -297,6 +299,9 @@ export class Router {
/** An observable of router events */ /** An observable of router events */
get events(): Observable<Event> { return this.routerEvents; } get events(): Observable<Event> { return this.routerEvents; }
/** @internal */
triggerEvent(e: Event) { this.routerEvents.next(e); }
/** /**
* Resets the configuration used for navigation and generating links. * Resets the configuration used for navigation and generating links.
* *

View File

@ -30,14 +30,24 @@ export class LoadedRouterConfig {
export class RouterConfigLoader { export class RouterConfigLoader {
constructor( constructor(
private loader: NgModuleFactoryLoader, private compiler: Compiler, 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<LoadedRouterConfig> { load(parentInjector: Injector, route: Route): Observable<LoadedRouterConfig> {
if (this.onLoadStartListener) {
this.onLoadStartListener(route);
}
const moduleFactory$ = this.loadModuleFactory(route.loadChildren); const moduleFactory$ = this.loadModuleFactory(route.loadChildren);
return map.call(moduleFactory$, (factory: NgModuleFactory<any>) => { return map.call(moduleFactory$, (factory: NgModuleFactory<any>) => {
if (this.onLoadEndListener) {
this.onLoadEndListener(route);
}
const module = factory.create(parentInjector); const module = factory.create(parentInjector);
const injectorFactory = (parent: Injector) => factory.create(parent).injector; const injectorFactory = (parent: Injector) => factory.create(parent).injector;
this.onLoadListener(route);
return new LoadedRouterConfig( return new LoadedRouterConfig(
flatten(module.injector.get(ROUTES)), module.injector, module.componentFactoryResolver, flatten(module.injector.get(ROUTES)), module.injector, module.componentFactoryResolver,
injectorFactory); injectorFactory);

View File

@ -17,7 +17,7 @@ import {filter} from 'rxjs/operator/filter';
import {mergeAll} from 'rxjs/operator/mergeAll'; import {mergeAll} from 'rxjs/operator/mergeAll';
import {mergeMap} from 'rxjs/operator/mergeMap'; import {mergeMap} from 'rxjs/operator/mergeMap';
import {Route, Routes} from './config'; import {Route, Routes} from './config';
import {NavigationEnd, RouteConfigLoaded} from './events'; import {NavigationEnd, RouteConfigLoadEnd, RouteConfigLoadStart} from './events';
import {Router} from './router'; import {Router} from './router';
import {RouterConfigLoader} from './router_config_loader'; import {RouterConfigLoader} from './router_config_loader';
@ -80,8 +80,10 @@ export class RouterPreloader {
constructor( constructor(
private router: Router, moduleLoader: NgModuleFactoryLoader, compiler: Compiler, private router: Router, moduleLoader: NgModuleFactoryLoader, compiler: Compiler,
private injector: Injector, private preloadingStrategy: PreloadingStrategy) { private injector: Injector, private preloadingStrategy: PreloadingStrategy) {
this.loader = new RouterConfigLoader( const onStartLoad = (r: Route) => router.triggerEvent(new RouteConfigLoadStart(r));
moduleLoader, compiler, (r: Route) => router.routerEvents.next(new RouteConfigLoaded(r))); const onEndLoad = (r: Route) => router.triggerEvent(new RouteConfigLoadEnd(r));
this.loader = new RouterConfigLoader(moduleLoader, compiler, onStartLoad, onEndLoad);
}; };
setUpPreloading(): void { setUpPreloading(): void {

View File

@ -14,7 +14,7 @@ import {expect} from '@angular/platform-browser/testing/matchers';
import {Observable} from 'rxjs/Observable'; import {Observable} from 'rxjs/Observable';
import {map} from 'rxjs/operator/map'; 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 {RouterPreloader} from '../src/router_preloader';
import {forEach} from '../src/utils/collection'; import {forEach} from '../src/utils/collection';
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing'; import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
@ -1949,15 +1949,16 @@ describe('Integration', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
{provide: 'alwaysFalse', useValue: (a: any) => false}, { {provide: 'alwaysFalse', useValue: (a: any) => false},
{
provide: 'returnFalseAndNavigate', provide: 'returnFalseAndNavigate',
useFactory: (router: any) => (a: any) => { useFactory: (router: any) => (a: any) => {
router.navigate(['blank']); router.navigate(['blank']);
return false; 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, [ expectEvents(recordedEvents, [
[NavigationStart, '/lazyFalse/loaded'], [NavigationStart, '/lazyFalse/loaded'],
[NavigationCancel, '/lazyFalse/loaded'] [NavigationCancel, '/lazyFalse/loaded'],
]); ]);
recordedEvents.splice(0); recordedEvents.splice(0);
@ -2011,8 +2012,11 @@ describe('Integration', () => {
expect(location.path()).toEqual('/lazyTrue/loaded'); expect(location.path()).toEqual('/lazyTrue/loaded');
expectEvents(recordedEvents, [ expectEvents(recordedEvents, [
[NavigationStart, '/lazyTrue/loaded'], [RouteConfigLoaded, undefined], [NavigationStart, '/lazyTrue/loaded'],
[RoutesRecognized, '/lazyTrue/loaded'], [NavigationEnd, '/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]'); 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( fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader], [Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => { (router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
@Component({ @Component({
selector: 'lazy', selector: 'lazy',
template: 'lazy-loaded-parent [<router-outlet></router-outlet>]' template: 'lazy-loaded-parent [<router-outlet></router-outlet>]',
}) })
class ParentLazyLoadedComponent { class ParentLazyLoadedComponent {
} }
@ -2319,16 +2323,16 @@ describe('Integration', () => {
imports: [RouterModule.forChild([{ imports: [RouterModule.forChild([{
path: 'loaded', path: 'loaded',
component: ParentLazyLoadedComponent, component: ParentLazyLoadedComponent,
children: [{path: 'child', component: ChildLazyLoadedComponent}] children: [{path: 'child', component: ChildLazyLoadedComponent}],
}])] }])]
}) })
class LoadedModule { class LoadedModule {
} }
const events: RouteConfigLoaded[] = []; const events: Array<RouteConfigLoadStart|RouteConfigLoadEnd> = [];
router.events.subscribe(e => { router.events.subscribe(e => {
if (e instanceof RouteConfigLoaded) { if (e instanceof RouteConfigLoadStart || e instanceof RouteConfigLoadEnd) {
events.push(e); events.push(e);
} }
}); });
@ -2340,8 +2344,9 @@ describe('Integration', () => {
router.navigateByUrl('/lazy/loaded/child'); router.navigateByUrl('/lazy/loaded/child');
advance(fixture); advance(fixture);
expect(events.length).toEqual(1); expect(events.length).toEqual(2);
expect(events[0].route.path).toEqual('lazy'); 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', it('throws an error when forRoot() is used in a lazy context',
@ -2535,9 +2540,11 @@ describe('Integration', () => {
expect(location.path()).toEqual('/'); expect(location.path()).toEqual('/');
expectEvents( expectEvents(recordedEvents, [
recordedEvents, [NavigationStart, '/lazy/loaded'],
[[NavigationStart, '/lazy/loaded'], [NavigationError, '/lazy/loaded']]); [RouteConfigLoadStart],
[NavigationError, '/lazy/loaded'],
]);
}))); })));
it('should work with complex redirect rules', it('should work with complex redirect rules',

View File

@ -9,7 +9,7 @@
import {Component, NgModule, NgModuleFactoryLoader} from '@angular/core'; import {Component, NgModule, NgModuleFactoryLoader} from '@angular/core';
import {TestBed, fakeAsync, inject, tick} from '@angular/core/testing'; 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 {PreloadAllModules, PreloadingStrategy, RouterPreloader} from '../src/router_preloader';
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing'; import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
@ -18,10 +18,6 @@ describe('RouterPreloader', () => {
class LazyLoadedCmp { class LazyLoadedCmp {
} }
@Component({})
class BlankCmp {
}
describe('should preload configurations', () => { describe('should preload configurations', () => {
@NgModule({ @NgModule({
declarations: [LazyLoadedCmp], declarations: [LazyLoadedCmp],
@ -46,13 +42,18 @@ describe('RouterPreloader', () => {
fakeAsync(inject( fakeAsync(inject(
[NgModuleFactoryLoader, RouterPreloader, Router], [NgModuleFactoryLoader, RouterPreloader, Router],
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router) => { (loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router) => {
const events: RouteConfigLoaded[] = []; const events: Array<RouteConfigLoadStart|RouteConfigLoadEnd> = [];
router.events.subscribe(e => { router.events.subscribe(e => {
if (e instanceof RouteConfigLoaded) { if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) {
events.push(e); events.push(e);
} }
}); });
loader.stubbedModules = {expected: LoadedModule1, expected2: LoadedModule2};
loader.stubbedModules = {
expected: LoadedModule1,
expected2: LoadedModule2,
};
preloader.preload().subscribe(() => {}); preloader.preload().subscribe(() => {});
@ -66,9 +67,13 @@ describe('RouterPreloader', () => {
const loaded2: any = (<any>loaded[0])._loadedConfig.routes; const loaded2: any = (<any>loaded[0])._loadedConfig.routes;
expect(loaded2[0].path).toEqual('LoadedModule2'); expect(loaded2[0].path).toEqual('LoadedModule2');
expect(events.length).toEqual(2);
expect(events[0].route.path).toEqual('lazy'); expect(events.map(e => e.toString())).toEqual([
expect(events[1].route.path).toEqual('LoadedModule1'); 'RouteConfigLoadStart(path: lazy)',
'RouteConfigLoadEnd(path: lazy)',
'RouteConfigLoadStart(path: LoadedModule1)',
'RouteConfigLoadEnd(path: LoadedModule1)',
]);
}))); })));
}); });

View File

@ -70,7 +70,7 @@ export declare class DefaultUrlSerializer implements UrlSerializer {
export declare type DetachedRouteHandle = {}; export declare type DetachedRouteHandle = {};
/** @stable */ /** @stable */
export declare type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized | RouteConfigLoaded; export declare type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized | RouteConfigLoadStart | RouteConfigLoadEnd;
/** @stable */ /** @stable */
export interface ExtraOptions { export interface ExtraOptions {
@ -200,7 +200,14 @@ export interface Route {
} }
/** @experimental */ /** @experimental */
export declare class RouteConfigLoaded { export declare class RouteConfigLoadEnd {
route: Route;
constructor(route: Route);
toString(): string;
}
/** @experimental */
export declare class RouteConfigLoadStart {
route: Route; route: Route;
constructor(route: Route); constructor(route: Route);
toString(): string; toString(): string;