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 {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]);
|
||||||
|
|
|
@ -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 {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>,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue