fix(router): make router links work on non-a tags

This commit is contained in:
vsavkin 2016-06-28 16:47:13 -07:00
parent 810c722413
commit 8c45aebc18
5 changed files with 103 additions and 15 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {RouterLink} from './src/directives/router_link'; import {RouterLink, RouterLinkWithHref} from './src/directives/router_link';
import {RouterLinkActive} from './src/directives/router_link_active'; import {RouterLinkActive} from './src/directives/router_link_active';
import {RouterOutlet} from './src/directives/router_outlet'; import {RouterOutlet} from './src/directives/router_outlet';
@ -24,4 +24,4 @@ export {DefaultUrlSerializer, UrlPathWithParams, UrlSerializer, UrlTree} from '.
/** /**
* @experimental * @experimental
*/ */
export const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink, RouterLinkActive]; export const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink, RouterLinkWithHref, RouterLinkActive];

View File

@ -54,8 +54,54 @@ import {UrlTree} from '../url_tree';
* *
* @stable * @stable
*/ */
@Directive({selector: '[routerLink]'}) @Directive({selector: ':not(a)[routerLink]'})
export class RouterLink implements OnChanges { export class RouterLink {
@Input() target: string;
private commands: any[] = [];
@Input() queryParams: {[k: string]: any};
@Input() fragment: string;
urlTree: UrlTree;
/**
* @internal
*/
constructor(
private router: Router, private route: ActivatedRoute,
private locationStrategy: LocationStrategy) {}
@Input()
set routerLink(data: any[]|string) {
if (Array.isArray(data)) {
this.commands = <any>data;
} else {
this.commands = [data];
}
}
@HostListener('click', ['$event.button', '$event.ctrlKey', '$event.metaKey'])
onClick(button: number, ctrlKey: boolean, metaKey: boolean): boolean {
if (button !== 0 || ctrlKey || metaKey) {
return true;
}
if (typeof this.target === 'string' && this.target != '_self') {
return true;
}
this.router.navigate(
this.commands,
{relativeTo: this.route, queryParams: this.queryParams, fragment: this.fragment});
return false;
}
}
/**
* See {@link RouterLink} for more information.
* @stable
*/
@Directive({selector: 'a[routerLink]'})
export class RouterLinkWithHref implements OnChanges {
@Input() target: string; @Input() target: string;
private commands: any[] = []; private commands: any[] = [];
@Input() queryParams: {[k: string]: any}; @Input() queryParams: {[k: string]: any};

View File

@ -10,9 +10,10 @@ import {AfterContentInit, ContentChildren, Directive, ElementRef, Input, OnChang
import {Subscription} from 'rxjs/Subscription'; import {Subscription} from 'rxjs/Subscription';
import {NavigationEnd, Router} from '../router'; import {NavigationEnd, Router} from '../router';
import {containsTree} from '../url_tree'; import {UrlTree, containsTree} from '../url_tree';
import {RouterLink, RouterLinkWithHref} from './router_link';
import {RouterLink} from './router_link';
/** /**
* The RouterLinkActive directive lets you add a CSS class to an element when the link's route * The RouterLinkActive directive lets you add a CSS class to an element when the link's route
@ -59,6 +60,8 @@ import {RouterLink} from './router_link';
@Directive({selector: '[routerLinkActive]'}) @Directive({selector: '[routerLinkActive]'})
export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit { export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit {
@ContentChildren(RouterLink) private links: QueryList<RouterLink>; @ContentChildren(RouterLink) private links: QueryList<RouterLink>;
@ContentChildren(RouterLinkWithHref) private linksWithHrefs: QueryList<RouterLinkWithHref>;
private classes: string[] = []; private classes: string[] = [];
private subscription: Subscription; private subscription: Subscription;
@ -77,6 +80,7 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
ngAfterContentInit(): void { ngAfterContentInit(): void {
this.links.changes.subscribe(s => this.update()); this.links.changes.subscribe(s => this.update());
this.linksWithHrefs.changes.subscribe(s => this.update());
this.update(); this.update();
} }
@ -93,15 +97,20 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
ngOnDestroy(): any { this.subscription.unsubscribe(); } ngOnDestroy(): any { this.subscription.unsubscribe(); }
private update(): void { private update(): void {
if (!this.links || this.links.length === 0) return; if (!this.links || !this.linksWithHrefs) return;
const currentUrlTree = this.router.parseUrl(this.router.url); const currentUrlTree = this.router.parseUrl(this.router.url);
const isActive = this.links.reduce( const isActiveLinks = this.reduceList(currentUrlTree, this.links);
(res, link) => const isActiveLinksWithHrefs = this.reduceList(currentUrlTree, this.linksWithHrefs);
this.classes.forEach(
c => this.renderer.setElementClass(
this.element.nativeElement, c, isActiveLinks || isActiveLinksWithHrefs));
}
private reduceList(currentUrlTree: UrlTree, q: QueryList<any>): boolean {
return q.reduce(
(res: boolean, link: any) =>
res || containsTree(currentUrlTree, link.urlTree, this.routerLinkActiveOptions.exact), res || containsTree(currentUrlTree, link.urlTree, this.routerLinkActiveOptions.exact),
false); false);
this.classes.forEach(
c => this.renderer.setElementClass(this.element.nativeElement, c, isActive));
} }
} }

View File

@ -476,7 +476,6 @@ describe('Integration', () => {
}))); })));
}); });
describe('router links', () => { describe('router links', () => {
it('should support string router links', it('should support string router links',
fakeAsync( fakeAsync(
@ -504,6 +503,31 @@ describe('Integration', () => {
expect(fixture.debugElement.nativeElement).toHaveText('team 33 { simple, right: }'); expect(fixture.debugElement.nativeElement).toHaveText('team 33 { simple, right: }');
}))); })));
it('should support using links on non-a tags',
fakeAsync(
inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => {
const fixture = createRoot(tcb, router, RootCmp);
router.resetConfig([{
path: 'team/:id',
component: TeamCmp,
children: [
{path: 'link', component: StringLinkButtonCmp},
{path: 'simple', component: SimpleCmp}
]
}]);
router.navigateByUrl('/team/22/link');
advance(fixture);
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, right: }');
const native = fixture.debugElement.nativeElement.querySelector('button');
native.click();
advance(fixture);
expect(fixture.debugElement.nativeElement).toHaveText('team 33 { simple, right: }');
})));
it('should support absolute router links', it('should support absolute router links',
fakeAsync( fakeAsync(
inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => {
@ -1023,6 +1047,15 @@ function expectEvents(events: Event[], pairs: any[]) {
class StringLinkCmp { class StringLinkCmp {
} }
@Component({
selector: 'link-cmp',
template: `<button routerLink
="/team/33/simple">link</button>`,
directives: ROUTER_DIRECTIVES
})
class StringLinkButtonCmp {
}
@Component({ @Component({
selector: 'link-cmp', selector: 'link-cmp',
template: `<router-outlet></router-outlet><a [routerLink]="['/team/33/simple']">link</a>`, template: `<router-outlet></router-outlet><a [routerLink]="['/team/33/simple']">link</a>`,
@ -1159,7 +1192,7 @@ class RelativeLinkInIfCmp {
precompile: [ precompile: [
BlankCmp, SimpleCmp, TeamCmp, UserCmp, StringLinkCmp, DummyLinkCmp, AbsoluteLinkCmp, BlankCmp, SimpleCmp, TeamCmp, UserCmp, StringLinkCmp, DummyLinkCmp, AbsoluteLinkCmp,
RelativeLinkCmp, DummyLinkWithParentCmp, LinkWithQueryParamsAndFragment, CollectParamsCmp, RelativeLinkCmp, DummyLinkWithParentCmp, LinkWithQueryParamsAndFragment, CollectParamsCmp,
QueryParamsAndFragmentCmp QueryParamsAndFragmentCmp, StringLinkButtonCmp
] ]
}) })
class RootCmp { class RootCmp {

View File

@ -123,7 +123,7 @@ export declare class Router {
} }
/** @experimental */ /** @experimental */
export declare const ROUTER_DIRECTIVES: (typeof RouterOutlet | typeof RouterLink | typeof RouterLinkActive)[]; export declare const ROUTER_DIRECTIVES: (typeof RouterOutlet | typeof RouterLink | typeof RouterLinkWithHref | typeof RouterLinkActive)[];
/** @stable */ /** @stable */
export declare type RouterConfig = Route[]; export declare type RouterConfig = Route[];