feat(router): add support for lazily loaded modules
This commit is contained in:
parent
6fcf962fb5
commit
8ebb8e44c8
|
@ -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';
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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};
|
||||||
|
}
|
|
@ -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`);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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') &&
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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[] = [];
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue