feat(router): introduce RouteConfigLoaded event

Closes #14036
This commit is contained in:
Dzmitry Shylovich 2017-02-02 00:13:57 +03:00 committed by Victor Berchet
parent 601fd3e305
commit 7df6f46c1c
12 changed files with 230 additions and 146 deletions

View File

@ -233,7 +233,7 @@ class ApplyRedirects {
segments: UrlSegment[]): Observable<UrlSegmentGroup> {
if (route.path === '**') {
if (route.loadChildren) {
return map.call(this.configLoader.load(injector, route.loadChildren), (r: any) => {
return map.call(this.configLoader.load(injector, route), (r: any) => {
(<any>route)._loadedConfig = r;
return new UrlSegmentGroup(segments, {});
});
@ -281,7 +281,7 @@ class ApplyRedirects {
if ((<any>route)._loadedConfig) {
return of ((<any>route)._loadedConfig);
} else {
return map.call(this.configLoader.load(injector, route.loadChildren), (r: any) => {
return map.call(this.configLoader.load(injector, route), (r: any) => {
(<any>route)._loadedConfig = r;
return r;
});

View File

@ -11,7 +11,8 @@ import {Attribute, Directive, ElementRef, HostBinding, HostListener, Input, OnCh
import {Subscription} from 'rxjs/Subscription';
import {QueryParamsHandling} from '../config';
import {NavigationEnd, Router} from '../router';
import {NavigationEnd} from '../events';
import {Router} from '../router';
import {ActivatedRoute} from '../router_state';
import {UrlTree} from '../url_tree';

View File

@ -8,13 +8,10 @@
import {AfterContentInit, ChangeDetectorRef, ContentChildren, Directive, ElementRef, Input, OnChanges, OnDestroy, QueryList, Renderer, SimpleChanges} from '@angular/core';
import {Subscription} from 'rxjs/Subscription';
import {NavigationEnd, Router} from '../router';
import {NavigationEnd} from '../events';
import {Router} from '../router';
import {RouterLink, RouterLinkWithHref} from './router_link';
/**
* @whatItDoes Lets you add a CSS class to an element when the link's route becomes active.
*

View File

@ -6,8 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Attribute, ComponentFactory, ComponentFactoryResolver, ComponentRef, Directive, EventEmitter, Injector, OnDestroy, Output, ReflectiveInjector, ResolvedReflectiveProvider, ViewContainerRef} from '@angular/core';
import {Attribute, ComponentFactoryResolver, ComponentRef, Directive, EventEmitter, Injector, OnDestroy, Output, ReflectiveInjector, ResolvedReflectiveProvider, ViewContainerRef} from '@angular/core';
import {RouterOutletMap} from '../router_outlet_map';
import {ActivatedRoute} from '../router_state';
import {PRIMARY_OUTLET} from '../shared';

View File

@ -0,0 +1,133 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Route} from './config';
import {RouterStateSnapshot} from './router_state';
/**
* @whatItDoes Represents an event triggered when a navigation starts.
*
* @stable
*/
export class NavigationStart {
// TODO: vsavkin: make internal
constructor(
/** @docsNotRequired */
public id: number,
/** @docsNotRequired */
public url: string) {}
/** @docsNotRequired */
toString(): string { return `NavigationStart(id: ${this.id}, url: '${this.url}')`; }
}
/**
* @whatItDoes Represents an event triggered when a navigation ends successfully.
*
* @stable
*/
export class NavigationEnd {
// TODO: vsavkin: make internal
constructor(
/** @docsNotRequired */
public id: number,
/** @docsNotRequired */
public url: string,
/** @docsNotRequired */
public urlAfterRedirects: string) {}
/** @docsNotRequired */
toString(): string {
return `NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`;
}
}
/**
* @whatItDoes Represents an event triggered when a navigation is canceled.
*
* @stable
*/
export class NavigationCancel {
// TODO: vsavkin: make internal
constructor(
/** @docsNotRequired */
public id: number,
/** @docsNotRequired */
public url: string,
/** @docsNotRequired */
public reason: string) {}
/** @docsNotRequired */
toString(): string { return `NavigationCancel(id: ${this.id}, url: '${this.url}')`; }
}
/**
* @whatItDoes Represents an event triggered when a navigation fails due to an unexpected error.
*
* @stable
*/
export class NavigationError {
// TODO: vsavkin: make internal
constructor(
/** @docsNotRequired */
public id: number,
/** @docsNotRequired */
public url: string,
/** @docsNotRequired */
public error: any) {}
/** @docsNotRequired */
toString(): string {
return `NavigationError(id: ${this.id}, url: '${this.url}', error: ${this.error})`;
}
}
/**
* @whatItDoes Represents an event triggered when routes are recognized.
*
* @stable
*/
export class RoutesRecognized {
// TODO: vsavkin: make internal
constructor(
/** @docsNotRequired */
public id: number,
/** @docsNotRequired */
public url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot) {}
/** @docsNotRequired */
toString(): string {
return `RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`;
}
}
/**
* @whatItDoes Represents an event triggered when route is lazy loaded.
*
* @experimental
*/
export class RouteConfigLoaded {
constructor(public route: Route) {}
toString(): string { return `RouteConfigLoaded(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.
*
* @stable
*/
export type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError |
RoutesRecognized | RouteConfigLoaded;

View File

@ -11,9 +11,9 @@ 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 {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces';
export {DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
export {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationExtras, NavigationStart, Router, RoutesRecognized} from './router';
export {ROUTES} from './router_config_loader';
export {ExtraOptions, ROUTER_CONFIGURATION, ROUTER_INITIALIZER, RouterModule, provideRoutes} from './router_module';
export {RouterOutletMap} from './router_outlet_map';

View File

@ -22,10 +22,11 @@ import {mergeMap} from 'rxjs/operator/mergeMap';
import {reduce} from 'rxjs/operator/reduce';
import {applyRedirects} from './apply_redirects';
import {QueryParamsHandling, ResolveData, Routes, validateConfig} from './config';
import {QueryParamsHandling, ResolveData, Route, Routes, validateConfig} from './config';
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 {recognize} from './recognize';
import {DetachedRouteHandle, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader';
@ -151,119 +152,6 @@ export interface NavigationExtras {
replaceUrl?: boolean;
}
/**
* @whatItDoes Represents an event triggered when a navigation starts.
*
* @stable
*/
export class NavigationStart {
// TODO: vsavkin: make internal
constructor(
/** @docsNotRequired */
public id: number,
/** @docsNotRequired */
public url: string) {}
/** @docsNotRequired */
toString(): string { return `NavigationStart(id: ${this.id}, url: '${this.url}')`; }
}
/**
* @whatItDoes Represents an event triggered when a navigation ends successfully.
*
* @stable
*/
export class NavigationEnd {
// TODO: vsavkin: make internal
constructor(
/** @docsNotRequired */
public id: number,
/** @docsNotRequired */
public url: string,
/** @docsNotRequired */
public urlAfterRedirects: string) {}
/** @docsNotRequired */
toString(): string {
return `NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`;
}
}
/**
* @whatItDoes Represents an event triggered when a navigation is canceled.
*
* @stable
*/
export class NavigationCancel {
// TODO: vsavkin: make internal
constructor(
/** @docsNotRequired */
public id: number,
/** @docsNotRequired */
public url: string,
/** @docsNotRequired */
public reason: string) {}
/** @docsNotRequired */
toString(): string { return `NavigationCancel(id: ${this.id}, url: '${this.url}')`; }
}
/**
* @whatItDoes Represents an event triggered when a navigation fails due to an unexpected error.
*
* @stable
*/
export class NavigationError {
// TODO: vsavkin: make internal
constructor(
/** @docsNotRequired */
public id: number,
/** @docsNotRequired */
public url: string,
/** @docsNotRequired */
public error: any) {}
/** @docsNotRequired */
toString(): string {
return `NavigationError(id: ${this.id}, url: '${this.url}', error: ${this.error})`;
}
}
/**
* @whatItDoes Represents an event triggered when routes are recognized.
*
* @stable
*/
export class RoutesRecognized {
// TODO: vsavkin: make internal
constructor(
/** @docsNotRequired */
public id: number,
/** @docsNotRequired */
public url: string,
/** @docsNotRequired */
public urlAfterRedirects: string,
/** @docsNotRequired */
public state: RouterStateSnapshot) {}
/** @docsNotRequired */
toString(): string {
return `RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`;
}
}
/**
* @whatItDoes Represents a router event.
*
* Please see {@link NavigationStart}, {@link NavigationEnd}, {@link NavigationCancel}, {@link
* NavigationError},
* {@link RoutesRecognized} for more information.
*
* @stable
*/
export type Event =
NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized;
/**
* @whatItDoes Error handler that is invoked when a navigation errors.
*
@ -320,7 +208,8 @@ export class Router {
private rawUrlTree: UrlTree;
private navigations = new BehaviorSubject<NavigationParams>(null);
private routerEvents = new Subject<Event>();
/** @internal */
routerEvents = new Subject<Event>();
private currentRouterState: RouterState;
private locationSubscription: Subscription;
@ -357,7 +246,8 @@ export class Router {
this.resetConfig(config);
this.currentUrlTree = createEmptyUrlTree();
this.rawUrlTree = this.currentUrlTree;
this.configLoader = new RouterConfigLoader(loader, compiler);
this.configLoader = new RouterConfigLoader(
loader, compiler, (r: Route) => this.routerEvents.next(new RouteConfigLoaded(r)));
this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType);
this.processNavigations();
}

View File

@ -12,11 +12,9 @@ import {fromPromise} from 'rxjs/observable/fromPromise';
import {of } from 'rxjs/observable/of';
import {map} from 'rxjs/operator/map';
import {mergeMap} from 'rxjs/operator/mergeMap';
import {LoadChildren, Route} from './config';
import {flatten, wrapIntoObservable} from './utils/collection';
/**
* @docsNotRequired
* @experimental
@ -30,14 +28,18 @@ export class LoadedRouterConfig {
}
export class RouterConfigLoader {
constructor(private loader: NgModuleFactoryLoader, private compiler: Compiler) {}
constructor(
private loader: NgModuleFactoryLoader, private compiler: Compiler,
private onLoadListener: (r: Route) => void) {}
load(parentInjector: Injector, loadChildren: LoadChildren): Observable<LoadedRouterConfig> {
return map.call(this.loadModuleFactory(loadChildren), (r: NgModuleFactory<any>) => {
const ref = r.create(parentInjector);
const injectorFactory = (parent: Injector) => r.create(parent).injector;
load(parentInjector: Injector, route: Route): Observable<LoadedRouterConfig> {
const moduleFactory$ = this.loadModuleFactory(route.loadChildren);
return map.call(moduleFactory$, (factory: NgModuleFactory<any>) => {
const module = factory.create(parentInjector);
const injectorFactory = (parent: Injector) => factory.create(parent).injector;
this.onLoadListener(route);
return new LoadedRouterConfig(
flatten(ref.injector.get(ROUTES)), ref.injector, ref.componentFactoryResolver,
flatten(module.injector.get(ROUTES)), module.injector, module.componentFactoryResolver,
injectorFactory);
});
}

View File

@ -16,9 +16,9 @@ import {concatMap} from 'rxjs/operator/concatMap';
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, Router} from './router';
import {NavigationEnd, RouteConfigLoaded} from './events';
import {Router} from './router';
import {RouterConfigLoader} from './router_config_loader';
/**
@ -80,17 +80,18 @@ export class RouterPreloader {
constructor(
private router: Router, moduleLoader: NgModuleFactoryLoader, compiler: Compiler,
private injector: Injector, private preloadingStrategy: PreloadingStrategy) {
this.loader = new RouterConfigLoader(moduleLoader, compiler);
this.loader = new RouterConfigLoader(
moduleLoader, compiler, (r: Route) => router.routerEvents.next(new RouteConfigLoaded(r)));
};
setUpPreloading(): void {
const navigations = filter.call(this.router.events, (e: any) => e instanceof NavigationEnd);
this.subscription = concatMap.call(navigations, () => this.preload()).subscribe((v: any) => {});
this.subscription = concatMap.call(navigations, () => this.preload()).subscribe(() => {});
}
preload(): Observable<any> { return this.processRoutes(this.injector, this.router.config); }
ngOnDestroy() { this.subscription.unsubscribe(); }
ngOnDestroy(): void { this.subscription.unsubscribe(); }
private processRoutes(injector: Injector, routes: Routes): Observable<void> {
const res: Observable<any>[] = [];
@ -114,7 +115,7 @@ export class RouterPreloader {
private preloadConfig(injector: Injector, route: Route): Observable<void> {
return this.preloadingStrategy.preload(route, () => {
const loaded = this.loader.load(injector, route.loadChildren);
const loaded = this.loader.load(injector, route);
return mergeMap.call(loaded, (config: any): any => {
const c: any = route;
c._loadedConfig = config;

View File

@ -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, 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, RouteConfigLoaded, 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';
@ -2011,8 +2011,8 @@ describe('Integration', () => {
expect(location.path()).toEqual('/lazyTrue/loaded');
expectEvents(recordedEvents, [
[NavigationStart, '/lazyTrue/loaded'], [RoutesRecognized, '/lazyTrue/loaded'],
[NavigationEnd, '/lazyTrue/loaded']
[NavigationStart, '/lazyTrue/loaded'], [RouteConfigLoaded, undefined],
[RoutesRecognized, '/lazyTrue/loaded'], [NavigationEnd, '/lazyTrue/loaded']
]);
})));
@ -2299,6 +2299,51 @@ describe('Integration', () => {
expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]');
})));
it('should emit RouteConfigLoaded event when route is lazy loaded',
fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
@Component({
selector: 'lazy',
template: 'lazy-loaded-parent [<router-outlet></router-outlet>]'
})
class ParentLazyLoadedComponent {
}
@Component({selector: 'lazy', template: 'lazy-loaded-child'})
class ChildLazyLoadedComponent {
}
@NgModule({
declarations: [ParentLazyLoadedComponent, ChildLazyLoadedComponent],
imports: [RouterModule.forChild([{
path: 'loaded',
component: ParentLazyLoadedComponent,
children: [{path: 'child', component: ChildLazyLoadedComponent}]
}])]
})
class LoadedModule {
}
const events: RouteConfigLoaded[] = [];
router.events.subscribe(e => {
if (e instanceof RouteConfigLoaded) {
events.push(e);
}
});
loader.stubbedModules = {expected: LoadedModule};
const fixture = createRoot(router, RootCmp);
router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]);
router.navigateByUrl('/lazy/loaded/child');
advance(fixture);
expect(events.length).toEqual(1);
expect(events[0].route.path).toEqual('lazy');
})));
it('throws an error when forRoot() is used in a lazy context',
fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],

View File

@ -9,7 +9,7 @@
import {Component, NgModule, NgModuleFactoryLoader} from '@angular/core';
import {TestBed, fakeAsync, inject, tick} from '@angular/core/testing';
import {Router, RouterModule} from '../index';
import {RouteConfigLoaded, Router, RouterModule} from '../index';
import {PreloadAllModules, PreloadingStrategy, RouterPreloader} from '../src/router_preloader';
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
@ -46,6 +46,12 @@ describe('RouterPreloader', () => {
fakeAsync(inject(
[NgModuleFactoryLoader, RouterPreloader, Router],
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router) => {
const events: RouteConfigLoaded[] = [];
router.events.subscribe(e => {
if (e instanceof RouteConfigLoaded) {
events.push(e);
}
});
loader.stubbedModules = {expected: LoadedModule1, expected2: LoadedModule2};
preloader.preload().subscribe(() => {});
@ -60,6 +66,9 @@ describe('RouterPreloader', () => {
const loaded2: any = (<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');
})));
});

View File

@ -70,7 +70,7 @@ export declare class DefaultUrlSerializer implements UrlSerializer {
export declare type DetachedRouteHandle = {};
/** @stable */
export declare type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized;
export declare type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized | RouteConfigLoaded;
/** @stable */
export interface ExtraOptions {
@ -199,6 +199,13 @@ export interface Route {
resolve?: ResolveData;
}
/** @experimental */
export declare class RouteConfigLoaded {
route: Route;
constructor(route: Route);
toString(): string;
}
/** @stable */
export declare class Router {
config: Routes;