feat(router): add Router and RouterOutlet to support aux routes
This commit is contained in:
parent
d35c109cb9
commit
6e1fed42b7
|
@ -3,28 +3,35 @@ import {
|
||||||
Directive,
|
Directive,
|
||||||
DynamicComponentLoader,
|
DynamicComponentLoader,
|
||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
Input,
|
Attribute,
|
||||||
ComponentRef,
|
ComponentRef,
|
||||||
ComponentFactory,
|
ComponentFactory,
|
||||||
ReflectiveInjector
|
ReflectiveInjector,
|
||||||
|
OnInit
|
||||||
} from 'angular2/core';
|
} from 'angular2/core';
|
||||||
import {RouterOutletMap} from '../router';
|
import {RouterOutletMap} from '../router';
|
||||||
import {isPresent} from 'angular2/src/facade/lang';
|
import {DEFAULT_OUTLET_NAME} from '../constants';
|
||||||
|
import {isPresent, isBlank} from 'angular2/src/facade/lang';
|
||||||
|
|
||||||
@Directive({selector: 'router-outlet'})
|
@Directive({selector: 'router-outlet'})
|
||||||
export class RouterOutlet {
|
export class RouterOutlet {
|
||||||
private _loaded: ComponentRef;
|
private _loaded: ComponentRef;
|
||||||
public outletMap: RouterOutletMap;
|
public outletMap: RouterOutletMap;
|
||||||
@Input() name: string = "";
|
|
||||||
|
|
||||||
constructor(parentOutletMap: RouterOutletMap, private _location: ViewContainerRef) {
|
constructor(parentOutletMap: RouterOutletMap, private _location: ViewContainerRef,
|
||||||
parentOutletMap.registerOutlet("", this);
|
@Attribute('name') name: string) {
|
||||||
|
parentOutletMap.registerOutlet(isBlank(name) ? DEFAULT_OUTLET_NAME : name, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
unload(): void {
|
||||||
|
this._loaded.destroy();
|
||||||
|
this._loaded = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
load(factory: ComponentFactory, providers: ResolvedReflectiveProvider[],
|
load(factory: ComponentFactory, providers: ResolvedReflectiveProvider[],
|
||||||
outletMap: RouterOutletMap): ComponentRef {
|
outletMap: RouterOutletMap): ComponentRef {
|
||||||
if (isPresent(this._loaded)) {
|
if (isPresent(this._loaded)) {
|
||||||
this._loaded.destroy();
|
this.unload();
|
||||||
}
|
}
|
||||||
this.outletMap = outletMap;
|
this.outletMap = outletMap;
|
||||||
let inj = ReflectiveInjector.fromResolvedProviders(providers, this._location.parentInjector);
|
let inj = ReflectiveInjector.fromResolvedProviders(providers, this._location.parentInjector);
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
import {provide, ReflectiveInjector, ComponentResolver} from 'angular2/core';
|
import {provide, ReflectiveInjector, ComponentResolver} from 'angular2/core';
|
||||||
import {RouterOutlet} from './directives/router_outlet';
|
import {RouterOutlet} from './directives/router_outlet';
|
||||||
import {Type, isBlank, isPresent} from 'angular2/src/facade/lang';
|
import {Type, isBlank, isPresent} from 'angular2/src/facade/lang';
|
||||||
|
import {StringMapWrapper} from 'angular2/src/facade/collection';
|
||||||
|
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||||
import {RouterUrlParser} from './router_url_parser';
|
import {RouterUrlParser} from './router_url_parser';
|
||||||
import {recognize} from './recognize';
|
import {recognize} from './recognize';
|
||||||
import {equalSegments, routeSegmentComponentFactory, RouteSegment, Tree} from './segments';
|
import {
|
||||||
|
equalSegments,
|
||||||
|
routeSegmentComponentFactory,
|
||||||
|
RouteSegment,
|
||||||
|
Tree,
|
||||||
|
rootNode,
|
||||||
|
TreeNode
|
||||||
|
} from './segments';
|
||||||
import {hasLifecycleHook} from './lifecycle_reflector';
|
import {hasLifecycleHook} from './lifecycle_reflector';
|
||||||
|
import {DEFAULT_OUTLET_NAME} from './constants';
|
||||||
|
|
||||||
export class RouterOutletMap {
|
export class RouterOutletMap {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
@ -18,38 +28,78 @@ export class Router {
|
||||||
private _urlParser: RouterUrlParser, private _routerOutletMap: RouterOutletMap) {}
|
private _urlParser: RouterUrlParser, private _routerOutletMap: RouterOutletMap) {}
|
||||||
|
|
||||||
navigateByUrl(url: string): Promise<void> {
|
navigateByUrl(url: string): Promise<void> {
|
||||||
let urlSegmentTree = this._urlParser.parse(url.substring(1));
|
let urlSegmentTree = this._urlParser.parse(url);
|
||||||
return recognize(this._componentResolver, this._componentType, urlSegmentTree)
|
return recognize(this._componentResolver, this._componentType, urlSegmentTree)
|
||||||
.then(currTree => {
|
.then(currTree => {
|
||||||
let prevRoot = isPresent(this.prevTree) ? this.prevTree.root : null;
|
let prevRoot = isPresent(this.prevTree) ? rootNode(this.prevTree) : null;
|
||||||
_loadSegments(currTree, currTree.root, this.prevTree, prevRoot, this,
|
new _SegmentLoader(currTree, this.prevTree)
|
||||||
this._routerOutletMap);
|
.loadSegments(rootNode(currTree), prevRoot, this._routerOutletMap);
|
||||||
this.prevTree = currTree;
|
this.prevTree = currTree;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _loadSegments(currTree: Tree<RouteSegment>, curr: RouteSegment,
|
class _SegmentLoader {
|
||||||
prevTree: Tree<RouteSegment>, prev: RouteSegment, router: Router,
|
constructor(private currTree: Tree<RouteSegment>, private prevTree: Tree<RouteSegment>) {}
|
||||||
parentOutletMap: RouterOutletMap): void {
|
|
||||||
let outlet = parentOutletMap._outlets[curr.outlet];
|
|
||||||
|
|
||||||
let outletMap;
|
loadSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,
|
||||||
if (equalSegments(curr, prev)) {
|
parentOutletMap: RouterOutletMap): void {
|
||||||
outletMap = outlet.outletMap;
|
let curr = currNode.value;
|
||||||
} else {
|
let prev = isPresent(prevNode) ? prevNode.value : null;
|
||||||
outletMap = new RouterOutletMap();
|
let outlet = this.getOutlet(parentOutletMap, currNode.value);
|
||||||
|
|
||||||
|
if (equalSegments(curr, prev)) {
|
||||||
|
this.loadChildSegments(currNode, prevNode, outlet.outletMap);
|
||||||
|
} else {
|
||||||
|
let outletMap = new RouterOutletMap();
|
||||||
|
this.loadNewSegment(outletMap, curr, prev, outlet);
|
||||||
|
this.loadChildSegments(currNode, prevNode, outletMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadNewSegment(outletMap: RouterOutletMap, curr: RouteSegment, prev: RouteSegment,
|
||||||
|
outlet: RouterOutlet): void {
|
||||||
let resolved = ReflectiveInjector.resolve(
|
let resolved = ReflectiveInjector.resolve(
|
||||||
[provide(RouterOutletMap, {useValue: outletMap}), provide(RouteSegment, {useValue: curr})]);
|
[provide(RouterOutletMap, {useValue: outletMap}), provide(RouteSegment, {useValue: curr})]);
|
||||||
let ref = outlet.load(routeSegmentComponentFactory(curr), resolved, outletMap);
|
let ref = outlet.load(routeSegmentComponentFactory(curr), resolved, outletMap);
|
||||||
if (hasLifecycleHook("routerOnActivate", ref.instance)) {
|
if (hasLifecycleHook("routerOnActivate", ref.instance)) {
|
||||||
ref.instance.routerOnActivate(curr, prev, currTree, prevTree);
|
ref.instance.routerOnActivate(curr, prev, this.currTree, this.prevTree);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPresent(currTree.firstChild(curr))) {
|
private loadChildSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,
|
||||||
let cc = currTree.firstChild(curr);
|
outletMap: RouterOutletMap): void {
|
||||||
let pc = isBlank(prevTree) ? null : prevTree.firstChild(prev);
|
let prevChildren = isPresent(prevNode) ?
|
||||||
_loadSegments(currTree, cc, prevTree, pc, router, outletMap);
|
prevNode.children.reduce(
|
||||||
|
(m, c) => {
|
||||||
|
m[c.value.outlet] = c;
|
||||||
|
return m;
|
||||||
|
},
|
||||||
|
{}) :
|
||||||
|
{};
|
||||||
|
|
||||||
|
currNode.children.forEach(c => {
|
||||||
|
this.loadSegments(c, prevChildren[c.value.outlet], outletMap);
|
||||||
|
StringMapWrapper.delete(prevChildren, c.value.outlet);
|
||||||
|
});
|
||||||
|
|
||||||
|
StringMapWrapper.forEach(prevChildren, (v, k) => this.unloadOutlet(outletMap._outlets[k]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOutlet(outletMap: RouterOutletMap, segment: RouteSegment): RouterOutlet {
|
||||||
|
let outlet = outletMap._outlets[segment.outlet];
|
||||||
|
if (isBlank(outlet)) {
|
||||||
|
if (segment.outlet == DEFAULT_OUTLET_NAME) {
|
||||||
|
throw new BaseException(`Cannot find default outlet`);
|
||||||
|
} else {
|
||||||
|
throw new BaseException(`Cannot find the outlet ${segment.outlet}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outlet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unloadOutlet(outlet: RouterOutlet): void {
|
||||||
|
StringMapWrapper.forEach(outlet.outletMap._outlets, (v, k) => { this.unloadOutlet(v); });
|
||||||
|
outlet.unload();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -49,7 +49,51 @@ export function main() {
|
||||||
.then((_) => router.navigateByUrl('/team/22/user/victor'))
|
.then((_) => router.navigateByUrl('/team/22/user/victor'))
|
||||||
.then((_) => {
|
.then((_) => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello victor }');
|
expect(fixture.debugElement.nativeElement)
|
||||||
|
.toHaveText('team 22 { hello victor, aux: }');
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should support aux routes',
|
||||||
|
inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => {
|
||||||
|
let fixture;
|
||||||
|
compileRoot(tcb)
|
||||||
|
.then((rtc) => {fixture = rtc})
|
||||||
|
.then((_) => router.navigateByUrl('/team/22/user/victor(/simple)'))
|
||||||
|
.then((_) => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.nativeElement)
|
||||||
|
.toHaveText('team 22 { hello victor, aux: simple }');
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should unload outlets',
|
||||||
|
inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => {
|
||||||
|
let fixture;
|
||||||
|
compileRoot(tcb)
|
||||||
|
.then((rtc) => {fixture = rtc})
|
||||||
|
.then((_) => router.navigateByUrl('/team/22/user/victor(/simple)'))
|
||||||
|
.then((_) => router.navigateByUrl('/team/22/user/victor'))
|
||||||
|
.then((_) => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.nativeElement)
|
||||||
|
.toHaveText('team 22 { hello victor, aux: }');
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should unload nested outlets',
|
||||||
|
inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => {
|
||||||
|
let fixture;
|
||||||
|
compileRoot(tcb)
|
||||||
|
.then((rtc) => {fixture = rtc})
|
||||||
|
.then((_) => router.navigateByUrl('/team/22/user/victor(/simple)'))
|
||||||
|
.then((_) => router.navigateByUrl('/'))
|
||||||
|
.then((_) => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.nativeElement).toHaveText('');
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
@ -68,10 +112,13 @@ export function main() {
|
||||||
.then((_) => {
|
.then((_) => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(team1).toBe(team2);
|
expect(team1).toBe(team2);
|
||||||
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello fedor }');
|
expect(fixture.debugElement.nativeElement)
|
||||||
|
.toHaveText('team 22 { hello fedor, aux: }');
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// unload unused nodes
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,12 +132,19 @@ class UserCmp implements OnActivate {
|
||||||
routerOnActivate(s: RouteSegment, a?, b?, c?) { this.user = s.getParam('name'); }
|
routerOnActivate(s: RouteSegment, a?, b?, c?) { this.user = s.getParam('name'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'simple-cmp', template: `simple`})
|
||||||
|
class SimpleCmp {
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'team-cmp',
|
selector: 'team-cmp',
|
||||||
template: `team {{id}} { <router-outlet></router-outlet> }`,
|
template: `team {{id}} { <router-outlet></router-outlet>, aux: <router-outlet name="aux"></router-outlet> }`,
|
||||||
directives: [ROUTER_DIRECTIVES]
|
directives: [ROUTER_DIRECTIVES]
|
||||||
})
|
})
|
||||||
@Routes([new Route({path: 'user/:name', component: UserCmp})])
|
@Routes([
|
||||||
|
new Route({path: 'user/:name', component: UserCmp}),
|
||||||
|
new Route({path: 'simple', component: SimpleCmp})
|
||||||
|
])
|
||||||
class TeamCmp implements OnActivate {
|
class TeamCmp implements OnActivate {
|
||||||
id: string;
|
id: string;
|
||||||
routerOnActivate(s: RouteSegment, a?, b?, c?) { this.id = s.getParam('id'); }
|
routerOnActivate(s: RouteSegment, a?, b?, c?) { this.id = s.getParam('id'); }
|
||||||
|
|
Loading…
Reference in New Issue