feat(router): extend support for lazy loading children (#10705)

This commit is contained in:
Victor Savkin 2016-08-15 21:11:09 -07:00 committed by vikerman
parent bec5c5fdad
commit 6b26102931
10 changed files with 84 additions and 25 deletions

View File

@ -8,7 +8,7 @@
export {ExtraOptions, provideRouterConfig, provideRoutes} from './src/common_router_providers'; export {ExtraOptions, provideRouterConfig, provideRoutes} from './src/common_router_providers';
export {Data, ResolveData, Route, RouterConfig, Routes} from './src/config'; export {Data, LoadChildren, LoadChildrenCallback, ResolveData, Route, RouterConfig, Routes} from './src/config';
export {RouterLink, RouterLinkWithHref} from './src/directives/router_link'; export {RouterLink, RouterLinkWithHref} from './src/directives/router_link';
export {RouterLinkActive} from './src/directives/router_link_active'; export {RouterLinkActive} from './src/directives/router_link_active';
export {RouterOutlet} from './src/directives/router_outlet'; export {RouterOutlet} from './src/directives/router_outlet';

View File

@ -7,7 +7,7 @@
*/ */
import {Location, LocationStrategy, PathLocationStrategy} from '@angular/common'; import {Location, LocationStrategy, PathLocationStrategy} from '@angular/common';
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, ComponentResolver, Injector, NgModuleFactoryLoader, OpaqueToken, SystemJsNgModuleLoader} from '@angular/core'; import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, Compiler, ComponentResolver, Injector, NgModuleFactoryLoader, OpaqueToken, SystemJsNgModuleLoader} from '@angular/core';
import {Route, Routes} from './config'; import {Route, Routes} from './config';
import {Router} from './router'; import {Router} from './router';
@ -30,13 +30,13 @@ export interface ExtraOptions {
export function setupRouter( export function setupRouter(
ref: ApplicationRef, resolver: ComponentResolver, urlSerializer: UrlSerializer, ref: ApplicationRef, resolver: ComponentResolver, urlSerializer: UrlSerializer,
outletMap: RouterOutletMap, location: Location, injector: Injector, outletMap: RouterOutletMap, location: Location, injector: Injector,
loader: NgModuleFactoryLoader, config: Route[][], opts: ExtraOptions = {}) { loader: NgModuleFactoryLoader, compiler: Compiler, config: Route[][], opts: ExtraOptions = {}) {
if (ref.componentTypes.length == 0) { if (ref.componentTypes.length == 0) {
throw new Error('Bootstrap at least one component before injecting Router.'); throw new Error('Bootstrap at least one component before injecting Router.');
} }
const componentType = ref.componentTypes[0]; const componentType = ref.componentTypes[0];
const r = new Router( const r = new Router(
componentType, resolver, urlSerializer, outletMap, location, injector, loader, componentType, resolver, urlSerializer, outletMap, location, injector, loader, compiler,
flatten(config)); flatten(config));
if (opts.enableTracing) { if (opts.enableTracing) {
@ -92,7 +92,7 @@ export function provideRouter(routes: Routes, config: ExtraOptions = {}): any[]
useFactory: setupRouter, useFactory: setupRouter,
deps: [ deps: [
ApplicationRef, ComponentResolver, UrlSerializer, RouterOutletMap, Location, Injector, ApplicationRef, ComponentResolver, UrlSerializer, RouterOutletMap, Location, Injector,
NgModuleFactoryLoader, ROUTES, ROUTER_CONFIGURATION NgModuleFactoryLoader, Compiler, ROUTES, ROUTER_CONFIGURATION
] ]
}, },

View File

@ -7,6 +7,7 @@
*/ */
import {Type} from '@angular/core'; import {Type} from '@angular/core';
import {Observable} from 'rxjs/Observable';
/** /**
@ -474,6 +475,16 @@ export type ResolveData = {
[name: string]: any [name: string]: any
}; };
/**
* @experimental
*/
export type LoadChildrenCallback = () => Type<any>| Promise<Type<any>>| Observable<Type<any>>;
/**
* @experimental
*/
export type LoadChildren = string | LoadChildrenCallback;
/** /**
* See {@link Routes} for more details. * See {@link Routes} for more details.
* @stable * @stable
@ -496,7 +507,7 @@ export interface Route {
data?: Data; data?: Data;
resolve?: ResolveData; resolve?: ResolveData;
children?: Route[]; children?: Route[];
loadChildren?: string; loadChildren?: LoadChildren;
} }
export function validateConfig(config: Routes): void { export function validateConfig(config: Routes): void {

View File

@ -13,7 +13,7 @@ import 'rxjs/add/operator/reduce';
import 'rxjs/add/operator/every'; import 'rxjs/add/operator/every';
import {Location} from '@angular/common'; import {Location} from '@angular/common';
import {ComponentFactoryResolver, ComponentResolver, Injector, NgModuleFactoryLoader, ReflectiveInjector, Type} from '@angular/core'; import {Compiler, ComponentFactoryResolver, ComponentResolver, Injector, NgModuleFactoryLoader, ReflectiveInjector, Type} from '@angular/core';
import {Observable} from 'rxjs/Observable'; import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject'; import {Subject} from 'rxjs/Subject';
import {Subscription} from 'rxjs/Subscription'; import {Subscription} from 'rxjs/Subscription';
@ -148,11 +148,11 @@ export class Router {
private rootComponentType: Type<any>, private resolver: ComponentResolver, private rootComponentType: Type<any>, private resolver: ComponentResolver,
private urlSerializer: UrlSerializer, private outletMap: RouterOutletMap, private urlSerializer: UrlSerializer, private outletMap: RouterOutletMap,
private location: Location, private injector: Injector, loader: NgModuleFactoryLoader, private location: Location, private injector: Injector, loader: NgModuleFactoryLoader,
public config: Routes) { compiler: Compiler, public config: Routes) {
this.resetConfig(config); this.resetConfig(config);
this.routerEvents = new Subject<Event>(); this.routerEvents = new Subject<Event>();
this.currentUrlTree = createEmptyUrlTree(); this.currentUrlTree = createEmptyUrlTree();
this.configLoader = new RouterConfigLoader(loader); this.configLoader = new RouterConfigLoader(loader, compiler);
this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType); this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType);
} }

View File

@ -6,12 +6,14 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ComponentFactoryResolver, Injector, NgModuleFactoryLoader, OpaqueToken} from '@angular/core'; import {Compiler, ComponentFactoryResolver, Injector, NgModuleFactory, NgModuleFactoryLoader, OpaqueToken} from '@angular/core';
import {Observable} from 'rxjs/Observable'; import {Observable} from 'rxjs/Observable';
import {fromPromise} from 'rxjs/observable/fromPromise'; import {fromPromise} from 'rxjs/observable/fromPromise';
import {of } from 'rxjs/observable/of';
import {LoadChildren, Route} from './config';
import {flatten, wrapIntoObservable} from './utils/collection';
import {Route} from './config';
import {flatten} from './utils/collection';
/** /**
@ -27,13 +29,24 @@ export class LoadedRouterConfig {
} }
export class RouterConfigLoader { export class RouterConfigLoader {
constructor(private loader: NgModuleFactoryLoader) {} constructor(private loader: NgModuleFactoryLoader, private compiler: Compiler) {}
load(parentInjector: Injector, path: string): Observable<LoadedRouterConfig> { load(parentInjector: Injector, loadChildren: LoadChildren): Observable<LoadedRouterConfig> {
return fromPromise(this.loader.load(path).then(r => { return this.loadModuleFactory(loadChildren).map(r => {
const ref = r.create(parentInjector); const ref = r.create(parentInjector);
return new LoadedRouterConfig( return new LoadedRouterConfig(
flatten(ref.injector.get(ROUTES)), ref.injector, ref.componentFactoryResolver); flatten(ref.injector.get(ROUTES)), ref.injector, ref.componentFactoryResolver);
})); });
} }
}
private loadModuleFactory(loadChildren: LoadChildren): Observable<NgModuleFactory<any>> {
if (typeof loadChildren === 'string') {
return fromPromise(this.loader.load(loadChildren));
} else {
const offlineMode = this.compiler instanceof Compiler;
return wrapIntoObservable(loadChildren())
.mergeMap(
t => offlineMode ? of (<any>t) : fromPromise(this.compiler.compileModuleAsync(t)));
}
}
}

View File

@ -7,7 +7,7 @@
*/ */
import {APP_BASE_HREF, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common'; import {APP_BASE_HREF, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
import {ApplicationRef, ComponentResolver, Inject, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, OpaqueToken, Optional, SystemJsNgModuleLoader} from '@angular/core'; import {ApplicationRef, Compiler, ComponentResolver, Inject, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, OpaqueToken, Optional, SystemJsNgModuleLoader} from '@angular/core';
import {ExtraOptions, ROUTER_CONFIGURATION, provideRouterConfig, provideRouterInitializer, provideRoutes, rootRoute, setupRouter} from './common_router_providers'; import {ExtraOptions, ROUTER_CONFIGURATION, provideRouterConfig, provideRouterInitializer, provideRoutes, rootRoute, setupRouter} from './common_router_providers';
import {Routes} from './config'; import {Routes} from './config';
@ -42,7 +42,7 @@ export const ROUTER_PROVIDERS: any[] = [
useFactory: setupRouter, useFactory: setupRouter,
deps: [ deps: [
ApplicationRef, ComponentResolver, UrlSerializer, RouterOutletMap, Location, Injector, ApplicationRef, ComponentResolver, UrlSerializer, RouterOutletMap, Location, Injector,
NgModuleFactoryLoader, ROUTES, ROUTER_CONFIGURATION NgModuleFactoryLoader, Compiler, ROUTES, ROUTER_CONFIGURATION
] ]
}, },
RouterOutletMap, {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]}, RouterOutletMap, {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]},

View File

@ -122,7 +122,7 @@ export function andObservables(observables: Observable<Observable<any>>): Observ
return observables.mergeAll().every(result => result === true); return observables.mergeAll().every(result => result === true);
} }
export function wrapIntoObservable<T>(value: T | Observable<T>): Observable<T> { export function wrapIntoObservable<T>(value: T | Promise<T>| Observable<T>): Observable<T> {
if (value instanceof Observable) { if (value instanceof Observable) {
return value; return value;
} else if (value instanceof Promise) { } else if (value instanceof Promise) {

View File

@ -1608,6 +1608,33 @@ describe('Integration', () => {
expect(fixture.debugElement.nativeElement).toHaveText('lazy-loaded'); expect(fixture.debugElement.nativeElement).toHaveText('lazy-loaded');
}))); })));
it('works when given a callback',
fakeAsync(inject(
[Router, TestComponentBuilder, Location, NgModuleFactoryLoader],
(router: Router, tcb: TestComponentBuilder, location: Location) => {
@Component({selector: 'lazy', template: 'lazy-loaded'})
class LazyLoadedComponent {
}
@NgModule({
declarations: [LazyLoadedComponent],
imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])],
entryComponents: [LazyLoadedComponent]
})
class LoadedModule {
}
const fixture = createRoot(tcb, router, RootCmp);
router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]);
router.navigateByUrl('/lazy/loaded');
advance(fixture);
expect(location.path()).toEqual('/lazy/loaded');
expect(fixture.debugElement.nativeElement).toHaveText('lazy-loaded');
})));
it('error emit an error when cannot load a config', it('error emit an error when cannot load a config',
fakeAsync(inject( fakeAsync(inject(
[Router, TestComponentBuilder, Location, NgModuleFactoryLoader], [Router, TestComponentBuilder, Location, NgModuleFactoryLoader],

View File

@ -40,9 +40,11 @@ export class SpyNgModuleFactoryLoader implements NgModuleFactoryLoader {
function setupTestingRouter( function setupTestingRouter(
resolver: ComponentResolver, urlSerializer: UrlSerializer, outletMap: RouterOutletMap, resolver: ComponentResolver, urlSerializer: UrlSerializer, outletMap: RouterOutletMap,
location: Location, loader: NgModuleFactoryLoader, injector: Injector, routes: Route[][]) { location: Location, loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector,
routes: Route[][]) {
return new Router( return new Router(
null, resolver, urlSerializer, outletMap, location, injector, loader, flatten(routes)); null, resolver, urlSerializer, outletMap, location, injector, loader, compiler,
flatten(routes));
} }
/** /**
@ -75,7 +77,7 @@ function setupTestingRouter(
useFactory: setupTestingRouter, useFactory: setupTestingRouter,
deps: [ deps: [
ComponentResolver, UrlSerializer, RouterOutletMap, Location, NgModuleFactoryLoader, ComponentResolver, UrlSerializer, RouterOutletMap, Location, NgModuleFactoryLoader,
Injector, ROUTES Compiler, Injector, ROUTES
] ]
}, },
] ]

View File

@ -75,6 +75,12 @@ export interface ExtraOptions {
useHash?: boolean; useHash?: boolean;
} }
/** @experimental */
export declare type LoadChildren = string | LoadChildrenCallback;
/** @experimental */
export declare type LoadChildrenCallback = () => Type<any> | Promise<Type<any>> | Observable<Type<any>>;
/** @stable */ /** @stable */
export declare class NavigationCancel { export declare class NavigationCancel {
id: number; id: number;
@ -156,7 +162,7 @@ export interface Route {
children?: Route[]; children?: Route[];
component?: Type<any> | string; component?: Type<any> | string;
data?: Data; data?: Data;
loadChildren?: string; loadChildren?: LoadChildren;
outlet?: string; outlet?: string;
path?: string; path?: string;
pathMatch?: string; pathMatch?: string;
@ -172,7 +178,7 @@ export declare class Router {
/** @experimental */ navigated: boolean; /** @experimental */ navigated: boolean;
routerState: RouterState; routerState: RouterState;
url: string; url: string;
constructor(rootComponentType: Type<any>, resolver: ComponentResolver, urlSerializer: UrlSerializer, outletMap: RouterOutletMap, location: Location, injector: Injector, loader: NgModuleFactoryLoader, config: Routes); constructor(rootComponentType: Type<any>, resolver: ComponentResolver, urlSerializer: UrlSerializer, outletMap: RouterOutletMap, location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler, config: Routes);
createUrlTree(commands: any[], {relativeTo, queryParams, fragment, preserveQueryParams, preserveFragment}?: NavigationExtras): UrlTree; createUrlTree(commands: any[], {relativeTo, queryParams, fragment, preserveQueryParams, preserveFragment}?: NavigationExtras): UrlTree;
dispose(): void; dispose(): void;
initialNavigation(): void; initialNavigation(): void;