Skip to content

Commit

Permalink
fix(router): evaluate routerLinkActive state when routerLink changes (a…
Browse files Browse the repository at this point in the history
  • Loading branch information
linnenschmidt committed Sep 18, 2018
1 parent 5653874 commit 2dc9a7c
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 156 deletions.
162 changes: 157 additions & 5 deletions packages/router/src/directives/router_link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {LocationStrategy} from '@angular/common';
import {Attribute, Directive, ElementRef, HostBinding, HostListener, Input, OnChanges, OnDestroy, Renderer2, isDevMode} from '@angular/core';
import {AfterContentInit, Attribute, ChangeDetectorRef, ContentChildren, Directive, ElementRef, HostBinding, HostListener, Inject, Input, OnChanges, OnDestroy, Optional, QueryList, Renderer2, SimpleChanges, forwardRef, isDevMode} from '@angular/core';
import {Subscription} from 'rxjs';

import {QueryParamsHandling} from '../config';
Expand Down Expand Up @@ -91,7 +91,7 @@ import {UrlTree} from '../url_tree';
*
*/
@Directive({selector: ':not(a)[routerLink]'})
export class RouterLink {
export class RouterLink implements OnChanges {
// TODO(issue/24571): remove '!'.
@Input() queryParams !: {[k: string]: any};
// TODO(issue/24571): remove '!'.
Expand All @@ -110,7 +110,9 @@ export class RouterLink {

constructor(
private router: Router, private route: ActivatedRoute,
@Attribute('tabindex') tabIndex: string, renderer: Renderer2, el: ElementRef) {
@Attribute('tabindex') tabIndex: string, renderer: Renderer2, el: ElementRef,
@Optional() @Inject(forwardRef(() => RouterLinkActive)) private routeLinkActive:
RouterLinkActive) {
if (tabIndex == null) {
renderer.setAttribute(el.nativeElement, 'tabindex', '0');
}
Expand Down Expand Up @@ -156,6 +158,14 @@ export class RouterLink {
preserveFragment: attrBoolValue(this.preserveFragment),
});
}

ngOnChanges(changes: {}): any { this.updateRouterLinkActive(); }

private updateRouterLinkActive() {
if (this.routeLinkActive !== null) {
this.routeLinkActive.update();
}
}
}

/**
Expand Down Expand Up @@ -196,7 +206,9 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {

constructor(
private router: Router, private route: ActivatedRoute,
private locationStrategy: LocationStrategy) {
private locationStrategy: LocationStrategy,
@Optional() @Inject(forwardRef(() => RouterLinkActive)) private routeLinkActive:
RouterLinkActive) {
this.subscription = router.events.subscribe((s: RouterEvent) => {
if (s instanceof NavigationEnd) {
this.updateTargetUrlAndHref();
Expand All @@ -221,7 +233,10 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
this.preserve = value;
}

ngOnChanges(changes: {}): any { this.updateTargetUrlAndHref(); }
ngOnChanges(changes: {}): any {
this.updateTargetUrlAndHref();
this.updateRouterLinkActive();
}
ngOnDestroy(): any { this.subscription.unsubscribe(); }

@HostListener('click', ['$event.button', '$event.ctrlKey', '$event.metaKey', '$event.shiftKey'])
Expand All @@ -246,6 +261,12 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
this.href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.urlTree));
}

private updateRouterLinkActive() {
if (this.routeLinkActive !== null) {
this.routeLinkActive.update();
}
}

get urlTree(): UrlTree {
return this.router.createUrlTree(this.commands, {
relativeTo: this.route,
Expand All @@ -261,3 +282,134 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
function attrBoolValue(s: any): boolean {
return s === '' || !!s;
}

/**
*
* @description
*
* Lets you add a CSS class to an element when the link's route becomes active.
*
* This directive lets you add a CSS class to an element when the link's route
* becomes active.
*
* Consider the following example:
*
* ```
* <a routerLink="/user/bob" routerLinkActive="active-link">Bob</a>
* ```
*
* When the url is either '/user' or '/user/bob', the active-link class will
* be added to the `a` tag. If the url changes, the class will be removed.
*
* You can set more than one class, as follows:
*
* ```
* <a routerLink="/user/bob" routerLinkActive="class1 class2">Bob</a>
* <a routerLink="/user/bob" [routerLinkActive]="['class1', 'class2']">Bob</a>
* ```
*
* You can configure RouterLinkActive by passing `exact: true`. This will add the classes
* only when the url matches the link exactly.
*
* ```
* <a routerLink="/user/bob" routerLinkActive="active-link" [routerLinkActiveOptions]="{exact:
* true}">Bob</a>
* ```
*
* You can assign the RouterLinkActive instance to a template variable and directly check
* the `isActive` status.
* ```
* <a routerLink="/user/bob" routerLinkActive #rla="routerLinkActive">
* Bob {{ rla.isActive ? '(already open)' : ''}}
* </a>
* ```
*
* Finally, you can apply the RouterLinkActive directive to an ancestor of a RouterLink.
*
* ```
* <div routerLinkActive="active-link" [routerLinkActiveOptions]="{exact: true}">
* <a routerLink="/user/jim">Jim</a>
* <a routerLink="/user/bob">Bob</a>
* </div>
* ```
*
* This will set the active-link class on the div tag if the url is either '/user/jim' or
* '/user/bob'.
*
* @ngModule RouterModule
*
*
*/
@Directive({
selector: '[routerLinkActive]',
exportAs: 'routerLinkActive',
})
export class RouterLinkActive implements OnChanges,
OnDestroy, AfterContentInit {
// TODO(issue/24571): remove '!'.
@ContentChildren(RouterLink, {descendants: true})
links !: QueryList<RouterLink>;
// TODO(issue/24571): remove '!'.
@ContentChildren(RouterLinkWithHref, {descendants: true})
linksWithHrefs !: QueryList<RouterLinkWithHref>;

private classes: string[] = [];
private subscription: Subscription;
public readonly isActive: boolean = false;

@Input() routerLinkActiveOptions: {exact: boolean} = {exact: false};

constructor(
private router: Router, private element: ElementRef, private renderer: Renderer2,
private cdr: ChangeDetectorRef) {
this.subscription = router.events.subscribe((s: RouterEvent) => {
if (s instanceof NavigationEnd) {
this.update();
}
});
}


ngAfterContentInit(): void {
this.links.changes.subscribe(_ => this.update());
this.linksWithHrefs.changes.subscribe(_ => this.update());
this.update();
}

@Input()
set routerLinkActive(data: string[]|string) {
const classes = Array.isArray(data) ? data : data.split(' ');
this.classes = classes.filter(c => !!c);
}

ngOnChanges(changes: SimpleChanges): void { this.update(); }
ngOnDestroy(): void { this.subscription.unsubscribe(); }

/** @internal */
update(): void {
if (!this.links || !this.linksWithHrefs || !this.router.navigated) return;
Promise.resolve().then(() => {
const hasActiveLinks = this.hasActiveLinks();
if (this.isActive !== hasActiveLinks) {
(this as any).isActive = hasActiveLinks;
this.classes.forEach((c) => {
if (hasActiveLinks) {
this.renderer.addClass(this.element.nativeElement, c);
} else {
this.renderer.removeClass(this.element.nativeElement, c);
}
});
}
});
}

private isLinkActive(router: Router): (link: (RouterLink|RouterLinkWithHref)) => boolean {
return (link: RouterLink | RouterLinkWithHref) =>
router.isActive(link.urlTree, this.routerLinkActiveOptions.exact);
}

private hasActiveLinks(): boolean {
return this.links.some(this.isLinkActive(this.router)) ||
this.linksWithHrefs.some(this.isLinkActive(this.router));
}
}
146 changes: 0 additions & 146 deletions packages/router/src/directives/router_link_active.ts

This file was deleted.

3 changes: 1 addition & 2 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@


export {Data, LoadChildren, LoadChildrenCallback, ResolveData, Route, Routes, RunGuardsAndResolvers, UrlMatchResult, UrlMatcher} from './config';
export {RouterLink, RouterLinkWithHref} from './directives/router_link';
export {RouterLinkActive} from './directives/router_link_active';
export {RouterLink, RouterLinkActive, RouterLinkWithHref} from './directives/router_link';
export {RouterOutlet} from './directives/router_outlet';
export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events';
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces';
Expand Down
3 changes: 1 addition & 2 deletions packages/router/src/router_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import {Subject, of } from 'rxjs';

import {EmptyOutletComponent} from './components/empty_outlet';
import {Route, Routes} from './config';
import {RouterLink, RouterLinkWithHref} from './directives/router_link';
import {RouterLinkActive} from './directives/router_link_active';
import {RouterLink, RouterLinkActive, RouterLinkWithHref} from './directives/router_link';
import {RouterOutlet} from './directives/router_outlet';
import {RouterEvent} from './events';
import {RouteReuseStrategy} from './route_reuse_strategy';
Expand Down

0 comments on commit 2dc9a7c

Please sign in to comment.