diff --git a/modules/angular2/alt_router.ts b/modules/angular2/alt_router.ts index f31900532b..b65383f665 100644 --- a/modules/angular2/alt_router.ts +++ b/modules/angular2/alt_router.ts @@ -8,10 +8,14 @@ export {Router, RouterOutletMap} from './src/alt_router/router'; export {RouteSegment} from './src/alt_router/segments'; export {Routes} from './src/alt_router/metadata/decorators'; export {Route} from './src/alt_router/metadata/metadata'; -export {RouterUrlParser, DefaultRouterUrlParser} from './src/alt_router/router_url_parser'; +export { + RouterUrlSerializer, + DefaultRouterUrlSerializer +} from './src/alt_router/router_url_serializer'; export {OnActivate} from './src/alt_router/interfaces'; import {RouterOutlet} from './src/alt_router/directives/router_outlet'; +import {RouterLink} from './src/alt_router/directives/router_link'; import {CONST_EXPR} from './src/facade/lang'; -export const ROUTER_DIRECTIVES: any[] = CONST_EXPR([RouterOutlet]); +export const ROUTER_DIRECTIVES: any[] = CONST_EXPR([RouterOutlet, RouterLink]); diff --git a/modules/angular2/src/alt_router/directives/router_link.ts b/modules/angular2/src/alt_router/directives/router_link.ts new file mode 100644 index 0000000000..09735da6d6 --- /dev/null +++ b/modules/angular2/src/alt_router/directives/router_link.ts @@ -0,0 +1,59 @@ +import { + ResolvedReflectiveProvider, + Directive, + DynamicComponentLoader, + ViewContainerRef, + Attribute, + ComponentRef, + ComponentFactory, + ReflectiveInjector, + OnInit, + HostListener, + HostBinding, + Input, + OnDestroy +} 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 {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, private _segment: RouteSegment) { + this._subscription = ObservableWrapper.subscribe(_router.changes, (_) => { + this._targetUrl = _router.urlTree; + this._updateTargetUrlAndHref(); + }); + } + + ngOnDestroy() { ObservableWrapper.dispose(this._subscription); } + + @Input() + set routerLink(data: any[]) { + this._changes = data; + this._updateTargetUrlAndHref(); + } + + @HostListener("click") + onClick(): boolean { + if (!isString(this.target) || this.target == '_self') { + this._router.navigate(this._targetUrl); + return false; + } + return true; + } + + private _updateTargetUrlAndHref(): void { + this._targetUrl = link(this._segment, this._router.urlTree, this._changes); + this.href = this._router.serializeUrl(this._targetUrl); + } +} \ No newline at end of file diff --git a/modules/angular2/src/alt_router/router.ts b/modules/angular2/src/alt_router/router.ts index fcf02f8794..a5e217a99e 100644 --- a/modules/angular2/src/alt_router/router.ts +++ b/modules/angular2/src/alt_router/router.ts @@ -1,9 +1,10 @@ import {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 {StringMapWrapper} from 'angular2/src/facade/collection'; import {BaseException} from 'angular2/src/facade/exceptions'; -import {RouterUrlParser} from './router_url_parser'; +import {RouterUrlSerializer} from './router_url_serializer'; import {recognize} from './recognize'; import { equalSegments, @@ -11,7 +12,9 @@ import { RouteSegment, Tree, rootNode, - TreeNode + TreeNode, + UrlSegment, + serializeRouteSegmentTree } from './segments'; import {hasLifecycleHook} from './lifecycle_reflector'; import {DEFAULT_OUTLET_NAME} from './constants'; @@ -23,23 +26,39 @@ export class RouterOutletMap { } export class Router { - private prevTree: Tree; - constructor(private _componentType: Type, private _componentResolver: ComponentResolver, - private _urlParser: RouterUrlParser, private _routerOutletMap: RouterOutletMap) {} + private _prevTree: Tree; + private _urlTree: Tree; - navigateByUrl(url: string): Promise { - let urlSegmentTree = this._urlParser.parse(url); - return recognize(this._componentResolver, this._componentType, urlSegmentTree) + private _changes: EventEmitter = new EventEmitter(); + + constructor(private _componentType: Type, private _componentResolver: ComponentResolver, + private _urlSerializer: RouterUrlSerializer, + private _routerOutletMap: RouterOutletMap) {} + + get urlTree(): Tree { return this._urlTree; } + + navigate(url: Tree): Promise { + this._urlTree = url; + return recognize(this._componentResolver, this._componentType, url) .then(currTree => { - let prevRoot = isPresent(this.prevTree) ? rootNode(this.prevTree) : null; - new _SegmentLoader(currTree, this.prevTree) + let prevRoot = isPresent(this._prevTree) ? rootNode(this._prevTree) : null; + new _LoadSegments(currTree, this._prevTree) .loadSegments(rootNode(currTree), prevRoot, this._routerOutletMap); - this.prevTree = currTree; + this._prevTree = currTree; + this._changes.emit(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; } } -class _SegmentLoader { +class _LoadSegments { constructor(private currTree: Tree, private prevTree: Tree) {} loadSegments(currNode: TreeNode, prevNode: TreeNode, diff --git a/modules/angular2/test/alt_router/integration_spec.ts b/modules/angular2/test/alt_router/integration_spec.ts index 27704ef30c..bad66dfd4f 100644 --- a/modules/angular2/test/alt_router/integration_spec.ts +++ b/modules/angular2/test/alt_router/integration_spec.ts @@ -12,10 +12,13 @@ import { inject, beforeEachProviders, it, - xit + xit, + fakeAsync, + tick } from 'angular2/testing_internal'; import {provide, Component, ComponentResolver} from 'angular2/core'; + import { Router, RouterOutletMap, @@ -23,105 +26,129 @@ import { Route, ROUTER_DIRECTIVES, Routes, - RouterUrlParser, - DefaultRouterUrlParser, + RouterUrlSerializer, + DefaultRouterUrlSerializer, OnActivate } from 'angular2/alt_router'; +import {DOM} from 'angular2/src/platform/dom/dom_adapter'; export function main() { describe('navigation', () => { beforeEachProviders(() => [ - provide(RouterUrlParser, {useClass: DefaultRouterUrlParser}), + provide(RouterUrlSerializer, {useClass: DefaultRouterUrlSerializer}), RouterOutletMap, provide(Router, { useFactory: (resolver, urlParser, outletMap) => new Router(RootCmp, resolver, urlParser, outletMap), - deps: [ComponentResolver, RouterUrlParser, RouterOutletMap] + deps: [ComponentResolver, RouterUrlSerializer, RouterOutletMap] }) ]); it('should support nested routes', - inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => { - let fixture; - compileRoot(tcb) - .then((rtc) => {fixture = rtc}) - .then((_) => router.navigateByUrl('/team/22/user/victor')) - .then((_) => { - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement) - .toHaveText('team 22 { hello victor, aux: }'); - async.done(); - }); - })); + fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + let fixture = tcb.createFakeAsync(RootCmp); + + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello victor, aux: }'); + }))); 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(); - }); - })); + fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + let fixture = tcb.createFakeAsync(RootCmp); - 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(); - }); - })); + router.navigateByUrl('/team/22/user/victor(/simple)'); + advance(fixture); + + expect(fixture.debugElement.nativeElement) + .toHaveText('team 22 { hello victor, aux: simple }'); + }))); + + it('should unload outlets', fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + let fixture = tcb.createFakeAsync(RootCmp); + + router.navigateByUrl('/team/22/user/victor(/simple)'); + advance(fixture); + + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello victor, aux: }'); + }))); 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(); - }); - })); + fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + let fixture = tcb.createFakeAsync(RootCmp); + + router.navigateByUrl('/team/22/user/victor(/simple)'); + advance(fixture); + + router.navigateByUrl('/'); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText(''); + }))); it('should update nested routes when url changes', - inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => { - let fixture; - let team1; - let team2; - compileRoot(tcb) - .then((rtc) => {fixture = rtc}) - .then((_) => router.navigateByUrl('/team/22/user/victor')) - .then((_) => { team1 = fixture.debugElement.children[1].componentInstance; }) - .then((_) => router.navigateByUrl('/team/22/user/fedor')) - .then((_) => { team2 = fixture.debugElement.children[1].componentInstance; }) - .then((_) => { - fixture.detectChanges(); - expect(team1).toBe(team2); - expect(fixture.debugElement.nativeElement) - .toHaveText('team 22 { hello fedor, aux: }'); - async.done(); - }); - })); + fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + let fixture = tcb.createFakeAsync(RootCmp); - // unload unused nodes + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + let team1 = fixture.debugElement.children[1].componentInstance; + + router.navigateByUrl('/team/22/user/fedor'); + advance(fixture); + let team2 = fixture.debugElement.children[1].componentInstance; + + expect(team1).toBe(team2); + expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello fedor, aux: }'); + }))); + + it("should support router links", + fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + let fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + + router.navigateByUrl('/team/22/link'); + advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, aux: }'); + + let native = DOM.querySelector(fixture.debugElement.nativeElement, "a"); + expect(DOM.getAttribute(native, "href")).toEqual("/team/33/simple"); + DOM.dispatchEvent(native, DOM.createMouseEvent('click')); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('team 33 { simple, aux: }'); + }))); + + it("should update router links when router changes", + fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + let fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + + router.navigateByUrl('/team/22/link(simple)'); + advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, aux: simple }'); + + let native = DOM.querySelector(fixture.debugElement.nativeElement, "a"); + expect(DOM.getAttribute(native, "href")).toEqual("/team/33/simple(aux:simple)"); + + router.navigateByUrl('/team/22/link(simple2)'); + advance(fixture); + + expect(DOM.getAttribute(native, "href")).toEqual("/team/33/simple(aux:simple2)"); + }))); }); } +function advance(fixture: ComponentFixture): void { + tick(); + fixture.detectChanges(); +} + function compileRoot(tcb: TestComponentBuilder): Promise { return tcb.createAsync(RootCmp); } @@ -136,6 +163,18 @@ class UserCmp implements OnActivate { class SimpleCmp { } +@Component({selector: 'simple2-cmp', template: `simple2`}) +class Simple2Cmp { +} + +@Component({ + selector: 'link-cmp', + template: `link`, + directives: ROUTER_DIRECTIVES +}) +class LinkCmp { +} + @Component({ selector: 'team-cmp', template: `team {{id}} { , aux: }`, @@ -143,7 +182,9 @@ class SimpleCmp { }) @Routes([ new Route({path: 'user/:name', component: UserCmp}), - new Route({path: 'simple', component: SimpleCmp}) + new Route({path: 'simple', component: SimpleCmp}), + new Route({path: 'simple2', component: Simple2Cmp}), + new Route({path: 'link', component: LinkCmp}) ]) class TeamCmp implements OnActivate { id: string;