From e5b87e55da794c367d475e0aec9a09e005899aa9 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Fri, 29 Apr 2016 18:04:55 -0700 Subject: [PATCH] feat(router): implement relative navigation --- modules/angular2/alt_router.ts | 2 +- .../src/alt_router/directives/router_link.ts | 23 ++- .../alt_router/directives/router_outlet.ts | 7 +- modules/angular2/src/alt_router/link.ts | 64 +++++++- modules/angular2/src/alt_router/recognize.ts | 1 + modules/angular2/src/alt_router/router.ts | 148 +++++++++++++----- .../src/alt_router/router_providers_common.ts | 3 +- modules/angular2/src/alt_router/segments.ts | 6 + .../test/alt_router/integration_spec.ts | 64 +++++++- modules/angular2/test/alt_router/link_spec.ts | 57 +++++-- 10 files changed, 295 insertions(+), 80 deletions(-) diff --git a/modules/angular2/alt_router.ts b/modules/angular2/alt_router.ts index 97442e6f85..0aa7ea79f4 100644 --- a/modules/angular2/alt_router.ts +++ b/modules/angular2/alt_router.ts @@ -12,7 +12,7 @@ export { RouterUrlSerializer, DefaultRouterUrlSerializer } from './src/alt_router/router_url_serializer'; -export {OnActivate} from './src/alt_router/interfaces'; +export {OnActivate, CanDeactivate} from './src/alt_router/interfaces'; export {ROUTER_PROVIDERS} from './src/alt_router/router_providers'; import {RouterOutlet} from './src/alt_router/directives/router_outlet'; diff --git a/modules/angular2/src/alt_router/directives/router_link.ts b/modules/angular2/src/alt_router/directives/router_link.ts index 7a440e4059..83bfd19b67 100644 --- a/modules/angular2/src/alt_router/directives/router_link.ts +++ b/modules/angular2/src/alt_router/directives/router_link.ts @@ -11,28 +11,25 @@ import { HostListener, HostBinding, Input, - OnDestroy + OnDestroy, + Optional } from 'angular2/core'; import {RouterOutletMap, Router} from '../router'; import {RouteSegment, UrlSegment, Tree} from '../segments'; -import {link} from '../link'; -import {isString} from 'angular2/src/facade/lang'; +import {isString, isPresent} from 'angular2/src/facade/lang'; import {ObservableWrapper} from 'angular2/src/facade/async'; @Directive({selector: '[routerLink]'}) export class RouterLink implements OnDestroy { @Input() target: string; private _changes: any[] = []; - private _targetUrl: Tree; private _subscription: any; @HostBinding() private href: string; - constructor(private _router: Router) { - this._subscription = ObservableWrapper.subscribe(_router.changes, (_) => { - this._targetUrl = _router.urlTree; - this._updateTargetUrlAndHref(); - }); + constructor(@Optional() private _routeSegment: RouteSegment, private _router: Router) { + this._subscription = + ObservableWrapper.subscribe(_router.changes, (_) => { this._updateTargetUrlAndHref(); }); } ngOnDestroy() { ObservableWrapper.dispose(this._subscription); } @@ -46,14 +43,16 @@ export class RouterLink implements OnDestroy { @HostListener("click") onClick(): boolean { if (!isString(this.target) || this.target == '_self') { - this._router.navigate(this._targetUrl); + this._router.navigate(this._changes, this._routeSegment); return false; } return true; } private _updateTargetUrlAndHref(): void { - this._targetUrl = link(null, this._router.urlTree, this._changes); - this.href = this._router.serializeUrl(this._targetUrl); + let tree = this._router.createUrlTree(this._changes, this._routeSegment); + if (isPresent(tree)) { + this.href = this._router.serializeUrl(tree); + } } } \ No newline at end of file diff --git a/modules/angular2/src/alt_router/directives/router_outlet.ts b/modules/angular2/src/alt_router/directives/router_outlet.ts index dc6a12dfab..5e7b5dc5a7 100644 --- a/modules/angular2/src/alt_router/directives/router_outlet.ts +++ b/modules/angular2/src/alt_router/directives/router_outlet.ts @@ -28,11 +28,12 @@ export class RouterOutlet { this._loaded = null; } + get loadedComponent(): Object { return isPresent(this._loaded) ? this._loaded.instance : null; } + + get isLoaded(): boolean { return isPresent(this._loaded); } + load(factory: ComponentFactory, providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): ComponentRef { - if (isPresent(this._loaded)) { - this.unload(); - } this.outletMap = outletMap; let inj = ReflectiveInjector.fromResolvedProviders(providers, this._location.parentInjector); this._loaded = this._location.createComponent(factory, this._location.length, inj, []); diff --git a/modules/angular2/src/alt_router/link.ts b/modules/angular2/src/alt_router/link.ts index 5c228964c8..1b151aef42 100644 --- a/modules/angular2/src/alt_router/link.ts +++ b/modules/angular2/src/alt_router/link.ts @@ -1,12 +1,64 @@ import {Tree, TreeNode, UrlSegment, RouteSegment, rootNode} from './segments'; -import {isBlank, isString, isStringMap} from 'angular2/src/facade/lang'; +import {isBlank, isPresent, isString, isStringMap} from 'angular2/src/facade/lang'; import {ListWrapper} from 'angular2/src/facade/collection'; -export function link(segment: RouteSegment, tree: Tree, - change: any[]): Tree { - if (change.length === 0) return tree; - let normalizedChange = (change.length === 1 && change[0] == "/") ? change : ["/"].concat(change); - return new Tree(_update(rootNode(tree), normalizedChange)); +export function link(segment: RouteSegment, routeTree: Tree, + urlTree: Tree, change: any[]): Tree { + if (change.length === 0) return urlTree; + + let startingNode; + let normalizedChange; + + if (isString(change[0]) && change[0].startsWith("./")) { + normalizedChange = ["/", change[0].substring(2)].concat(change.slice(1)); + startingNode = _findStartingNode(_findUrlSegment(segment, routeTree), rootNode(urlTree)); + + } else if (isString(change[0]) && change.length === 1 && change[0] == "/") { + normalizedChange = change; + startingNode = rootNode(urlTree); + + } else if (isString(change[0]) && !change[0].startsWith("/")) { + normalizedChange = ["/"].concat(change); + startingNode = _findStartingNode(_findUrlSegment(segment, routeTree), rootNode(urlTree)); + + } else { + normalizedChange = ["/"].concat(change); + startingNode = rootNode(urlTree); + } + + let updated = _update(startingNode, normalizedChange); + let newRoot = _constructNewTree(rootNode(urlTree), startingNode, updated); + + return new Tree(newRoot); +} + +function _findUrlSegment(segment: RouteSegment, routeTree: Tree): UrlSegment { + let s = segment; + let res = null; + while (isBlank(res)) { + res = ListWrapper.last(s.urlSegments); + s = routeTree.parent(s); + } + return res; +} + +function _findStartingNode(segment: UrlSegment, node: TreeNode): TreeNode { + if (node.value === segment) return node; + for (var c of node.children) { + let r = _findStartingNode(segment, c); + if (isPresent(r)) return r; + } + return null; +} + +function _constructNewTree(node: TreeNode, original: TreeNode, + updated: TreeNode): TreeNode { + if (node === original) { + return new TreeNode(node.value, updated.children); + } else { + return new TreeNode( + node.value, node.children.map(c => _constructNewTree(c, original, updated))); + } } function _update(node: TreeNode, changes: any[]): TreeNode { diff --git a/modules/angular2/src/alt_router/recognize.ts b/modules/angular2/src/alt_router/recognize.ts index 7fb631566c..e5c783076a 100644 --- a/modules/angular2/src/alt_router/recognize.ts +++ b/modules/angular2/src/alt_router/recognize.ts @@ -8,6 +8,7 @@ import {ComponentResolver} from 'angular2/core'; import {DEFAULT_OUTLET_NAME} from './constants'; import {reflector} from 'angular2/src/core/reflection/reflection'; +// TODO: vsavkin: recognize should take the old tree and merge it export function recognize(componentResolver: ComponentResolver, type: Type, url: Tree): Promise> { let matched = new _MatchResult(type, [url.root], null, rootNode(url).children, []); diff --git a/modules/angular2/src/alt_router/router.ts b/modules/angular2/src/alt_router/router.ts index 8c3a20730d..a156ab9e38 100644 --- a/modules/angular2/src/alt_router/router.ts +++ b/modules/angular2/src/alt_router/router.ts @@ -1,12 +1,16 @@ import {OnInit, provide, ReflectiveInjector, ComponentResolver} from 'angular2/core'; import {RouterOutlet} from './directives/router_outlet'; import {Type, isBlank, isPresent} from 'angular2/src/facade/lang'; -import {EventEmitter, Observable} from 'angular2/src/facade/async'; +import {ListWrapper} from 'angular2/src/facade/collection'; +import {EventEmitter, Observable, PromiseWrapper} from 'angular2/src/facade/async'; import {StringMapWrapper} from 'angular2/src/facade/collection'; import {BaseException} from 'angular2/src/facade/exceptions'; import {RouterUrlSerializer} from './router_url_serializer'; +import {CanDeactivate} from './interfaces'; import {recognize} from './recognize'; import {Location} from 'angular2/platform/common'; +import {link} from './link'; + import { equalSegments, routeSegmentComponentFactory, @@ -32,7 +36,8 @@ export class Router { private _changes: EventEmitter = new EventEmitter(); - constructor(private _componentType: Type, private _componentResolver: ComponentResolver, + constructor(private _rootComponent: Object, private _rootComponentType: Type, + private _componentResolver: ComponentResolver, private _urlSerializer: RouterUrlSerializer, private _routerOutletMap: RouterOutletMap, private _location: Location) { this.navigateByUrl(this._location.path()); @@ -40,62 +45,89 @@ export class Router { get urlTree(): Tree { return this._urlTree; } - navigate(url: Tree): Promise { + navigateByUrl(url: string): Promise { + return this._navigate(this._urlSerializer.parse(url)); + } + + navigate(changes: any[], segment?: RouteSegment): Promise { + return this._navigate(this.createUrlTree(changes, segment)); + } + + private _navigate(url: Tree): Promise { this._urlTree = url; - return recognize(this._componentResolver, this._componentType, url) + return recognize(this._componentResolver, this._rootComponentType, url) .then(currTree => { - new _LoadSegments(currTree, this._prevTree).load(this._routerOutletMap); - this._prevTree = currTree; - this._location.go(this._urlSerializer.serialize(this._urlTree)); - this._changes.emit(null); + return new _LoadSegments(currTree, this._prevTree) + .load(this._routerOutletMap, this._rootComponent) + .then(_ => { + this._prevTree = currTree; + this._location.go(this._urlSerializer.serialize(this._urlTree)); + this._changes.emit(null); + }); }); } + createUrlTree(changes: any[], segment?: RouteSegment): Tree { + if (isPresent(this._prevTree)) { + let s = isPresent(segment) ? segment : this._prevTree.root; + return link(s, this._prevTree, this.urlTree, changes); + } else { + return null; + } + } + serializeUrl(url: Tree): string { return this._urlSerializer.serialize(url); } - navigateByUrl(url: string): Promise { - return this.navigate(this._urlSerializer.parse(url)); - } - get changes(): Observable { return this._changes; } + + get routeTree(): Tree { return this._prevTree; } } + class _LoadSegments { + private deactivations: Object[][] = []; + private performMutation: boolean = true; + constructor(private currTree: Tree, private prevTree: Tree) {} - load(parentOutletMap: RouterOutletMap): void { + load(parentOutletMap: RouterOutletMap, rootComponent: Object): Promise { let prevRoot = isPresent(this.prevTree) ? rootNode(this.prevTree) : null; let currRoot = rootNode(this.currTree); - this.loadChildSegments(currRoot, prevRoot, parentOutletMap); + + return this.canDeactivate(currRoot, prevRoot, parentOutletMap, rootComponent) + .then(res => { + this.performMutation = true; + if (res) { + this.loadChildSegments(currRoot, prevRoot, parentOutletMap, [rootComponent]); + } + }); } - 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); + private canDeactivate(currRoot: TreeNode, prevRoot: TreeNode, + outletMap: RouterOutletMap, rootComponent: Object): Promise { + this.performMutation = false; + this.loadChildSegments(currRoot, prevRoot, outletMap, [rootComponent]); - 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); - } + let allPaths = PromiseWrapper.all(this.deactivations.map(r => this.checkCanDeactivatePath(r))); + return allPaths.then((values: boolean[]) => values.filter(v => v).length === values.length); } - 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, this.currTree, this.prevTree); + private checkCanDeactivatePath(path: Object[]): Promise { + let curr = PromiseWrapper.resolve(true); + for (let p of ListWrapper.reversed(path)) { + curr = curr.then(_ => { + if (hasLifecycleHook("routerCanDeactivate", p)) { + return (p).routerCanDeactivate(this.prevTree, this.currTree); + } else { + return _; + } + }); } + return curr; } private loadChildSegments(currNode: TreeNode, prevNode: TreeNode, - outletMap: RouterOutletMap): void { + outletMap: RouterOutletMap, components: Object[]): void { let prevChildren = isPresent(prevNode) ? prevNode.children.reduce( (m, c) => { @@ -106,11 +138,42 @@ class _LoadSegments { {}; currNode.children.forEach(c => { - this.loadSegments(c, prevChildren[c.value.outlet], outletMap); + this.loadSegments(c, prevChildren[c.value.outlet], outletMap, components); StringMapWrapper.delete(prevChildren, c.value.outlet); }); - StringMapWrapper.forEach(prevChildren, (v, k) => this.unloadOutlet(outletMap._outlets[k])); + StringMapWrapper.forEach(prevChildren, + (v, k) => this.unloadOutlet(outletMap._outlets[k], components)); + } + + loadSegments(currNode: TreeNode, prevNode: TreeNode, + parentOutletMap: RouterOutletMap, components: Object[]): 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, + components.concat([outlet.loadedComponent])); + } else { + this.unloadOutlet(outlet, components); + if (this.performMutation) { + let outletMap = new RouterOutletMap(); + let loadedComponent = this.loadNewSegment(outletMap, curr, prev, outlet); + this.loadChildSegments(currNode, prevNode, outletMap, components.concat([loadedComponent])); + } + } + } + + private loadNewSegment(outletMap: RouterOutletMap, curr: RouteSegment, prev: RouteSegment, + outlet: RouterOutlet): Object { + 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, this.currTree, this.prevTree); + } + return ref.instance; } private getOutlet(outletMap: RouterOutletMap, segment: RouteSegment): RouterOutlet { @@ -125,8 +188,15 @@ class _LoadSegments { return outlet; } - private unloadOutlet(outlet: RouterOutlet): void { - StringMapWrapper.forEach(outlet.outletMap._outlets, (v, k) => { this.unloadOutlet(v); }); - outlet.unload(); + private unloadOutlet(outlet: RouterOutlet, components: Object[]): void { + if (outlet.isLoaded) { + StringMapWrapper.forEach(outlet.outletMap._outlets, + (v, k) => this.unloadOutlet(v, components)); + if (this.performMutation) { + outlet.unload(); + } else { + this.deactivations.push(components.concat([outlet.loadedComponent])); + } + } } } \ No newline at end of file diff --git a/modules/angular2/src/alt_router/router_providers_common.ts b/modules/angular2/src/alt_router/router_providers_common.ts index f75edfa6cd..c5c3153d8a 100644 --- a/modules/angular2/src/alt_router/router_providers_common.ts +++ b/modules/angular2/src/alt_router/router_providers_common.ts @@ -23,6 +23,7 @@ function routerFactory(app: ApplicationRef, componentResolver: ComponentResolver if (app.componentTypes.length == 0) { throw new BaseException("Bootstrap at least one component before injecting Router."); } - return new Router(app.componentTypes[0], componentResolver, urlSerializer, routerOutletMap, + // TODO: vsavkin this should not be null + return new Router(null, app.componentTypes[0], componentResolver, urlSerializer, routerOutletMap, location); } \ No newline at end of file diff --git a/modules/angular2/src/alt_router/segments.ts b/modules/angular2/src/alt_router/segments.ts index 1564fc7f4c..53d3cefa74 100644 --- a/modules/angular2/src/alt_router/segments.ts +++ b/modules/angular2/src/alt_router/segments.ts @@ -33,6 +33,8 @@ export function rootNode(tree: Tree): TreeNode { } function _findNode(expected: T, c: TreeNode): TreeNode { + // TODO: vsavkin remove it once recognize is fixed + if (expected instanceof RouteSegment && equalSegments(expected, c.value)) return c; if (expected === c.value) return c; for (let cc of c.children) { let r = _findNode(expected, cc); @@ -44,6 +46,9 @@ function _findNode(expected: T, c: TreeNode): TreeNode { function _findPath(expected: T, c: TreeNode, collected: TreeNode[]): TreeNode[] { collected.push(c); + // TODO: vsavkin remove it once recognize is fixed + if (expected instanceof RouteSegment && equalSegments(expected, c.value)) + return collected; if (expected === c.value) return collected; for (let cc of c.children) { let r = _findPath(expected, cc, ListWrapper.clone(collected)); @@ -114,6 +119,7 @@ export function equalSegments(a: RouteSegment, b: RouteSegment): boolean { if (a._type !== b._type) return false; if (isBlank(a.parameters) && !isBlank(b.parameters)) return false; if (!isBlank(a.parameters) && isBlank(b.parameters)) return false; + if (isBlank(a.parameters) && isBlank(b.parameters)) return true; return StringMapWrapper.equals(a.parameters, b.parameters); } diff --git a/modules/angular2/test/alt_router/integration_spec.ts b/modules/angular2/test/alt_router/integration_spec.ts index 558baf557f..d97554161d 100644 --- a/modules/angular2/test/alt_router/integration_spec.ts +++ b/modules/angular2/test/alt_router/integration_spec.ts @@ -17,6 +17,7 @@ import { tick } from 'angular2/testing_internal'; import {provide, Component, ComponentResolver} from 'angular2/core'; +import {PromiseWrapper} from 'angular2/src/facade/async'; import { @@ -28,7 +29,8 @@ import { Routes, RouterUrlSerializer, DefaultRouterUrlSerializer, - OnActivate + OnActivate, + CanDeactivate } from 'angular2/alt_router'; import {SpyLocation} from 'angular2/src/mock/location_mock'; import {Location} from 'angular2/platform/common'; @@ -42,8 +44,8 @@ export function main() { provide(Location, {useClass: SpyLocation}), provide(Router, { - useFactory: (resolver, urlParser, outletMap, location) => - new Router(RootCmp, resolver, urlParser, outletMap, location), + useFactory: (resolver, urlParser, outletMap, location) => new Router( + "RootComponent", RootCmp, resolver, urlParser, outletMap, location), deps: [ComponentResolver, RouterUrlSerializer, RouterOutletMap, Location] }) ]); @@ -124,9 +126,22 @@ export function main() { expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello fedor, aux: }'); }))); - if (DOM.supportsDOMEvents()) { // this is required to use fakeAsync + it('should not unload the route if can deactivate returns false', + fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + let fixture = tcb.createFakeAsync(RootCmp); - it("should support router links", + router.navigateByUrl('/team/22/cannotDeactivate'); + advance(fixture); + + router.navigateByUrl('/team/22/user/fedor'); + advance(fixture); + + expect(fixture.debugElement.nativeElement) + .toHaveText('team 22 { cannotDeactivate, aux: }'); + }))); + + if (DOM.supportsDOMEvents()) { + it("should support absolute router links", fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { let fixture = tcb.createFakeAsync(RootCmp); advance(fixture); @@ -143,6 +158,25 @@ export function main() { expect(fixture.debugElement.nativeElement).toHaveText('team 33 { simple, aux: }'); }))); + it("should support relative router links", + fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + let fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + + router.navigateByUrl('/team/22/relativelink'); + advance(fixture); + expect(fixture.debugElement.nativeElement) + .toHaveText('team 22 { relativelink { }, aux: }'); + + let native = DOM.querySelector(fixture.debugElement.nativeElement, "a"); + expect(DOM.getAttribute(native, "href")).toEqual("/team/22/relativelink/simple"); + DOM.dispatchEvent(native, DOM.createMouseEvent('click')); + advance(fixture); + + expect(fixture.debugElement.nativeElement) + .toHaveText('team 22 { relativelink { simple }, aux: }'); + }))); + it("should update router links when router changes", fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { let fixture = tcb.createFakeAsync(RootCmp); @@ -179,6 +213,11 @@ class UserCmp implements OnActivate { routerOnActivate(s: RouteSegment, a?, b?, c?) { this.user = s.getParam('name'); } } +@Component({selector: 'cannot-deactivate', template: `cannotDeactivate`}) +class CanDeactivateCmp implements CanDeactivate { + routerCanDeactivate(a?, b?): Promise { return PromiseWrapper.resolve(false); } +} + @Component({selector: 'simple-cmp', template: `simple`}) class SimpleCmp { } @@ -189,12 +228,21 @@ class Simple2Cmp { @Component({ selector: 'link-cmp', - template: `link`, + template: `link`, directives: ROUTER_DIRECTIVES }) class LinkCmp { } +@Component({ + selector: 'link-cmp', + template: `relativelink { }`, + directives: ROUTER_DIRECTIVES +}) +@Routes([new Route({path: 'simple', component: SimpleCmp})]) +class RelativeLinkCmp { +} + @Component({ selector: 'team-cmp', template: `team {{id}} { , aux: }`, @@ -204,7 +252,9 @@ class LinkCmp { new Route({path: 'user/:name', component: UserCmp}), new Route({path: 'simple', component: SimpleCmp}), new Route({path: 'simple2', component: Simple2Cmp}), - new Route({path: 'link', component: LinkCmp}) + new Route({path: 'link', component: LinkCmp}), + new Route({path: 'relativelink', component: RelativeLinkCmp}), + new Route({path: 'cannotDeactivate', component: CanDeactivateCmp}) ]) class TeamCmp implements OnActivate { id: string; diff --git a/modules/angular2/test/alt_router/link_spec.ts b/modules/angular2/test/alt_router/link_spec.ts index 3426cbbdeb..fb82a52561 100644 --- a/modules/angular2/test/alt_router/link_spec.ts +++ b/modules/angular2/test/alt_router/link_spec.ts @@ -15,7 +15,7 @@ import { xit } from 'angular2/testing_internal'; -import {RouteSegment, UrlSegment, Tree} from 'angular2/src/alt_router/segments'; +import {RouteSegment, UrlSegment, Tree, TreeNode} from 'angular2/src/alt_router/segments'; import {link} from 'angular2/src/alt_router/link'; import {DefaultRouterUrlSerializer} from 'angular2/src/alt_router/router_url_serializer'; @@ -25,43 +25,78 @@ export function main() { it("should return the original tree when given an empty array", () => { let p = parser.parse("/"); - let t = link(s(p.root), p, []); + let tree = s(p.root); + let t = link(tree.root, tree, p, []); expect(t).toBe(p); }); it("should support going to root", () => { let p = parser.parse("/"); - let t = link(s(p.root), p, ["/"]); + let tree = s(p.root); + let t = link(tree.root, tree, p, ["/"]); expect(parser.serialize(t)).toEqual(""); }); it("should support positional params", () => { - let p = parser.parse("/"); - let t = link(s(p.root), p, ["/one", 11, "two", 22]); + let p = parser.parse("/a/b"); + let tree = s(p.firstChild(p.root)); + let t = link(tree.root, tree, p, ["/one", 11, "two", 22]); expect(parser.serialize(t)).toEqual("/one/11/two/22"); }); it("should preserve route siblings when changing the main route", () => { let p = parser.parse("/a/11/b(c)"); - let t = link(s(p.root), p, ["/a", 11, 'd']); + let tree = s(p.root); + let t = link(tree.root, tree, p, ["/a", 11, 'd']); expect(parser.serialize(t)).toEqual("/a/11/d(aux:c)"); }); it("should preserve route siblings when changing a aux route", () => { let p = parser.parse("/a/11/b(c)"); - let t = link(s(p.root), p, ["/a", 11, 'aux:d']); + let tree = s(p.root); + let t = link(tree.root, tree, p, ["/a", 11, 'aux:d']); expect(parser.serialize(t)).toEqual("/a/11/b(aux:d)"); }); - it('should update parameters', () => { let p = parser.parse("/a;aa=11"); - let t = link(s(p.root), p, ["/a", {aa: 22, bb: 33}]); + let tree = s(p.root); + let t = link(tree.root, tree, p, ["/a", {aa: 22, bb: 33}]); expect(parser.serialize(t)).toEqual("/a;aa=22;bb=33"); }); + + it("should update relative subtree (when starts with ./)", () => { + let p = parser.parse("/a(ap)/c(cp)"); + let c = p.firstChild(p.root); + let tree = s(c); + let t = link(tree.root, tree, p, ["./c2"]); + expect(parser.serialize(t)).toEqual("/a(aux:ap)/c2(aux:cp)"); + }); + + it("should update relative subtree (when does not start with ./)", () => { + let p = parser.parse("/a(ap)/c(cp)"); + let c = p.firstChild(p.root); + let tree = s(c); + let t = link(tree.root, tree, p, ["c2"]); + expect(parser.serialize(t)).toEqual("/a(aux:ap)/c2(aux:cp)"); + }); + + it("should update relative subtree when the provided segment doesn't have url segments", () => { + let p = parser.parse("/a(ap)/c(cp)"); + let c = p.firstChild(p.root); + + let child = new RouteSegment([], null, null, null, null); + let root = new TreeNode(new RouteSegment([c], {}, null, null, null), + [new TreeNode(child, [])]); + let tree = new Tree(root); + + let t = link(child, tree, p, ["./c2"]); + expect(parser.serialize(t)).toEqual("/a(aux:ap)/c2(aux:cp)"); + }); }); } -function s(u: UrlSegment): RouteSegment { - return new RouteSegment([u], {}, null, null, null); +function s(u: UrlSegment): Tree { + let root = new TreeNode(new RouteSegment([u], {}, null, null, null), []); + return new Tree(root); } \ No newline at end of file