diff --git a/modules/@angular/router/src/apply_redirects.ts b/modules/@angular/router/src/apply_redirects.ts index 779bebb3bd..f97cb6de49 100644 --- a/modules/@angular/router/src/apply_redirects.ts +++ b/modules/@angular/router/src/apply_redirects.ts @@ -233,7 +233,7 @@ class ApplyRedirects { segments: UrlSegment[]): Observable { 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) => { (route)._loadedConfig = r; return new UrlSegmentGroup(segments, {}); }); @@ -281,7 +281,7 @@ class ApplyRedirects { if ((route)._loadedConfig) { return of ((route)._loadedConfig); } else { - return map.call(this.configLoader.load(injector, route.loadChildren), (r: any) => { + return map.call(this.configLoader.load(injector, route), (r: any) => { (route)._loadedConfig = r; return r; }); diff --git a/modules/@angular/router/src/directives/router_link.ts b/modules/@angular/router/src/directives/router_link.ts index cd7f56724b..9c162f3f7d 100644 --- a/modules/@angular/router/src/directives/router_link.ts +++ b/modules/@angular/router/src/directives/router_link.ts @@ -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'; diff --git a/modules/@angular/router/src/directives/router_link_active.ts b/modules/@angular/router/src/directives/router_link_active.ts index 546f515ce7..be6288a296 100644 --- a/modules/@angular/router/src/directives/router_link_active.ts +++ b/modules/@angular/router/src/directives/router_link_active.ts @@ -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. * diff --git a/modules/@angular/router/src/directives/router_outlet.ts b/modules/@angular/router/src/directives/router_outlet.ts index 3b1a0c6d78..49ba9db661 100644 --- a/modules/@angular/router/src/directives/router_outlet.ts +++ b/modules/@angular/router/src/directives/router_outlet.ts @@ -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'; diff --git a/modules/@angular/router/src/events.ts b/modules/@angular/router/src/events.ts new file mode 100644 index 0000000000..f5071c92ad --- /dev/null +++ b/modules/@angular/router/src/events.ts @@ -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; diff --git a/modules/@angular/router/src/index.ts b/modules/@angular/router/src/index.ts index ff8d5ebd8b..86d8e8a0e1 100644 --- a/modules/@angular/router/src/index.ts +++ b/modules/@angular/router/src/index.ts @@ -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'; diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index a54175bd74..7e9bff311f 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -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(null); - private routerEvents = new Subject(); + /** @internal */ + routerEvents = new Subject(); 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(); } diff --git a/modules/@angular/router/src/router_config_loader.ts b/modules/@angular/router/src/router_config_loader.ts index e05ffd32a8..36f2a77c84 100644 --- a/modules/@angular/router/src/router_config_loader.ts +++ b/modules/@angular/router/src/router_config_loader.ts @@ -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 { - return map.call(this.loadModuleFactory(loadChildren), (r: NgModuleFactory) => { - const ref = r.create(parentInjector); - const injectorFactory = (parent: Injector) => r.create(parent).injector; + load(parentInjector: Injector, route: Route): Observable { + const moduleFactory$ = this.loadModuleFactory(route.loadChildren); + return map.call(moduleFactory$, (factory: NgModuleFactory) => { + 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); }); } diff --git a/modules/@angular/router/src/router_preloader.ts b/modules/@angular/router/src/router_preloader.ts index 753f54c42e..1457f685d0 100644 --- a/modules/@angular/router/src/router_preloader.ts +++ b/modules/@angular/router/src/router_preloader.ts @@ -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 { return this.processRoutes(this.injector, this.router.config); } - ngOnDestroy() { this.subscription.unsubscribe(); } + ngOnDestroy(): void { this.subscription.unsubscribe(); } private processRoutes(injector: Injector, routes: Routes): Observable { const res: Observable[] = []; @@ -114,7 +115,7 @@ export class RouterPreloader { private preloadConfig(injector: Injector, route: Route): Observable { 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; diff --git a/modules/@angular/router/test/integration.spec.ts b/modules/@angular/router/test/integration.spec.ts index 70331c2896..5f3d9682f3 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, 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 []' + }) + 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], diff --git a/modules/@angular/router/test/router_preloader.spec.ts b/modules/@angular/router/test/router_preloader.spec.ts index e74665a5cc..14392d2e38 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 {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 = (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'); }))); }); diff --git a/tools/public_api_guard/router/index.d.ts b/tools/public_api_guard/router/index.d.ts index 586486d80d..4b54855779 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; +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;