feat(router): add RouterLink

This commit is contained in:
vsavkin 2016-04-27 15:37:20 -07:00 committed by Victor Savkin
parent fa5bfe4b64
commit de56dd5f30
4 changed files with 212 additions and 89 deletions

View File

@ -8,10 +8,14 @@ export {Router, RouterOutletMap} from './src/alt_router/router';
export {RouteSegment} from './src/alt_router/segments'; export {RouteSegment} from './src/alt_router/segments';
export {Routes} from './src/alt_router/metadata/decorators'; export {Routes} from './src/alt_router/metadata/decorators';
export {Route} from './src/alt_router/metadata/metadata'; 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'; export {OnActivate} from './src/alt_router/interfaces';
import {RouterOutlet} from './src/alt_router/directives/router_outlet'; 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'; import {CONST_EXPR} from './src/facade/lang';
export const ROUTER_DIRECTIVES: any[] = CONST_EXPR([RouterOutlet]); export const ROUTER_DIRECTIVES: any[] = CONST_EXPR([RouterOutlet, RouterLink]);

View File

@ -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<UrlSegment>;
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);
}
}

View File

@ -1,9 +1,10 @@
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 {EventEmitter, Observable} from 'angular2/src/facade/async';
import {StringMapWrapper} from 'angular2/src/facade/collection'; import {StringMapWrapper} from 'angular2/src/facade/collection';
import {BaseException} from 'angular2/src/facade/exceptions'; import {BaseException} from 'angular2/src/facade/exceptions';
import {RouterUrlParser} from './router_url_parser'; import {RouterUrlSerializer} from './router_url_serializer';
import {recognize} from './recognize'; import {recognize} from './recognize';
import { import {
equalSegments, equalSegments,
@ -11,7 +12,9 @@ import {
RouteSegment, RouteSegment,
Tree, Tree,
rootNode, rootNode,
TreeNode TreeNode,
UrlSegment,
serializeRouteSegmentTree
} from './segments'; } from './segments';
import {hasLifecycleHook} from './lifecycle_reflector'; import {hasLifecycleHook} from './lifecycle_reflector';
import {DEFAULT_OUTLET_NAME} from './constants'; import {DEFAULT_OUTLET_NAME} from './constants';
@ -23,23 +26,39 @@ export class RouterOutletMap {
} }
export class Router { export class Router {
private prevTree: Tree<RouteSegment>; private _prevTree: Tree<RouteSegment>;
constructor(private _componentType: Type, private _componentResolver: ComponentResolver, private _urlTree: Tree<UrlSegment>;
private _urlParser: RouterUrlParser, private _routerOutletMap: RouterOutletMap) {}
navigateByUrl(url: string): Promise<void> { private _changes: EventEmitter<void> = new EventEmitter<void>();
let urlSegmentTree = this._urlParser.parse(url);
return recognize(this._componentResolver, this._componentType, urlSegmentTree) constructor(private _componentType: Type, private _componentResolver: ComponentResolver,
private _urlSerializer: RouterUrlSerializer,
private _routerOutletMap: RouterOutletMap) {}
get urlTree(): Tree<UrlSegment> { return this._urlTree; }
navigate(url: Tree<UrlSegment>): Promise<void> {
this._urlTree = url;
return recognize(this._componentResolver, this._componentType, url)
.then(currTree => { .then(currTree => {
let prevRoot = isPresent(this.prevTree) ? rootNode(this.prevTree) : null; let prevRoot = isPresent(this._prevTree) ? rootNode(this._prevTree) : null;
new _SegmentLoader(currTree, this.prevTree) new _LoadSegments(currTree, this._prevTree)
.loadSegments(rootNode(currTree), prevRoot, this._routerOutletMap); .loadSegments(rootNode(currTree), prevRoot, this._routerOutletMap);
this.prevTree = currTree; this._prevTree = currTree;
this._changes.emit(null);
}); });
} }
serializeUrl(url: Tree<UrlSegment>): string { return this._urlSerializer.serialize(url); }
navigateByUrl(url: string): Promise<void> {
return this.navigate(this._urlSerializer.parse(url));
}
get changes(): Observable<void> { return this._changes; }
} }
class _SegmentLoader { class _LoadSegments {
constructor(private currTree: Tree<RouteSegment>, private prevTree: Tree<RouteSegment>) {} constructor(private currTree: Tree<RouteSegment>, private prevTree: Tree<RouteSegment>) {}
loadSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>, loadSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,

View File

@ -12,10 +12,13 @@ import {
inject, inject,
beforeEachProviders, beforeEachProviders,
it, it,
xit xit,
fakeAsync,
tick
} from 'angular2/testing_internal'; } from 'angular2/testing_internal';
import {provide, Component, ComponentResolver} from 'angular2/core'; import {provide, Component, ComponentResolver} from 'angular2/core';
import { import {
Router, Router,
RouterOutletMap, RouterOutletMap,
@ -23,105 +26,129 @@ import {
Route, Route,
ROUTER_DIRECTIVES, ROUTER_DIRECTIVES,
Routes, Routes,
RouterUrlParser, RouterUrlSerializer,
DefaultRouterUrlParser, DefaultRouterUrlSerializer,
OnActivate OnActivate
} from 'angular2/alt_router'; } from 'angular2/alt_router';
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
export function main() { export function main() {
describe('navigation', () => { describe('navigation', () => {
beforeEachProviders(() => [ beforeEachProviders(() => [
provide(RouterUrlParser, {useClass: DefaultRouterUrlParser}), provide(RouterUrlSerializer, {useClass: DefaultRouterUrlSerializer}),
RouterOutletMap, RouterOutletMap,
provide(Router, provide(Router,
{ {
useFactory: (resolver, urlParser, outletMap) => useFactory: (resolver, urlParser, outletMap) =>
new Router(RootCmp, resolver, urlParser, outletMap), new Router(RootCmp, resolver, urlParser, outletMap),
deps: [ComponentResolver, RouterUrlParser, RouterOutletMap] deps: [ComponentResolver, RouterUrlSerializer, RouterOutletMap]
}) })
]); ]);
it('should support nested routes', it('should support nested routes',
inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => { fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
let fixture; let fixture = tcb.createFakeAsync(RootCmp);
compileRoot(tcb)
.then((rtc) => {fixture = rtc}) router.navigateByUrl('/team/22/user/victor');
.then((_) => router.navigateByUrl('/team/22/user/victor')) advance(fixture);
.then((_) => {
fixture.detectChanges(); expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello victor, aux: }');
expect(fixture.debugElement.nativeElement) })));
.toHaveText('team 22 { hello victor, aux: }');
async.done();
});
}));
it('should support aux routes', it('should support aux routes',
inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => { fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
let fixture; let fixture = tcb.createFakeAsync(RootCmp);
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', router.navigateByUrl('/team/22/user/victor(/simple)');
inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => { advance(fixture);
let fixture;
compileRoot(tcb) expect(fixture.debugElement.nativeElement)
.then((rtc) => {fixture = rtc}) .toHaveText('team 22 { hello victor, aux: simple }');
.then((_) => router.navigateByUrl('/team/22/user/victor(/simple)')) })));
.then((_) => router.navigateByUrl('/team/22/user/victor'))
.then((_) => { it('should unload outlets', fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
fixture.detectChanges(); let fixture = tcb.createFakeAsync(RootCmp);
expect(fixture.debugElement.nativeElement)
.toHaveText('team 22 { hello victor, aux: }'); router.navigateByUrl('/team/22/user/victor(/simple)');
async.done(); 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', it('should unload nested outlets',
inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => { fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
let fixture; let fixture = tcb.createFakeAsync(RootCmp);
compileRoot(tcb)
.then((rtc) => {fixture = rtc}) router.navigateByUrl('/team/22/user/victor(/simple)');
.then((_) => router.navigateByUrl('/team/22/user/victor(/simple)')) advance(fixture);
.then((_) => router.navigateByUrl('/'))
.then((_) => { router.navigateByUrl('/');
fixture.detectChanges(); advance(fixture);
expect(fixture.debugElement.nativeElement).toHaveText('');
async.done(); expect(fixture.debugElement.nativeElement).toHaveText('');
}); })));
}));
it('should update nested routes when url changes', it('should update nested routes when url changes',
inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => { fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
let fixture; let fixture = tcb.createFakeAsync(RootCmp);
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();
});
}));
// 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<ComponentFixture> { function compileRoot(tcb: TestComponentBuilder): Promise<ComponentFixture> {
return tcb.createAsync(RootCmp); return tcb.createAsync(RootCmp);
} }
@ -136,6 +163,18 @@ class UserCmp implements OnActivate {
class SimpleCmp { class SimpleCmp {
} }
@Component({selector: 'simple2-cmp', template: `simple2`})
class Simple2Cmp {
}
@Component({
selector: 'link-cmp',
template: `<a [routerLink]="['team', '33', 'simple']">link</a>`,
directives: ROUTER_DIRECTIVES
})
class LinkCmp {
}
@Component({ @Component({
selector: 'team-cmp', selector: 'team-cmp',
template: `team {{id}} { <router-outlet></router-outlet>, aux: <router-outlet name="aux"></router-outlet> }`, template: `team {{id}} { <router-outlet></router-outlet>, aux: <router-outlet name="aux"></router-outlet> }`,
@ -143,7 +182,9 @@ class SimpleCmp {
}) })
@Routes([ @Routes([
new Route({path: 'user/:name', component: UserCmp}), 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 { class TeamCmp implements OnActivate {
id: string; id: string;