feat(router): add RouterLink
This commit is contained in:
parent
fa5bfe4b64
commit
de56dd5f30
|
@ -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]);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
get changes(): Observable<void> { return this._changes; }
|
||||
}
|
||||
|
||||
class _SegmentLoader {
|
||||
class _LoadSegments {
|
||||
constructor(private currTree: Tree<RouteSegment>, private prevTree: Tree<RouteSegment>) {}
|
||||
|
||||
loadSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,
|
||||
|
|
|
@ -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<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;
|
||||
|
|
Loading…
Reference in New Issue