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 {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]);

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 {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<RouteSegment>;
constructor(private _componentType: Type, private _componentResolver: ComponentResolver,
private _urlParser: RouterUrlParser, private _routerOutletMap: RouterOutletMap) {}
private _prevTree: Tree<RouteSegment>;
private _urlTree: Tree<UrlSegment>;
navigateByUrl(url: string): Promise<void> {
let urlSegmentTree = this._urlParser.parse(url);
return recognize(this._componentResolver, this._componentType, urlSegmentTree)
private _changes: EventEmitter<void> = new EventEmitter<void>();
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 => {
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<UrlSegment>): string { return this._urlSerializer.serialize(url); }
navigateByUrl(url: string): Promise<void> {
return this.navigate(this._urlSerializer.parse(url));
}
class _SegmentLoader {
get changes(): Observable<void> { return this._changes; }
}
class _LoadSegments {
constructor(private currTree: Tree<RouteSegment>, private prevTree: Tree<RouteSegment>) {}
loadSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,

View File

@ -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();
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
let fixture = tcb.createFakeAsync(RootCmp);
router.navigateByUrl('/team/22/user/victor(/simple)');
advance(fixture);
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 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();
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('');
async.done();
});
}));
})));
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<ComponentFixture> {
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: `<a [routerLink]="['team', '33', 'simple']">link</a>`,
directives: ROUTER_DIRECTIVES
})
class LinkCmp {
}
@Component({
selector: 'team-cmp',
template: `team {{id}} { <router-outlet></router-outlet>, aux: <router-outlet name="aux"></router-outlet> }`,
@ -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;