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 {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';
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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};
|
||||
}
|
|
@ -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`);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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') &&
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
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[] = [];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue