feat(router): add support for lazily loaded modules

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

View File

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

View File

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

View File

@ -4,14 +4,14 @@ import {Location, LocationStrategy} from '@angular/common';
import {SpyLocation} from '@angular/common/testing';
import {MockLocationStrategy} from '@angular/common/testing/mock_location_strategy';
import {ComponentFixture, TestComponentBuilder} from '@angular/compiler/testing';
import {Component, Injector} from '@angular/core';
import {AppModule, AppModuleFactory, AppModuleFactoryLoader, Compiler, Component, Injectable, Injector, Type} from '@angular/core';
import {ComponentResolver} from '@angular/core';
import {beforeEach, beforeEachProviders, ddescribe, describe, fakeAsync, iit, inject, it, tick, xdescribe, xit} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/matchers';
import {Observable} from 'rxjs/Observable';
import {of } from 'rxjs/observable/of';
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DefaultUrlSerializer, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Params, ROUTER_DIRECTIVES, Resolve, Router, RouterConfig, RouterOutletMap, RouterStateSnapshot, RoutesRecognized, UrlSerializer} from '../index';
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DefaultUrlSerializer, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Params, ROUTER_DIRECTIVES, Resolve, Router, RouterConfig, RouterOutletMap, RouterStateSnapshot, RoutesRecognized, UrlSerializer, provideRoutes} from '../index';
describe('Integration', () => {
@ -26,13 +26,18 @@ describe('Integration', () => {
{provide: LocationStrategy, useClass: MockLocationStrategy},
{
provide: Router,
useFactory: (resolver: ComponentResolver, urlSerializer: UrlSerializer,
outletMap: RouterOutletMap, location: Location, injector: Injector) => {
return new Router(
RootCmp, resolver, urlSerializer, outletMap, location, injector, config);
},
deps: [ComponentResolver, UrlSerializer, RouterOutletMap, Location, Injector]
useFactory:
(resolver: ComponentResolver, urlSerializer: UrlSerializer, outletMap: RouterOutletMap,
location: Location, loader: AppModuleFactoryLoader, injector: Injector) => {
return new Router(
RootCmp, resolver, urlSerializer, outletMap, location, injector, loader, config);
},
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);