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 {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 {RouterLink, RouterLinkWithHref} from './src/directives/router_link';
export {RouterLinkActive} from './src/directives/router_link_active';

View File

@ -6,14 +6,20 @@
* 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 {Observer} from 'rxjs/Observer';
import {of } from 'rxjs/observable/of';
import {EmptyError} from 'rxjs/util/EmptyError';
import {Route, RouterConfig} from './config';
import {RouterConfigLoader} from './router_config_loader';
import {PRIMARY_OUTLET} from './shared';
import {UrlPathWithParams, UrlSegment, UrlTree, mapChildren} from './url_tree';
import {merge} from './utils/collection';
import {UrlPathWithParams, UrlSegment, UrlTree} from './url_tree';
import {merge, waitForMap} from './utils/collection';
class NoMatch {
constructor(public segment: UrlSegment = null) {}
@ -22,138 +28,192 @@ class AbsoluteRedirect {
constructor(public paths: UrlPathWithParams[]) {}
}
export function applyRedirects(urlTree: UrlTree, config: RouterConfig): Observable<UrlTree> {
try {
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 noMatch(segment: UrlSegment): Observable<UrlSegment> {
return new Observable<UrlSegment>((obs: Observer<UrlSegment>) => obs.error(new NoMatch(segment)));
}
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 ?
new UrlSegment([], {[PRIMARY_OUTLET]: 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()) {
return new UrlSegment([], expandSegmentChildren(routes, segment));
return expandSegmentChildren(configLoader, routes, segment)
.map(children => new UrlSegment([], children));
} 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} {
return mapChildren(segment, (child, childOutlet) => expandSegment(routes, child, childOutlet));
function expandSegmentChildren(
configLoader: RouterConfigLoader, routes: Route[],
segment: UrlSegment): Observable<{[name: string]: UrlSegment}> {
return waitForMap(
segment.children,
(childOutlet, child) => expandSegment(configLoader, routes, child, childOutlet));
}
function expandPathsWithParams(
segment: UrlSegment, routes: Route[], paths: UrlPathWithParams[], outlet: string,
allowRedirects: boolean): UrlSegment {
for (let r of routes) {
try {
return expandPathsWithParamsAgainstRoute(segment, routes, r, paths, outlet, allowRedirects);
} catch (e) {
if (!(e instanceof NoMatch)) throw e;
}
}
configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[],
paths: UrlPathWithParams[], outlet: string, allowRedirects: boolean): Observable<UrlSegment> {
const processRoutes =
of (...routes)
.map(r => {
return expandPathsWithParamsAgainstRoute(
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;
}
});
}
function expandPathsWithParamsAgainstRoute(
segment: UrlSegment, routes: Route[], route: Route, paths: UrlPathWithParams[], outlet: string,
allowRedirects: boolean): UrlSegment {
if (getOutlet(route) !== outlet) throw new NoMatch();
if (route.redirectTo !== undefined && !allowRedirects) throw new NoMatch();
configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[], route: Route,
paths: UrlPathWithParams[], outlet: string, allowRedirects: boolean): Observable<UrlSegment> {
if (getOutlet(route) !== outlet) return noMatch(segment);
if (route.redirectTo !== undefined && !allowRedirects) return noMatch(segment);
if (route.redirectTo !== undefined) {
return expandPathsWithParamsAgainstRouteUsingRedirect(segment, routes, route, paths, outlet);
return expandPathsWithParamsAgainstRouteUsingRedirect(
configLoader, segment, routes, route, paths, outlet);
} else {
return matchPathsWithParamsAgainstRoute(segment, route, paths);
return matchPathsWithParamsAgainstRoute(configLoader, segment, route, paths);
}
}
function expandPathsWithParamsAgainstRouteUsingRedirect(
segment: UrlSegment, routes: Route[], route: Route, paths: UrlPathWithParams[],
outlet: string): UrlSegment {
configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[], route: Route,
paths: UrlPathWithParams[], outlet: string): Observable<UrlSegment> {
if (route.path === '**') {
return expandWildCardWithParamsAgainstRouteUsingRedirect(route);
} else {
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, {});
if (route.redirectTo.startsWith('/')) {
throw new AbsoluteRedirect(newPaths);
return absoluteRedirect(newPaths);
} else {
return new UrlSegment(newPaths, {});
return of (new UrlSegment(newPaths, {}));
}
}
function expandRegularPathWithParamsAgainstRouteUsingRedirect(
segment: UrlSegment, routes: Route[], route: Route, paths: UrlPathWithParams[],
outlet: string): UrlSegment {
const {consumedPaths, lastChild, positionalParamSegments} = match(segment, route, paths);
configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[], route: Route,
paths: UrlPathWithParams[], outlet: string): Observable<UrlSegment> {
const {matched, consumedPaths, lastChild, positionalParamSegments} = match(segment, route, paths);
if (!matched) return noMatch(segment);
const newPaths =
applyRedirectCommands(consumedPaths, route.redirectTo, <any>positionalParamSegments);
if (route.redirectTo.startsWith('/')) {
throw new AbsoluteRedirect(newPaths);
return absoluteRedirect(newPaths);
} else {
return expandPathsWithParams(
segment, routes, newPaths.concat(paths.slice(lastChild)), outlet, false);
configLoader, segment, routes, newPaths.concat(paths.slice(lastChild)), outlet, false);
}
}
function matchPathsWithParamsAgainstRoute(
rawSegment: UrlSegment, route: Route, paths: UrlPathWithParams[]): UrlSegment {
configLoader: RouterConfigLoader, rawSegment: UrlSegment, route: Route,
paths: UrlPathWithParams[]): Observable<UrlSegment> {
if (route.path === '**') {
return new UrlSegment(paths, {});
return of (new UrlSegment(paths, {}));
} else {
const {consumedPaths, lastChild} = match(rawSegment, route, paths);
const childConfig = route.children ? route.children : [];
const {matched, consumedPaths, lastChild} = match(rawSegment, route, paths);
if (!matched) return noMatch(rawSegment);
const rawSlicedPath = paths.slice(lastChild);
return getChildConfig(configLoader, route).mergeMap(childConfig => {
const {segment, slicedPath} = split(rawSegment, consumedPaths, rawSlicedPath, childConfig);
if (slicedPath.length === 0 && segment.hasChildren()) {
const children = expandSegmentChildren(childConfig, segment);
return new UrlSegment(consumedPaths, children);
return expandSegmentChildren(configLoader, childConfig, segment)
.map(children => new UrlSegment(consumedPaths, children));
} else if (childConfig.length === 0 && slicedPath.length === 0) {
return new UrlSegment(consumedPaths, {});
return of (new UrlSegment(consumedPaths, {}));
} else {
const cs = expandPathsWithParams(segment, childConfig, slicedPath, PRIMARY_OUTLET, true);
return new UrlSegment(consumedPaths.concat(cs.pathsWithParams), cs.children);
return expandPathsWithParams(
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[]): {
matched: boolean,
consumedPaths: UrlPathWithParams[],
lastChild: number,
positionalParamSegments: {[k: string]: UrlPathWithParams}
} {
const noMatch =
{matched: false, consumedPaths: <any[]>[], lastChild: 0, positionalParamSegments: {}};
if (route.path === '') {
if ((route.terminal || route.pathMatch === 'full') &&
(segment.hasChildren() || paths.length > 0)) {
throw new NoMatch();
return {matched: false, consumedPaths: [], lastChild: 0, positionalParamSegments: {}};
} 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;
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 p = parts[i];
const isPosParam = p.startsWith(':');
if (!isPosParam && p !== current.path) throw new NoMatch();
if (!isPosParam && p !== current.path) return noMatch;
if (isPosParam) {
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)) {
throw new NoMatch();
return {matched: false, consumedPaths: [], lastChild: 0, positionalParamSegments: {}};
}
return {consumedPaths, lastChild: currentIndex, positionalParamSegments};
return {matched: true, consumedPaths, lastChild: currentIndex, positionalParamSegments};
}
function applyRedirectCommands(

View File

@ -7,7 +7,7 @@
*/
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 {Router} from './router';
@ -25,14 +25,14 @@ export interface ExtraOptions { enableTracing?: boolean; }
export function setupRouter(
ref: ApplicationRef, resolver: ComponentResolver, urlSerializer: UrlSerializer,
outletMap: RouterOutletMap, location: Location, injector: Injector, config: RouterConfig,
opts: ExtraOptions) {
outletMap: RouterOutletMap, location: Location, injector: Injector,
loader: AppModuleFactoryLoader, config: RouterConfig, opts: ExtraOptions) {
if (ref.componentTypes.length == 0) {
throw new Error('Bootstrap at least one component before injecting Router.');
}
const componentType = ref.componentTypes[0];
const r =
new Router(componentType, resolver, urlSerializer, outletMap, location, injector, config);
const r = new Router(
componentType, resolver, urlSerializer, outletMap, location, injector, loader, config);
ref.registerDisposeListener(() => r.dispose());
if (opts.enableTracing) {
@ -93,7 +93,7 @@ export function provideRouter(_config: RouterConfig, _opts: ExtraOptions): any[]
useFactory: setupRouter,
deps: [
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]},
// 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;
resolve?: ResolveData;
children?: Route[];
mountChildren?: string;
}
export function validateConfig(config: RouterConfig): void {
@ -278,13 +279,22 @@ function validateNode(route: Route): void {
throw new Error(
`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) {
throw new Error(
`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(
`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) {
throw new Error(`Invalid route configuration: routes must have path specified`);

View File

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

View File

@ -122,7 +122,7 @@ function processPathsWithParamsAgainstRoute(
const {consumedPaths, parameters, lastChild} = match(rawSegment, route, paths);
const rawSlicedPath = paths.slice(lastChild);
const childConfig = route.children ? route.children : [];
const childConfig = getChildConfig(route);
const newInherited = route.component ?
InheritedFromParent.empty :
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[]) {
if (route.path === '') {
if ((route.terminal || route.pathMatch === 'full') &&

View File

@ -15,24 +15,25 @@ import 'rxjs/add/observable/from';
import 'rxjs/add/observable/forkJoin';
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 {Subject} from 'rxjs/Subject';
import {Subscription} from 'rxjs/Subscription';
import {of } from 'rxjs/observable/of';
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 {createUrlTree} from './create_url_tree';
import {RouterOutlet} from './directives/router_outlet';
import {recognize} from './recognize';
import {resolve} from './resolve';
import {RouterConfigLoader} from './router_config_loader';
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 {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';
export interface NavigationExtras {
@ -124,6 +125,7 @@ export class Router {
private navigationId: number = 0;
private config: RouterConfig;
private futureUrlTree: UrlTree;
private configLoader: RouterConfigLoader;
/**
* Creates the router service.
@ -131,11 +133,13 @@ export class Router {
constructor(
private rootComponentType: Type, private resolver: ComponentResolver,
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.routerEvents = new Subject<Event>();
this.currentUrlTree = createEmptyUrlTree();
this.futureUrlTree = this.currentUrlTree;
this.configLoader = new RouterConfigLoader(loader);
this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType);
}
@ -310,7 +314,7 @@ export class Router {
let state: RouterState;
let navigationIsSuccessful: boolean;
let preActivation: PreActivation;
applyRedirects(url, this.config)
applyRedirects(this.configLoader, url, this.config)
.mergeMap(u => {
this.futureUrlTree = u;
return recognize(
@ -555,20 +559,11 @@ class PreActivation {
}
private resolveNode(resolve: ResolveData, future: ActivatedRouteSnapshot): Observable<any> {
const resolvingObs: Observable<any>[] = [];
const resolvedData: {[k: string]: any} = {};
forEach(resolve, (v: any, k: string) => {
return waitForMap(resolve, (k, 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));
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(
outletMap: RouterOutletMap, future: ActivatedRoute, outlet: RouterOutlet): void {
const resolved = ReflectiveInjector.resolve([
{provide: ActivatedRoute, useValue: future},
{provide: RouterOutletMap, useValue: outletMap}
]);
outlet.activate(future, resolved, outletMap);
const resolved = <any[]>[{provide: ActivatedRoute, useValue: future}, {
provide: RouterOutletMap,
useValue: 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 {

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;
}
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>(
segment: UrlSegment, fn: (v: UrlSegment, k: string) => T[]): T[] {
let res: T[] = [];

View File

@ -6,6 +6,14 @@
* 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 {
if (a.length !== b.length) return false;
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 {RouterConfig} from '../src/config';
import {LoadedRouterConfig} from '../src/router_config_loader';
import {DefaultUrlSerializer, UrlSegment, UrlTree, equalPathsWithParams} from '../src/url_tree';
import {TreeNode} from '../src/utils/tree';
describe('applyRedirects', () => {
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', () => {
applyRedirects(tree('/a/1'), [
applyRedirects(null, tree('/a/1'), [
{path: 'a/:id', redirectTo: 'a/:other'}
]).subscribe(() => {}, (e) => {
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')); });
});
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', () => {
it('redirect from an empty path should work (local redirect)', () => {
checkRedirect(
@ -171,7 +199,7 @@ describe('applyRedirects', () => {
{path: '', redirectTo: 'a', pathMatch: 'full'}
];
applyRedirects(tree('b'), config)
applyRedirects(null, tree('b'), config)
.subscribe(
(_) => { throw 'Should not be reached'; },
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(
(_) => { throw 'Should not be reached'; },
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 {
applyRedirects(tree(url), config).subscribe(callback, e => { throw e; });
applyRedirects(null, tree(url), config).subscribe(callback, e => { throw e; });
}
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`);
});
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', () => {
expect(() => { validateConfig([{path: 'a', component: ComponentA, redirectTo: 'b'}]); })
.toThrowError(
@ -30,7 +42,7 @@ describe('config', () => {
it('should throw when none of component and children or direct are missing', () => {
expect(() => { validateConfig([{path: 'a'}]); })
.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', () => {

View File

@ -4,14 +4,14 @@ import {Location, LocationStrategy} from '@angular/common';
import {SpyLocation} from '@angular/common/testing';
import {MockLocationStrategy} from '@angular/common/testing/mock_location_strategy';
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 {beforeEach, beforeEachProviders, ddescribe, describe, fakeAsync, iit, inject, it, tick, xdescribe, xit} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/matchers';
import {Observable} from 'rxjs/Observable';
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', () => {
@ -26,13 +26,18 @@ describe('Integration', () => {
{provide: LocationStrategy, useClass: MockLocationStrategy},
{
provide: Router,
useFactory: (resolver: ComponentResolver, urlSerializer: UrlSerializer,
outletMap: RouterOutletMap, location: Location, injector: Injector) => {
useFactory:
(resolver: ComponentResolver, urlSerializer: UrlSerializer, outletMap: RouterOutletMap,
location: Location, loader: AppModuleFactoryLoader, injector: Injector) => {
return new Router(
RootCmp, resolver, urlSerializer, outletMap, location, injector, config);
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]},
];
});
@ -713,6 +718,8 @@ describe('Integration', () => {
describe('should not activate a route when CanActivate returns false', () => {
beforeEachProviders(() => [{provide: 'alwaysFalse', useValue: (a: any, b: any) => false}]);
// handle errors
it('works',
fakeAsync(inject(
[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[]) {
for (let i = 0; i < events.length; ++i) {
expect((<any>events[i].constructor).name).toBe(pairs[i][0].name);