diff --git a/modules/@angular/router/index.ts b/modules/@angular/router/index.ts index 24b0f9d8d2..4e068bac81 100644 --- a/modules/@angular/router/index.ts +++ b/modules/@angular/router/index.ts @@ -6,7 +6,7 @@ * 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 {RouterOutlet} from './src/directives/router_outlet'; @@ -24,4 +24,4 @@ export {DefaultUrlSerializer, UrlPathWithParams, UrlSerializer, UrlTree} from '. /** * @experimental */ -export const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink, RouterLinkActive]; +export const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink, RouterLinkWithHref, RouterLinkActive]; diff --git a/modules/@angular/router/src/directives/router_link.ts b/modules/@angular/router/src/directives/router_link.ts index 2ca387a36f..3b0c87618d 100644 --- a/modules/@angular/router/src/directives/router_link.ts +++ b/modules/@angular/router/src/directives/router_link.ts @@ -54,8 +54,54 @@ import {UrlTree} from '../url_tree'; * * @stable */ -@Directive({selector: '[routerLink]'}) -export class RouterLink implements OnChanges { +@Directive({selector: ':not(a)[routerLink]'}) +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 = 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; private commands: any[] = []; @Input() queryParams: {[k: string]: any}; diff --git a/modules/@angular/router/src/directives/router_link_active.ts b/modules/@angular/router/src/directives/router_link_active.ts index a0a12c8af4..58bff8a109 100644 --- a/modules/@angular/router/src/directives/router_link_active.ts +++ b/modules/@angular/router/src/directives/router_link_active.ts @@ -10,9 +10,10 @@ import {AfterContentInit, ContentChildren, Directive, ElementRef, Input, OnChang import {Subscription} from 'rxjs/Subscription'; 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 @@ -59,6 +60,8 @@ import {RouterLink} from './router_link'; @Directive({selector: '[routerLinkActive]'}) export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit { @ContentChildren(RouterLink) private links: QueryList; + @ContentChildren(RouterLinkWithHref) private linksWithHrefs: QueryList; + private classes: string[] = []; private subscription: Subscription; @@ -77,6 +80,7 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit ngAfterContentInit(): void { this.links.changes.subscribe(s => this.update()); + this.linksWithHrefs.changes.subscribe(s => this.update()); this.update(); } @@ -93,15 +97,20 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit ngOnDestroy(): any { this.subscription.unsubscribe(); } 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 isActive = this.links.reduce( - (res, link) => + const isActiveLinks = this.reduceList(currentUrlTree, this.links); + 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): boolean { + return q.reduce( + (res: boolean, link: any) => res || containsTree(currentUrlTree, link.urlTree, this.routerLinkActiveOptions.exact), false); - - this.classes.forEach( - c => this.renderer.setElementClass(this.element.nativeElement, c, isActive)); } } diff --git a/modules/@angular/router/test/router.spec.ts b/modules/@angular/router/test/router.spec.ts index e38ad02aba..d7b29f196c 100644 --- a/modules/@angular/router/test/router.spec.ts +++ b/modules/@angular/router/test/router.spec.ts @@ -476,7 +476,6 @@ describe('Integration', () => { }))); }); - describe('router links', () => { it('should support string router links', fakeAsync( @@ -504,6 +503,31 @@ describe('Integration', () => { 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', fakeAsync( inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { @@ -1023,6 +1047,15 @@ function expectEvents(events: Event[], pairs: any[]) { class StringLinkCmp { } +@Component({ + selector: 'link-cmp', + template: ``, + directives: ROUTER_DIRECTIVES +}) +class StringLinkButtonCmp { +} + @Component({ selector: 'link-cmp', template: `link`, @@ -1159,7 +1192,7 @@ class RelativeLinkInIfCmp { precompile: [ BlankCmp, SimpleCmp, TeamCmp, UserCmp, StringLinkCmp, DummyLinkCmp, AbsoluteLinkCmp, RelativeLinkCmp, DummyLinkWithParentCmp, LinkWithQueryParamsAndFragment, CollectParamsCmp, - QueryParamsAndFragmentCmp + QueryParamsAndFragmentCmp, StringLinkButtonCmp ] }) class RootCmp { diff --git a/tools/public_api_guard/router/index.d.ts b/tools/public_api_guard/router/index.d.ts index 87156d842f..3c9a64ce91 100644 --- a/tools/public_api_guard/router/index.d.ts +++ b/tools/public_api_guard/router/index.d.ts @@ -123,7 +123,7 @@ export declare class Router { } /** @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 */ export declare type RouterConfig = Route[];