feat(router): add support for lazily loaded modules

This commit is contained in:
vsavkin 2016-07-06 11:02:16 -07:00
parent 6fcf962fb5
commit 8ebb8e44c8
13 changed files with 431 additions and 140 deletions

View File

@ -10,7 +10,7 @@ import {RouterLink, RouterLinkWithHref} from './src/directives/router_link';
import {RouterLinkActive} from './src/directives/router_link_active'; import {RouterLinkActive} from './src/directives/router_link_active';
import {RouterOutlet} from './src/directives/router_outlet'; import {RouterOutlet} from './src/directives/router_outlet';
export {ExtraOptions} from './src/common_router_providers'; export {ExtraOptions, provideRoutes} from './src/common_router_providers';
export {Data, ResolveData, Route, RouterConfig} from './src/config'; export {Data, ResolveData, Route, RouterConfig} 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';

View File

@ -6,14 +6,20 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/concatAll';
import {Observable} from 'rxjs/Observable'; import {Observable} from 'rxjs/Observable';
import {Observer} from 'rxjs/Observer'; import {Observer} from 'rxjs/Observer';
import {of } from 'rxjs/observable/of'; import {of } from 'rxjs/observable/of';
import {EmptyError} from 'rxjs/util/EmptyError';
import {Route, RouterConfig} from './config'; import {Route, RouterConfig} from './config';
import {RouterConfigLoader} from './router_config_loader';
import {PRIMARY_OUTLET} from './shared'; import {PRIMARY_OUTLET} from './shared';
import {UrlPathWithParams, UrlSegment, UrlTree, mapChildren} from './url_tree'; import {UrlPathWithParams, UrlSegment, UrlTree} from './url_tree';
import {merge} from './utils/collection'; import {merge, waitForMap} from './utils/collection';
class NoMatch { class NoMatch {
constructor(public segment: UrlSegment = null) {} constructor(public segment: UrlSegment = null) {}
@ -22,138 +28,192 @@ class AbsoluteRedirect {
constructor(public paths: UrlPathWithParams[]) {} constructor(public paths: UrlPathWithParams[]) {}
} }
export function applyRedirects(urlTree: UrlTree, config: RouterConfig): Observable<UrlTree> { function noMatch(segment: UrlSegment): Observable<UrlSegment> {
try { return new Observable<UrlSegment>((obs: Observer<UrlSegment>) => obs.error(new NoMatch(segment)));
return createUrlTree(urlTree, expandSegment(config, urlTree.root, PRIMARY_OUTLET));
} catch (e) {
if (e instanceof AbsoluteRedirect) {
return createUrlTree(
urlTree, new UrlSegment([], {[PRIMARY_OUTLET]: new UrlSegment(e.paths, {})}));
} else if (e instanceof NoMatch) {
return new Observable<UrlTree>(
(obs: Observer<UrlTree>) =>
obs.error(new Error(`Cannot match any routes: '${e.segment}'`)));
} else {
return new Observable<UrlTree>((obs: Observer<UrlTree>) => obs.error(e));
}
}
} }
function createUrlTree(urlTree: UrlTree, rootCandidate: UrlSegment): Observable<UrlTree> { function absoluteRedirect(newPaths: UrlPathWithParams[]): Observable<UrlSegment> {
return new Observable<UrlSegment>(
(obs: Observer<UrlSegment>) => obs.error(new AbsoluteRedirect(newPaths)));
}
export function applyRedirects(
configLoader: RouterConfigLoader, urlTree: UrlTree, config: RouterConfig): Observable<UrlTree> {
return expandSegment(configLoader, config, urlTree.root, PRIMARY_OUTLET)
.map(rootSegment => createUrlTree(urlTree, rootSegment))
.catch(e => {
if (e instanceof AbsoluteRedirect) {
return of (createUrlTree(
urlTree, new UrlSegment([], {[PRIMARY_OUTLET]: new UrlSegment(e.paths, {})})));
} else if (e instanceof NoMatch) {
throw new Error(`Cannot match any routes: '${e.segment}'`);
} else {
throw e;
}
});
}
function createUrlTree(urlTree: UrlTree, rootCandidate: UrlSegment): UrlTree {
const root = rootCandidate.pathsWithParams.length > 0 ? const root = rootCandidate.pathsWithParams.length > 0 ?
new UrlSegment([], {[PRIMARY_OUTLET]: rootCandidate}) : new UrlSegment([], {[PRIMARY_OUTLET]: rootCandidate}) :
rootCandidate; rootCandidate;
return of (new UrlTree(root, urlTree.queryParams, urlTree.fragment)); return new UrlTree(root, urlTree.queryParams, urlTree.fragment);
} }
function expandSegment(routes: Route[], segment: UrlSegment, outlet: string): UrlSegment { function expandSegment(
configLoader: RouterConfigLoader, routes: Route[], segment: UrlSegment,
outlet: string): Observable<UrlSegment> {
if (segment.pathsWithParams.length === 0 && segment.hasChildren()) { if (segment.pathsWithParams.length === 0 && segment.hasChildren()) {
return new UrlSegment([], expandSegmentChildren(routes, segment)); return expandSegmentChildren(configLoader, routes, segment)
.map(children => new UrlSegment([], children));
} else { } else {
return expandPathsWithParams(segment, routes, segment.pathsWithParams, outlet, true); return expandPathsWithParams(
configLoader, segment, routes, segment.pathsWithParams, outlet, true);
} }
} }
function expandSegmentChildren(routes: Route[], segment: UrlSegment): {[name: string]: UrlSegment} { function expandSegmentChildren(
return mapChildren(segment, (child, childOutlet) => expandSegment(routes, child, childOutlet)); configLoader: RouterConfigLoader, routes: Route[],
segment: UrlSegment): Observable<{[name: string]: UrlSegment}> {
return waitForMap(
segment.children,
(childOutlet, child) => expandSegment(configLoader, routes, child, childOutlet));
} }
function expandPathsWithParams( function expandPathsWithParams(
segment: UrlSegment, routes: Route[], paths: UrlPathWithParams[], outlet: string, configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[],
allowRedirects: boolean): UrlSegment { paths: UrlPathWithParams[], outlet: string, allowRedirects: boolean): Observable<UrlSegment> {
for (let r of routes) { const processRoutes =
try { of (...routes)
return expandPathsWithParamsAgainstRoute(segment, routes, r, paths, outlet, allowRedirects); .map(r => {
} catch (e) { return expandPathsWithParamsAgainstRoute(
if (!(e instanceof NoMatch)) throw e; configLoader, segment, routes, r, paths, outlet, allowRedirects)
.catch((e) => {
if (e instanceof NoMatch)
return of (null);
else
throw e;
});
})
.concatAll();
return processRoutes.first(s => !!s).catch((e: any, _: any): Observable<UrlSegment> => {
if (e instanceof EmptyError) {
throw new NoMatch(segment);
} else {
throw e;
} }
} });
throw new NoMatch(segment);
} }
function expandPathsWithParamsAgainstRoute( function expandPathsWithParamsAgainstRoute(
segment: UrlSegment, routes: Route[], route: Route, paths: UrlPathWithParams[], outlet: string, configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[], route: Route,
allowRedirects: boolean): UrlSegment { paths: UrlPathWithParams[], outlet: string, allowRedirects: boolean): Observable<UrlSegment> {
if (getOutlet(route) !== outlet) throw new NoMatch(); if (getOutlet(route) !== outlet) return noMatch(segment);
if (route.redirectTo !== undefined && !allowRedirects) throw new NoMatch(); if (route.redirectTo !== undefined && !allowRedirects) return noMatch(segment);
if (route.redirectTo !== undefined) { if (route.redirectTo !== undefined) {
return expandPathsWithParamsAgainstRouteUsingRedirect(segment, routes, route, paths, outlet); return expandPathsWithParamsAgainstRouteUsingRedirect(
configLoader, segment, routes, route, paths, outlet);
} else { } else {
return matchPathsWithParamsAgainstRoute(segment, route, paths); return matchPathsWithParamsAgainstRoute(configLoader, segment, route, paths);
} }
} }
function expandPathsWithParamsAgainstRouteUsingRedirect( function expandPathsWithParamsAgainstRouteUsingRedirect(
segment: UrlSegment, routes: Route[], route: Route, paths: UrlPathWithParams[], configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[], route: Route,
outlet: string): UrlSegment { paths: UrlPathWithParams[], outlet: string): Observable<UrlSegment> {
if (route.path === '**') { if (route.path === '**') {
return expandWildCardWithParamsAgainstRouteUsingRedirect(route); return expandWildCardWithParamsAgainstRouteUsingRedirect(route);
} else { } else {
return expandRegularPathWithParamsAgainstRouteUsingRedirect( return expandRegularPathWithParamsAgainstRouteUsingRedirect(
segment, routes, route, paths, outlet); configLoader, segment, routes, route, paths, outlet);
} }
} }
function expandWildCardWithParamsAgainstRouteUsingRedirect(route: Route): UrlSegment { function expandWildCardWithParamsAgainstRouteUsingRedirect(route: Route): Observable<UrlSegment> {
const newPaths = applyRedirectCommands([], route.redirectTo, {}); const newPaths = applyRedirectCommands([], route.redirectTo, {});
if (route.redirectTo.startsWith('/')) { if (route.redirectTo.startsWith('/')) {
throw new AbsoluteRedirect(newPaths); return absoluteRedirect(newPaths);
} else { } else {
return new UrlSegment(newPaths, {}); return of (new UrlSegment(newPaths, {}));
} }
} }
function expandRegularPathWithParamsAgainstRouteUsingRedirect( function expandRegularPathWithParamsAgainstRouteUsingRedirect(
segment: UrlSegment, routes: Route[], route: Route, paths: UrlPathWithParams[], configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[], route: Route,
outlet: string): UrlSegment { paths: UrlPathWithParams[], outlet: string): Observable<UrlSegment> {
const {consumedPaths, lastChild, positionalParamSegments} = match(segment, route, paths); const {matched, consumedPaths, lastChild, positionalParamSegments} = match(segment, route, paths);
if (!matched) return noMatch(segment);
const newPaths = const newPaths =
applyRedirectCommands(consumedPaths, route.redirectTo, <any>positionalParamSegments); applyRedirectCommands(consumedPaths, route.redirectTo, <any>positionalParamSegments);
if (route.redirectTo.startsWith('/')) { if (route.redirectTo.startsWith('/')) {
throw new AbsoluteRedirect(newPaths); return absoluteRedirect(newPaths);
} else { } else {
return expandPathsWithParams( return expandPathsWithParams(
segment, routes, newPaths.concat(paths.slice(lastChild)), outlet, false); configLoader, segment, routes, newPaths.concat(paths.slice(lastChild)), outlet, false);
} }
} }
function matchPathsWithParamsAgainstRoute( function matchPathsWithParamsAgainstRoute(
rawSegment: UrlSegment, route: Route, paths: UrlPathWithParams[]): UrlSegment { configLoader: RouterConfigLoader, rawSegment: UrlSegment, route: Route,
paths: UrlPathWithParams[]): Observable<UrlSegment> {
if (route.path === '**') { if (route.path === '**') {
return new UrlSegment(paths, {}); return of (new UrlSegment(paths, {}));
} else { } else {
const {consumedPaths, lastChild} = match(rawSegment, route, paths); const {matched, consumedPaths, lastChild} = match(rawSegment, route, paths);
const childConfig = route.children ? route.children : []; if (!matched) return noMatch(rawSegment);
const rawSlicedPath = paths.slice(lastChild); const rawSlicedPath = paths.slice(lastChild);
const {segment, slicedPath} = split(rawSegment, consumedPaths, rawSlicedPath, childConfig); return getChildConfig(configLoader, route).mergeMap(childConfig => {
const {segment, slicedPath} = split(rawSegment, consumedPaths, rawSlicedPath, childConfig);
if (slicedPath.length === 0 && segment.hasChildren()) { if (slicedPath.length === 0 && segment.hasChildren()) {
const children = expandSegmentChildren(childConfig, segment); return expandSegmentChildren(configLoader, childConfig, segment)
return new UrlSegment(consumedPaths, children); .map(children => new UrlSegment(consumedPaths, children));
} else if (childConfig.length === 0 && slicedPath.length === 0) { } else if (childConfig.length === 0 && slicedPath.length === 0) {
return new UrlSegment(consumedPaths, {}); return of (new UrlSegment(consumedPaths, {}));
} else { } else {
const cs = expandPathsWithParams(segment, childConfig, slicedPath, PRIMARY_OUTLET, true); return expandPathsWithParams(
return new UrlSegment(consumedPaths.concat(cs.pathsWithParams), cs.children); configLoader, segment, childConfig, slicedPath, PRIMARY_OUTLET, true)
} .map(cs => new UrlSegment(consumedPaths.concat(cs.pathsWithParams), cs.children));
}
});
}
}
function getChildConfig(configLoader: RouterConfigLoader, route: Route): Observable<Route[]> {
if (route.children) {
return of (route.children);
} else if (route.mountChildren) {
return configLoader.load(route.mountChildren).map(r => {
(<any>route)._loadedConfig = r;
return r.routes;
});
} else {
return of ([]);
} }
} }
function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]): { function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]): {
matched: boolean,
consumedPaths: UrlPathWithParams[], consumedPaths: UrlPathWithParams[],
lastChild: number, lastChild: number,
positionalParamSegments: {[k: string]: UrlPathWithParams} positionalParamSegments: {[k: string]: UrlPathWithParams}
} { } {
const noMatch =
{matched: false, consumedPaths: <any[]>[], lastChild: 0, positionalParamSegments: {}};
if (route.path === '') { if (route.path === '') {
if ((route.terminal || route.pathMatch === 'full') && if ((route.terminal || route.pathMatch === 'full') &&
(segment.hasChildren() || paths.length > 0)) { (segment.hasChildren() || paths.length > 0)) {
throw new NoMatch(); return {matched: false, consumedPaths: [], lastChild: 0, positionalParamSegments: {}};
} else { } else {
return {consumedPaths: [], lastChild: 0, positionalParamSegments: {}}; return {matched: true, consumedPaths: [], lastChild: 0, positionalParamSegments: {}};
} }
} }
@ -165,13 +225,13 @@ function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]): {
let currentIndex = 0; let currentIndex = 0;
for (let i = 0; i < parts.length; ++i) { for (let i = 0; i < parts.length; ++i) {
if (currentIndex >= paths.length) throw new NoMatch(); if (currentIndex >= paths.length) return noMatch;
const current = paths[currentIndex]; const current = paths[currentIndex];
const p = parts[i]; const p = parts[i];
const isPosParam = p.startsWith(':'); const isPosParam = p.startsWith(':');
if (!isPosParam && p !== current.path) throw new NoMatch(); if (!isPosParam && p !== current.path) return noMatch;
if (isPosParam) { if (isPosParam) {
positionalParamSegments[p.substring(1)] = current; positionalParamSegments[p.substring(1)] = current;
} }
@ -180,10 +240,10 @@ function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]): {
} }
if (route.terminal && (segment.hasChildren() || currentIndex < paths.length)) { if (route.terminal && (segment.hasChildren() || currentIndex < paths.length)) {
throw new NoMatch(); return {matched: false, consumedPaths: [], lastChild: 0, positionalParamSegments: {}};
} }
return {consumedPaths, lastChild: currentIndex, positionalParamSegments}; return {matched: true, consumedPaths, lastChild: currentIndex, positionalParamSegments};
} }
function applyRedirectCommands( function applyRedirectCommands(

View File

@ -7,7 +7,7 @@
*/ */
import {Location, LocationStrategy, PathLocationStrategy} from '@angular/common'; import {Location, LocationStrategy, PathLocationStrategy} from '@angular/common';
import {APP_INITIALIZER, ApplicationRef, ComponentResolver, Injector, OpaqueToken} from '@angular/core'; import {APP_INITIALIZER, AppModuleFactoryLoader, ApplicationRef, ComponentResolver, Injector, OpaqueToken, SystemJsAppModuleLoader} from '@angular/core';
import {RouterConfig} from './config'; import {RouterConfig} from './config';
import {Router} from './router'; import {Router} from './router';
@ -25,14 +25,14 @@ export interface ExtraOptions { enableTracing?: boolean; }
export function setupRouter( export function setupRouter(
ref: ApplicationRef, resolver: ComponentResolver, urlSerializer: UrlSerializer, ref: ApplicationRef, resolver: ComponentResolver, urlSerializer: UrlSerializer,
outletMap: RouterOutletMap, location: Location, injector: Injector, config: RouterConfig, outletMap: RouterOutletMap, location: Location, injector: Injector,
opts: ExtraOptions) { loader: AppModuleFactoryLoader, config: RouterConfig, 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 = const r = new Router(
new Router(componentType, resolver, urlSerializer, outletMap, location, injector, config); componentType, resolver, urlSerializer, outletMap, location, injector, loader, config);
ref.registerDisposeListener(() => r.dispose()); ref.registerDisposeListener(() => r.dispose());
if (opts.enableTracing) { if (opts.enableTracing) {
@ -93,7 +93,7 @@ export function provideRouter(_config: RouterConfig, _opts: ExtraOptions): any[]
useFactory: setupRouter, useFactory: setupRouter,
deps: [ deps: [
ApplicationRef, ComponentResolver, UrlSerializer, RouterOutletMap, Location, Injector, ApplicationRef, ComponentResolver, UrlSerializer, RouterOutletMap, Location, Injector,
ROUTER_CONFIG, ROUTER_OPTIONS AppModuleFactoryLoader, ROUTER_CONFIG, ROUTER_OPTIONS
] ]
}, },
@ -101,6 +101,27 @@ export function provideRouter(_config: RouterConfig, _opts: ExtraOptions): any[]
{provide: ActivatedRoute, useFactory: (r: Router) => r.routerState.root, deps: [Router]}, {provide: ActivatedRoute, useFactory: (r: Router) => r.routerState.root, deps: [Router]},
// Trigger initial navigation // Trigger initial navigation
{provide: APP_INITIALIZER, multi: true, useFactory: setupRouterInitializer, deps: [Injector]} {provide: APP_INITIALIZER, multi: true, useFactory: setupRouterInitializer, deps: [Injector]},
{provide: AppModuleFactoryLoader, useClass: SystemJsAppModuleLoader}
]; ];
} }
/**
* Router configuration.
*
* ### Example
*
* ```
* @AppModule({providers: [
* provideRoutes([{path: 'home', component: Home}])
* ]})
* class LazyLoadedModule {
* // ...
* }
* ```
*
* @experimental
*/
export function provideRoutes(config: RouterConfig): any {
return {provide: ROUTER_CONFIG, useValue: config};
}

View File

@ -267,6 +267,7 @@ export interface Route {
data?: Data; data?: Data;
resolve?: ResolveData; resolve?: ResolveData;
children?: Route[]; children?: Route[];
mountChildren?: string;
} }
export function validateConfig(config: RouterConfig): void { export function validateConfig(config: RouterConfig): void {
@ -278,13 +279,22 @@ function validateNode(route: Route): void {
throw new Error( throw new Error(
`Invalid configuration of route '${route.path}': redirectTo and children cannot be used together`); `Invalid configuration of route '${route.path}': redirectTo and children cannot be used together`);
} }
if (!!route.redirectTo && !!route.mountChildren) {
throw new Error(
`Invalid configuration of route '${route.path}': redirectTo and mountChildren cannot be used together`);
}
if (!!route.children && !!route.mountChildren) {
throw new Error(
`Invalid configuration of route '${route.path}': children and mountChildren cannot be used together`);
}
if (!!route.redirectTo && !!route.component) { if (!!route.redirectTo && !!route.component) {
throw new Error( throw new Error(
`Invalid configuration of route '${route.path}': redirectTo and component cannot be used together`); `Invalid configuration of route '${route.path}': redirectTo and component cannot be used together`);
} }
if (route.redirectTo === undefined && !route.component && !route.children) { if (route.redirectTo === undefined && !route.component && !route.children &&
!route.mountChildren) {
throw new Error( throw new Error(
`Invalid configuration of route '${route.path}': component, redirectTo, children must be provided`); `Invalid configuration of route '${route.path}': component, redirectTo, children, mountChildren must be provided`);
} }
if (route.path === undefined) { if (route.path === undefined) {
throw new Error(`Invalid route configuration: routes must have path specified`); throw new Error(`Invalid route configuration: routes must have path specified`);

View File

@ -33,7 +33,7 @@ export class RouterOutlet {
constructor( constructor(
parentOutletMap: RouterOutletMap, private location: ViewContainerRef, parentOutletMap: RouterOutletMap, private location: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver, @Attribute('name') name: string) { private resolver: ComponentFactoryResolver, @Attribute('name') name: string) {
parentOutletMap.registerOutlet(name ? name : PRIMARY_OUTLET, this); parentOutletMap.registerOutlet(name ? name : PRIMARY_OUTLET, this);
} }
@ -55,8 +55,8 @@ export class RouterOutlet {
} }
activate( activate(
activatedRoute: ActivatedRoute, providers: ResolvedReflectiveProvider[], activatedRoute: ActivatedRoute, loadedResolver: ComponentFactoryResolver,
outletMap: RouterOutletMap): void { providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void {
this.outletMap = outletMap; this.outletMap = outletMap;
this._activatedRoute = activatedRoute; this._activatedRoute = activatedRoute;
@ -65,9 +65,13 @@ export class RouterOutlet {
let factory: ComponentFactory<any>; let factory: ComponentFactory<any>;
try { try {
factory = typeof component === 'string' ? if (typeof component === 'string') {
snapshot._resolvedComponentFactory : factory = snapshot._resolvedComponentFactory;
this.componentFactoryResolver.resolveComponentFactory(component); } else if (loadedResolver) {
factory = loadedResolver.resolveComponentFactory(component);
} else {
factory = this.resolver.resolveComponentFactory(component);
}
} catch (e) { } catch (e) {
if (!(e instanceof NoComponentFactoryError)) throw e; if (!(e instanceof NoComponentFactoryError)) throw e;

View File

@ -122,7 +122,7 @@ function processPathsWithParamsAgainstRoute(
const {consumedPaths, parameters, lastChild} = match(rawSegment, route, paths); const {consumedPaths, parameters, lastChild} = match(rawSegment, route, paths);
const rawSlicedPath = paths.slice(lastChild); const rawSlicedPath = paths.slice(lastChild);
const childConfig = route.children ? route.children : []; const childConfig = getChildConfig(route);
const newInherited = route.component ? const newInherited = route.component ?
InheritedFromParent.empty : InheritedFromParent.empty :
new InheritedFromParent(inherited, parameters, getData(route), newInheritedResolve); new InheritedFromParent(inherited, parameters, getData(route), newInheritedResolve);
@ -149,6 +149,16 @@ function processPathsWithParamsAgainstRoute(
} }
} }
function getChildConfig(route: Route): Route[] {
if (route.children) {
return route.children;
} else if (route.mountChildren) {
return (<any>route)._loadedConfig.routes;
} else {
return [];
}
}
function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]) { function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]) {
if (route.path === '') { if (route.path === '') {
if ((route.terminal || route.pathMatch === 'full') && if ((route.terminal || route.pathMatch === 'full') &&

View File

@ -15,24 +15,25 @@ import 'rxjs/add/observable/from';
import 'rxjs/add/observable/forkJoin'; import 'rxjs/add/observable/forkJoin';
import {Location} from '@angular/common'; import {Location} from '@angular/common';
import {ComponentResolver, Injector, ReflectiveInjector, Type} from '@angular/core'; import {AppModuleFactoryLoader, ComponentFactoryResolver, ComponentResolver, Injector, 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';
import {of } from 'rxjs/observable/of'; import {of } from 'rxjs/observable/of';
import {applyRedirects} from './apply_redirects'; import {applyRedirects} from './apply_redirects';
import {Data, ResolveData, RouterConfig, validateConfig} from './config'; import {ResolveData, RouterConfig, validateConfig} from './config';
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 {recognize} from './recognize'; import {recognize} from './recognize';
import {resolve} from './resolve'; import {resolve} from './resolve';
import {RouterConfigLoader} from './router_config_loader';
import {RouterOutletMap} from './router_outlet_map'; import {RouterOutletMap} from './router_outlet_map';
import {ActivatedRoute, ActivatedRouteSnapshot, InheritedResolve, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state'; import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state';
import {PRIMARY_OUTLET, Params} from './shared'; import {PRIMARY_OUTLET, Params} from './shared';
import {UrlSerializer, UrlTree, createEmptyUrlTree} from './url_tree'; import {UrlSerializer, UrlTree, createEmptyUrlTree} from './url_tree';
import {forEach, merge, shallowEqual} from './utils/collection'; import {forEach, merge, shallowEqual, waitForMap} from './utils/collection';
import {TreeNode} from './utils/tree'; import {TreeNode} from './utils/tree';
export interface NavigationExtras { export interface NavigationExtras {
@ -124,6 +125,7 @@ export class Router {
private navigationId: number = 0; private navigationId: number = 0;
private config: RouterConfig; private config: RouterConfig;
private futureUrlTree: UrlTree; private futureUrlTree: UrlTree;
private configLoader: RouterConfigLoader;
/** /**
* Creates the router service. * Creates the router service.
@ -131,11 +133,13 @@ export class Router {
constructor( constructor(
private rootComponentType: Type, private resolver: ComponentResolver, private rootComponentType: Type, private resolver: ComponentResolver,
private urlSerializer: UrlSerializer, private outletMap: RouterOutletMap, private urlSerializer: UrlSerializer, private outletMap: RouterOutletMap,
private location: Location, private injector: Injector, config: RouterConfig) { private location: Location, private injector: Injector, loader: AppModuleFactoryLoader,
config: RouterConfig) {
this.resetConfig(config); this.resetConfig(config);
this.routerEvents = new Subject<Event>(); this.routerEvents = new Subject<Event>();
this.currentUrlTree = createEmptyUrlTree(); this.currentUrlTree = createEmptyUrlTree();
this.futureUrlTree = this.currentUrlTree; this.futureUrlTree = this.currentUrlTree;
this.configLoader = new RouterConfigLoader(loader);
this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType); this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType);
} }
@ -310,7 +314,7 @@ export class Router {
let state: RouterState; let state: RouterState;
let navigationIsSuccessful: boolean; let navigationIsSuccessful: boolean;
let preActivation: PreActivation; let preActivation: PreActivation;
applyRedirects(url, this.config) applyRedirects(this.configLoader, url, this.config)
.mergeMap(u => { .mergeMap(u => {
this.futureUrlTree = u; this.futureUrlTree = u;
return recognize( return recognize(
@ -555,20 +559,11 @@ class PreActivation {
} }
private resolveNode(resolve: ResolveData, future: ActivatedRouteSnapshot): Observable<any> { private resolveNode(resolve: ResolveData, future: ActivatedRouteSnapshot): Observable<any> {
const resolvingObs: Observable<any>[] = []; return waitForMap(resolve, (k, v) => {
const resolvedData: {[k: string]: any} = {};
forEach(resolve, (v: any, k: string) => {
const resolver = this.injector.get(v); const resolver = this.injector.get(v);
const obs = resolver.resolve ? wrapIntoObservable(resolver.resolve(future, this.future)) : return resolver.resolve ? wrapIntoObservable(resolver.resolve(future, this.future)) :
wrapIntoObservable(resolver(future, this.future)); wrapIntoObservable(resolver(future, this.future));
resolvingObs.push(obs.map((_: any) => { resolvedData[k] = _; }));
}); });
if (resolvingObs.length > 0) {
return Observable.forkJoin(resolvingObs).map(r => resolvedData);
} else {
return of (resolvedData);
}
} }
} }
@ -656,11 +651,22 @@ class ActivateRoutes {
private placeComponentIntoOutlet( private placeComponentIntoOutlet(
outletMap: RouterOutletMap, future: ActivatedRoute, outlet: RouterOutlet): void { outletMap: RouterOutletMap, future: ActivatedRoute, outlet: RouterOutlet): void {
const resolved = ReflectiveInjector.resolve([ const resolved = <any[]>[{provide: ActivatedRoute, useValue: future}, {
{provide: ActivatedRoute, useValue: future}, provide: RouterOutletMap,
{provide: RouterOutletMap, useValue: outletMap} useValue: outletMap
]); }];
outlet.activate(future, resolved, outletMap);
const parentFuture = this.futureState.parent(future); // find the closest parent?
const config = parentFuture ? parentFuture.snapshot._routeConfig : null;
let loadedFactoryResolver: ComponentFactoryResolver = null;
if (config && (<any>config)._loadedConfig) {
const loadedResolver = (<any>config)._loadedConfig.factoryResolver;
loadedFactoryResolver = loadedResolver;
resolved.push({provide: ComponentFactoryResolver, useValue: loadedResolver});
};
outlet.activate(future, loadedFactoryResolver, ReflectiveInjector.resolve(resolved), outletMap);
} }
private deactivateOutletAndItChildren(outlet: RouterOutlet): void { private deactivateOutletAndItChildren(outlet: RouterOutlet): void {

View File

@ -0,0 +1,29 @@
/**
* @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 {AppModuleFactoryLoader, AppModuleRef, ComponentFactoryResolver} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {fromPromise} from 'rxjs/observable/fromPromise';
import {ROUTER_CONFIG} from './common_router_providers';
import {Route} from './config';
export class LoadedRouterConfig {
constructor(public routes: Route[], public factoryResolver: ComponentFactoryResolver) {}
}
export class RouterConfigLoader {
constructor(private loader: AppModuleFactoryLoader) {}
load(path: string): Observable<LoadedRouterConfig> {
return fromPromise(this.loader.load(path).then(r => {
const ref = r.create();
return new LoadedRouterConfig(ref.injector.get(ROUTER_CONFIG), ref.componentFactoryResolver);
}));
}
}

View File

@ -135,22 +135,6 @@ export function equalPath(a: UrlPathWithParams[], b: UrlPathWithParams[]): boole
return true; return true;
} }
export function mapChildren(segment: UrlSegment, fn: (v: UrlSegment, k: string) => UrlSegment):
{[name: string]: UrlSegment} {
const newChildren: {[name: string]: UrlSegment} = {};
forEach(segment.children, (child: UrlSegment, childOutlet: string) => {
if (childOutlet === PRIMARY_OUTLET) {
newChildren[childOutlet] = fn(child, childOutlet);
}
});
forEach(segment.children, (child: UrlSegment, childOutlet: string) => {
if (childOutlet !== PRIMARY_OUTLET) {
newChildren[childOutlet] = fn(child, childOutlet);
}
});
return newChildren;
}
export function mapChildrenIntoArray<T>( export function mapChildrenIntoArray<T>(
segment: UrlSegment, fn: (v: UrlSegment, k: string) => T[]): T[] { segment: UrlSegment, fn: (v: UrlSegment, k: string) => T[]): T[] {
let res: T[] = []; let res: T[] = [];

View File

@ -6,6 +6,14 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import 'rxjs/add/operator/concatAll';
import 'rxjs/add/operator/last';
import {Observable} from 'rxjs/Observable';
import {of } from 'rxjs/observable/of';
import {PRIMARY_OUTLET} from '../shared';
export function shallowEqualArrays(a: any[], b: any[]): boolean { export function shallowEqualArrays(a: any[], b: any[]): boolean {
if (a.length !== b.length) return false; if (a.length !== b.length) return false;
for (let i = 0; i < a.length; ++i) { for (let i = 0; i < a.length; ++i) {
@ -78,3 +86,33 @@ export function forEach<K, V>(
} }
} }
} }
export function waitForMap<A, B>(
obj: {[k: string]: A}, fn: (k: string, a: A) => Observable<B>): Observable<{[k: string]: B}> {
const waitFor: Observable<B>[] = [];
const res: {[k: string]: B} = {};
forEach(obj, (a: A, k: string) => {
if (k === PRIMARY_OUTLET) {
waitFor.push(fn(k, a).map((_: B) => {
res[k] = _;
return _;
}));
}
});
forEach(obj, (a: A, k: string) => {
if (k !== PRIMARY_OUTLET) {
waitFor.push(fn(k, a).map((_: B) => {
res[k] = _;
return _;
}));
}
});
if (waitFor.length > 0) {
return of (...waitFor).concatAll().last().map((last) => res);
} else {
return of (res);
}
}

View File

@ -1,7 +1,10 @@
import {Observable} from 'rxjs/Observable';
import {of } from 'rxjs/observable/of';
import {applyRedirects} from '../src/apply_redirects'; import {applyRedirects} from '../src/apply_redirects';
import {RouterConfig} from '../src/config'; import {RouterConfig} from '../src/config';
import {LoadedRouterConfig} from '../src/router_config_loader';
import {DefaultUrlSerializer, UrlSegment, UrlTree, equalPathsWithParams} from '../src/url_tree'; import {DefaultUrlSerializer, UrlSegment, UrlTree, equalPathsWithParams} from '../src/url_tree';
import {TreeNode} from '../src/utils/tree';
describe('applyRedirects', () => { describe('applyRedirects', () => {
it('should return the same url tree when no redirects', () => { it('should return the same url tree when no redirects', () => {
@ -26,7 +29,7 @@ describe('applyRedirects', () => {
}); });
it('should throw when cannot handle a positional parameter', () => { it('should throw when cannot handle a positional parameter', () => {
applyRedirects(tree('/a/1'), [ applyRedirects(null, tree('/a/1'), [
{path: 'a/:id', redirectTo: 'a/:other'} {path: 'a/:id', redirectTo: 'a/:other'}
]).subscribe(() => {}, (e) => { ]).subscribe(() => {}, (e) => {
expect(e.message).toEqual('Cannot redirect to \'a/:other\'. Cannot find \':other\'.'); expect(e.message).toEqual('Cannot redirect to \'a/:other\'. Cannot find \':other\'.');
@ -128,6 +131,31 @@ describe('applyRedirects', () => {
'/a/b/1', (t: UrlTree) => { compareTrees(t, tree('/absolute/1')); }); '/a/b/1', (t: UrlTree) => { compareTrees(t, tree('/absolute/1')); });
}); });
describe('lazy loading', () => {
it('should load config on demand', () => {
const loadedConfig =
new LoadedRouterConfig([{path: 'b', component: ComponentB}], <any>'stubFactoryResolver');
const loader = {load: (p: any) => of (loadedConfig)};
const config = [{path: 'a', component: ComponentA, mountChildren: 'children'}];
applyRedirects(<any>loader, tree('a/b'), config).forEach(r => {
compareTrees(r, tree('/a/b'));
expect((<any>config[0])._loadedConfig).toBe(loadedConfig);
});
});
it('should handle the case when the loader errors', () => {
const loader = {
load: (p: any) => new Observable<any>((obs: any) => obs.error(new Error('Loading Error')))
};
const config = [{path: 'a', component: ComponentA, mountChildren: 'children'}];
applyRedirects(<any>loader, tree('a/b'), config).subscribe(() => {}, (e) => {
expect(e.message).toEqual('Loading Error');
});
});
});
describe('empty paths', () => { describe('empty paths', () => {
it('redirect from an empty path should work (local redirect)', () => { it('redirect from an empty path should work (local redirect)', () => {
checkRedirect( checkRedirect(
@ -171,7 +199,7 @@ describe('applyRedirects', () => {
{path: '', redirectTo: 'a', pathMatch: 'full'} {path: '', redirectTo: 'a', pathMatch: 'full'}
]; ];
applyRedirects(tree('b'), config) applyRedirects(null, tree('b'), config)
.subscribe( .subscribe(
(_) => { throw 'Should not be reached'; }, (_) => { throw 'Should not be reached'; },
e => { expect(e.message).toEqual('Cannot match any routes: \'b\''); }); e => { expect(e.message).toEqual('Cannot match any routes: \'b\''); });
@ -301,7 +329,7 @@ describe('applyRedirects', () => {
] ]
}]; }];
applyRedirects(tree('a/(d//aux:e)'), config) applyRedirects(null, tree('a/(d//aux:e)'), config)
.subscribe( .subscribe(
(_) => { throw 'Should not be reached'; }, (_) => { throw 'Should not be reached'; },
e => { expect(e.message).toEqual('Cannot match any routes: \'a\''); }); e => { expect(e.message).toEqual('Cannot match any routes: \'a\''); });
@ -311,7 +339,7 @@ describe('applyRedirects', () => {
}); });
function checkRedirect(config: RouterConfig, url: string, callback: any): void { function checkRedirect(config: RouterConfig, url: string, callback: any): void {
applyRedirects(tree(url), config).subscribe(callback, e => { throw e; }); applyRedirects(null, tree(url), config).subscribe(callback, e => { throw e; });
} }
function tree(url: string): UrlTree { function tree(url: string): UrlTree {

View File

@ -15,6 +15,18 @@ describe('config', () => {
`Invalid configuration of route 'a': redirectTo and children cannot be used together`); `Invalid configuration of route 'a': redirectTo and children cannot be used together`);
}); });
it('should throw when redirectTo and mountChildren are used together', () => {
expect(() => { validateConfig([{path: 'a', redirectTo: 'b', mountChildren: 'value'}]); })
.toThrowError(
`Invalid configuration of route 'a': redirectTo and mountChildren cannot be used together`);
});
it('should throw when children and mountChildren are used together', () => {
expect(() => { validateConfig([{path: 'a', children: [], mountChildren: 'value'}]); })
.toThrowError(
`Invalid configuration of route 'a': children and mountChildren cannot be used together`);
});
it('should throw when component and redirectTo are used together', () => { it('should throw when component and redirectTo are used together', () => {
expect(() => { validateConfig([{path: 'a', component: ComponentA, redirectTo: 'b'}]); }) expect(() => { validateConfig([{path: 'a', component: ComponentA, redirectTo: 'b'}]); })
.toThrowError( .toThrowError(
@ -30,7 +42,7 @@ describe('config', () => {
it('should throw when none of component and children or direct are missing', () => { it('should throw when none of component and children or direct are missing', () => {
expect(() => { validateConfig([{path: 'a'}]); }) expect(() => { validateConfig([{path: 'a'}]); })
.toThrowError( .toThrowError(
`Invalid configuration of route 'a': component, redirectTo, children must be provided`); `Invalid configuration of route 'a': component, redirectTo, children, mountChildren must be provided`);
}); });
it('should throw when path starts with a slash', () => { it('should throw when path starts with a slash', () => {

View File

@ -4,14 +4,14 @@ import {Location, LocationStrategy} from '@angular/common';
import {SpyLocation} from '@angular/common/testing'; import {SpyLocation} from '@angular/common/testing';
import {MockLocationStrategy} from '@angular/common/testing/mock_location_strategy'; import {MockLocationStrategy} from '@angular/common/testing/mock_location_strategy';
import {ComponentFixture, TestComponentBuilder} from '@angular/compiler/testing'; import {ComponentFixture, TestComponentBuilder} from '@angular/compiler/testing';
import {Component, Injector} from '@angular/core'; import {AppModule, AppModuleFactory, AppModuleFactoryLoader, Compiler, Component, Injectable, Injector, Type} from '@angular/core';
import {ComponentResolver} from '@angular/core'; import {ComponentResolver} from '@angular/core';
import {beforeEach, beforeEachProviders, ddescribe, describe, fakeAsync, iit, inject, it, tick, xdescribe, xit} from '@angular/core/testing'; import {beforeEach, beforeEachProviders, ddescribe, describe, fakeAsync, iit, inject, it, tick, xdescribe, xit} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/matchers'; import {expect} from '@angular/platform-browser/testing/matchers';
import {Observable} from 'rxjs/Observable'; import {Observable} from 'rxjs/Observable';
import {of } from 'rxjs/observable/of'; import {of } from 'rxjs/observable/of';
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DefaultUrlSerializer, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Params, ROUTER_DIRECTIVES, Resolve, Router, RouterConfig, RouterOutletMap, RouterStateSnapshot, RoutesRecognized, UrlSerializer} from '../index'; import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DefaultUrlSerializer, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Params, ROUTER_DIRECTIVES, Resolve, Router, RouterConfig, RouterOutletMap, RouterStateSnapshot, RoutesRecognized, UrlSerializer, provideRoutes} from '../index';
describe('Integration', () => { describe('Integration', () => {
@ -26,13 +26,18 @@ describe('Integration', () => {
{provide: LocationStrategy, useClass: MockLocationStrategy}, {provide: LocationStrategy, useClass: MockLocationStrategy},
{ {
provide: Router, provide: Router,
useFactory: (resolver: ComponentResolver, urlSerializer: UrlSerializer, useFactory:
outletMap: RouterOutletMap, location: Location, injector: Injector) => { (resolver: ComponentResolver, urlSerializer: UrlSerializer, outletMap: RouterOutletMap,
return new Router( location: Location, loader: AppModuleFactoryLoader, injector: Injector) => {
RootCmp, resolver, urlSerializer, outletMap, location, injector, config); return new Router(
}, RootCmp, resolver, urlSerializer, outletMap, location, injector, loader, config);
deps: [ComponentResolver, UrlSerializer, RouterOutletMap, Location, Injector] },
deps: [
ComponentResolver, UrlSerializer, RouterOutletMap, Location, AppModuleFactoryLoader,
Injector
]
}, },
{provide: AppModuleFactoryLoader, useClass: SpyAppModuleFactoryLoader},
{provide: ActivatedRoute, useFactory: (r: Router) => r.routerState.root, deps: [Router]}, {provide: ActivatedRoute, useFactory: (r: Router) => r.routerState.root, deps: [Router]},
]; ];
}); });
@ -713,6 +718,8 @@ describe('Integration', () => {
describe('should not activate a route when CanActivate returns false', () => { describe('should not activate a route when CanActivate returns false', () => {
beforeEachProviders(() => [{provide: 'alwaysFalse', useValue: (a: any, b: any) => false}]); beforeEachProviders(() => [{provide: 'alwaysFalse', useValue: (a: any, b: any) => false}]);
// handle errors
it('works', it('works',
fakeAsync(inject( fakeAsync(inject(
[Router, TestComponentBuilder, Location], [Router, TestComponentBuilder, Location],
@ -1084,8 +1091,90 @@ describe('Integration', () => {
}))); })));
}); });
describe('lazy loading', () => {
it('works', fakeAsync(inject(
[Router, TestComponentBuilder, Location, AppModuleFactoryLoader],
(router: Router, tcb: TestComponentBuilder, location: Location,
loader: AppModuleFactoryLoader) => {
@Component({
selector: 'lazy',
template: 'lazy-loaded-parent {<router-outlet></router-outlet>}',
directives: ROUTER_DIRECTIVES
})
class ParentLazyLoadedComponent {
}
@Component({selector: 'lazy', template: 'lazy-loaded-child'})
class ChildLazyLoadedComponent {
}
@AppModule({
providers: [provideRoutes([{
path: 'loaded',
component: ParentLazyLoadedComponent,
children: [{path: 'child', component: ChildLazyLoadedComponent}]
}])],
precompile: [ParentLazyLoadedComponent, ChildLazyLoadedComponent]
})
class LoadedModule {
}
(<any>loader).expectedPath = 'expected';
(<any>loader).expected = LoadedModule;
const fixture = createRoot(tcb, router, RootCmp);
router.resetConfig([{path: 'lazy', mountChildren: 'expected'}]);
router.navigateByUrl('/lazy/loaded/child');
advance(fixture);
expect(location.path()).toEqual('/lazy/loaded/child');
expect(fixture.debugElement.nativeElement)
.toHaveText('lazy-loaded-parent {lazy-loaded-child}');
})));
it('error emit an error when cannot load a config',
fakeAsync(inject(
[Router, TestComponentBuilder, Location, AppModuleFactoryLoader],
(router: Router, tcb: TestComponentBuilder, location: Location,
loader: AppModuleFactoryLoader) => {
(<any>loader).expectedPath = 'expected';
const fixture = createRoot(tcb, router, RootCmp);
router.resetConfig([{path: 'lazy', mountChildren: 'invalid'}]);
const recordedEvents: any = [];
router.events.forEach(e => recordedEvents.push(e));
router.navigateByUrl('/lazy/loaded').catch(s => {})
advance(fixture);
expect(location.path()).toEqual('/');
expectEvents(
recordedEvents,
[[NavigationStart, '/lazy/loaded'], [NavigationError, '/lazy/loaded']]);
})));
});
}); });
@Injectable()
class SpyAppModuleFactoryLoader implements AppModuleFactoryLoader {
public expected: any;
public expectedPath: string;
constructor(private compiler: Compiler) {}
load(path: string): Promise<AppModuleFactory<any>> {
if (path === this.expectedPath) {
return this.compiler.compileAppModuleAsync(this.expected);
} else {
return <any>Promise.reject(new Error('boom'));
}
}
}
function expectEvents(events: Event[], pairs: any[]) { function expectEvents(events: Event[], pairs: any[]) {
for (let i = 0; i < events.length; ++i) { for (let i = 0; i < events.length; ++i) {
expect((<any>events[i].constructor).name).toBe(pairs[i][0].name); expect((<any>events[i].constructor).name).toBe(pairs[i][0].name);