From 6e1fed42b745eb55b9ed2248ad6cf2c267d93c63 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 25 Apr 2016 16:57:27 -0700 Subject: [PATCH] feat(router): add Router and RouterOutlet to support aux routes --- .../alt_router/directives/router_outlet.ts | 21 +++-- modules/angular2/src/alt_router/router.ts | 88 +++++++++++++++---- .../test/alt_router/integration_spec.ts | 62 ++++++++++++- 3 files changed, 141 insertions(+), 30 deletions(-) diff --git a/modules/angular2/src/alt_router/directives/router_outlet.ts b/modules/angular2/src/alt_router/directives/router_outlet.ts index 7dcb149a0b..dc6a12dfab 100644 --- a/modules/angular2/src/alt_router/directives/router_outlet.ts +++ b/modules/angular2/src/alt_router/directives/router_outlet.ts @@ -3,28 +3,35 @@ import { Directive, DynamicComponentLoader, ViewContainerRef, - Input, + Attribute, ComponentRef, ComponentFactory, - ReflectiveInjector + ReflectiveInjector, + OnInit } from 'angular2/core'; 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'}) export class RouterOutlet { private _loaded: ComponentRef; public outletMap: RouterOutletMap; - @Input() name: string = ""; - constructor(parentOutletMap: RouterOutletMap, private _location: ViewContainerRef) { - parentOutletMap.registerOutlet("", this); + constructor(parentOutletMap: RouterOutletMap, private _location: ViewContainerRef, + @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[], outletMap: RouterOutletMap): ComponentRef { if (isPresent(this._loaded)) { - this._loaded.destroy(); + this.unload(); } this.outletMap = outletMap; let inj = ReflectiveInjector.fromResolvedProviders(providers, this._location.parentInjector); diff --git a/modules/angular2/src/alt_router/router.ts b/modules/angular2/src/alt_router/router.ts index bd9bf184ac..fcf02f8794 100644 --- a/modules/angular2/src/alt_router/router.ts +++ b/modules/angular2/src/alt_router/router.ts @@ -1,10 +1,20 @@ import {provide, ReflectiveInjector, ComponentResolver} from 'angular2/core'; import {RouterOutlet} from './directives/router_outlet'; 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 {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 {DEFAULT_OUTLET_NAME} from './constants'; export class RouterOutletMap { /** @internal */ @@ -18,38 +28,78 @@ export class Router { private _urlParser: RouterUrlParser, private _routerOutletMap: RouterOutletMap) {} navigateByUrl(url: string): Promise { - let urlSegmentTree = this._urlParser.parse(url.substring(1)); + let urlSegmentTree = this._urlParser.parse(url); return recognize(this._componentResolver, this._componentType, urlSegmentTree) .then(currTree => { - let prevRoot = isPresent(this.prevTree) ? this.prevTree.root : null; - _loadSegments(currTree, currTree.root, this.prevTree, prevRoot, this, - this._routerOutletMap); + let prevRoot = isPresent(this.prevTree) ? rootNode(this.prevTree) : null; + new _SegmentLoader(currTree, this.prevTree) + .loadSegments(rootNode(currTree), prevRoot, this._routerOutletMap); this.prevTree = currTree; }); } } -function _loadSegments(currTree: Tree, curr: RouteSegment, - prevTree: Tree, prev: RouteSegment, router: Router, - parentOutletMap: RouterOutletMap): void { - let outlet = parentOutletMap._outlets[curr.outlet]; +class _SegmentLoader { + constructor(private currTree: Tree, private prevTree: Tree) {} - let outletMap; - if (equalSegments(curr, prev)) { - outletMap = outlet.outletMap; - } else { - outletMap = new RouterOutletMap(); + loadSegments(currNode: TreeNode, prevNode: TreeNode, + parentOutletMap: RouterOutletMap): void { + let curr = currNode.value; + let prev = isPresent(prevNode) ? prevNode.value : null; + 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( [provide(RouterOutletMap, {useValue: outletMap}), provide(RouteSegment, {useValue: curr})]); let ref = outlet.load(routeSegmentComponentFactory(curr), resolved, outletMap); 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))) { - let cc = currTree.firstChild(curr); - let pc = isBlank(prevTree) ? null : prevTree.firstChild(prev); - _loadSegments(currTree, cc, prevTree, pc, router, outletMap); + private loadChildSegments(currNode: TreeNode, prevNode: TreeNode, + outletMap: RouterOutletMap): void { + let prevChildren = isPresent(prevNode) ? + 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(); } } \ No newline at end of file diff --git a/modules/angular2/test/alt_router/integration_spec.ts b/modules/angular2/test/alt_router/integration_spec.ts index 41ae480905..27704ef30c 100644 --- a/modules/angular2/test/alt_router/integration_spec.ts +++ b/modules/angular2/test/alt_router/integration_spec.ts @@ -49,7 +49,51 @@ export function main() { .then((_) => router.navigateByUrl('/team/22/user/victor')) .then((_) => { 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(); }); })); @@ -68,10 +112,13 @@ export function main() { .then((_) => { fixture.detectChanges(); 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(); }); })); + + // unload unused nodes }); } @@ -85,12 +132,19 @@ class UserCmp implements OnActivate { routerOnActivate(s: RouteSegment, a?, b?, c?) { this.user = s.getParam('name'); } } +@Component({selector: 'simple-cmp', template: `simple`}) +class SimpleCmp { +} + @Component({ selector: 'team-cmp', - template: `team {{id}} { }`, + template: `team {{id}} { , aux: }`, 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 { id: string; routerOnActivate(s: RouteSegment, a?, b?, c?) { this.id = s.getParam('id'); }