fix(router): routerLinkActive should not throw when not initialized (#13273)

Fixes #13270

PR Close #13273
This commit is contained in:
Dzmitry Shylovich 2016-12-07 03:22:38 +03:00 committed by Miško Hevery
parent 1a92e3d406
commit e8ea741039
3 changed files with 29 additions and 20 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 {AfterContentInit, ChangeDetectorRef, ContentChildren, Directive, ElementRef, Input, OnChanges, OnDestroy, QueryList, Renderer} from '@angular/core'; import {AfterContentInit, ChangeDetectorRef, ContentChildren, Directive, ElementRef, Input, OnChanges, OnDestroy, QueryList, Renderer, SimpleChanges} from '@angular/core';
import {Subscription} from 'rxjs/Subscription'; import {Subscription} from 'rxjs/Subscription';
import {NavigationEnd, Router} from '../router'; import {NavigationEnd, Router} from '../router';
@ -14,6 +14,7 @@ import {NavigationEnd, Router} from '../router';
import {RouterLink, RouterLinkWithHref} from './router_link'; import {RouterLink, RouterLinkWithHref} from './router_link';
/** /**
* @whatItDoes Lets you add a CSS class to an element when the link's route becomes active. * @whatItDoes Lets you add a CSS class to an element when the link's route becomes active.
* *
@ -82,17 +83,20 @@ import {RouterLink, RouterLinkWithHref} from './router_link';
exportAs: 'routerLinkActive', exportAs: 'routerLinkActive',
}) })
export class RouterLinkActive implements OnChanges, export class RouterLinkActive implements OnChanges,
OnDestroy, AfterContentInit { OnDestroy, AfterContentInit {
@ContentChildren(RouterLink, {descendants: true}) links: QueryList<RouterLink>; @ContentChildren(RouterLink, {descendants: true}) links: QueryList<RouterLink>;
@ContentChildren(RouterLinkWithHref, {descendants: true}) @ContentChildren(RouterLinkWithHref, {descendants: true})
linksWithHrefs: QueryList<RouterLinkWithHref>; linksWithHrefs: QueryList<RouterLinkWithHref>;
private classes: string[] = []; private classes: string[] = [];
private subscription: Subscription; private subscription: Subscription;
private active: boolean = false;
@Input() routerLinkActiveOptions: {exact: boolean} = {exact: false}; @Input() routerLinkActiveOptions: {exact: boolean} = {exact: false};
constructor(private router: Router, private element: ElementRef, private renderer: Renderer) { constructor(
private router: Router, private element: ElementRef, private renderer: Renderer,
private cdr: ChangeDetectorRef) {
this.subscription = router.events.subscribe(s => { this.subscription = router.events.subscribe(s => {
if (s instanceof NavigationEnd) { if (s instanceof NavigationEnd) {
this.update(); this.update();
@ -100,7 +104,7 @@ export class RouterLinkActive implements OnChanges,
}); });
} }
get isActive(): boolean { return this.hasActiveLink(); } get isActive(): boolean { return this.active; }
ngAfterContentInit(): void { ngAfterContentInit(): void {
this.links.changes.subscribe(_ => this.update()); this.links.changes.subscribe(_ => this.update());
@ -110,30 +114,33 @@ export class RouterLinkActive implements OnChanges,
@Input() @Input()
set routerLinkActive(data: string[]|string) { set routerLinkActive(data: string[]|string) {
this.classes = (Array.isArray(data) ? data : data.split(' ')).filter(c => !!c); const classes = Array.isArray(data) ? data : data.split(' ');
this.classes = classes.filter(c => !!c);
} }
ngOnChanges(changes: {}): void { this.update(); } ngOnChanges(changes: SimpleChanges): void { this.update(); }
ngOnDestroy(): void { this.subscription.unsubscribe(); } ngOnDestroy(): void { this.subscription.unsubscribe(); }
private update(): void { private update(): void {
if (!this.links || !this.linksWithHrefs || !this.router.navigated) return; if (!this.links || !this.linksWithHrefs || !this.router.navigated) return;
const hasActiveLinks = this.hasActiveLinks();
const isActive = this.hasActiveLink(); // react only when status has changed to prevent unnecessary dom updates
this.classes.forEach(c => { if (this.active !== hasActiveLinks) {
if (c) { this.active = hasActiveLinks;
this.renderer.setElementClass(this.element.nativeElement, c, isActive); this.classes.forEach(
} c => this.renderer.setElementClass(this.element.nativeElement, c, hasActiveLinks));
}); this.cdr.detectChanges();
}
} }
private isLinkActive(router: Router): (link: (RouterLink|RouterLinkWithHref)) => boolean { private isLinkActive(router: Router): (link: (RouterLink|RouterLinkWithHref)) => boolean {
return (link: RouterLink | RouterLinkWithHref) => return (link: RouterLink | RouterLinkWithHref) =>
router.isActive(link.urlTree, this.routerLinkActiveOptions.exact); router.isActive(link.urlTree, this.routerLinkActiveOptions.exact);
} }
private hasActiveLink(): boolean { private hasActiveLinks(): boolean {
return this.links.some(this.isLinkActive(this.router)) || return this.links.some(this.isLinkActive(this.router)) ||
this.linksWithHrefs.some(this.isLinkActive(this.router)); this.linksWithHrefs.some(this.isLinkActive(this.router));
} }
} }

View File

@ -2096,6 +2096,8 @@ describe('Integration', () => {
@Component({ @Component({
template: `<a routerLink="/team" routerLinkActive #rla="routerLinkActive"></a> template: `<a routerLink="/team" routerLinkActive #rla="routerLinkActive"></a>
<p>{{rla.isActive}}</p> <p>{{rla.isActive}}</p>
<span *ngIf="rla.isActive"></span>
<span [ngClass]="{'highlight': rla.isActive}"></span>
<router-outlet></router-outlet>` <router-outlet></router-outlet>`
}) })
class ComponentWithRouterLink { class ComponentWithRouterLink {
@ -2115,15 +2117,15 @@ describe('Integration', () => {
} }
]); ]);
const f = TestBed.createComponent(ComponentWithRouterLink); const fixture = TestBed.createComponent(ComponentWithRouterLink);
router.navigateByUrl('/team'); router.navigateByUrl('/team');
advance(f); expect(() => advance(fixture)).not.toThrow();
const paragraph = f.nativeElement.querySelector('p'); const paragraph = fixture.nativeElement.querySelector('p');
expect(paragraph.textContent).toEqual('true'); expect(paragraph.textContent).toEqual('true');
router.navigateByUrl('/otherteam'); router.navigateByUrl('/otherteam');
advance(f); advance(fixture);
expect(paragraph.textContent).toEqual('false'); expect(paragraph.textContent).toEqual('false');
})); }));

View File

@ -264,7 +264,7 @@ export declare class RouterLinkActive implements OnChanges, OnDestroy, AfterCont
}; };
constructor(router: Router, element: ElementRef, renderer: Renderer, cdr: ChangeDetectorRef); constructor(router: Router, element: ElementRef, renderer: Renderer, cdr: ChangeDetectorRef);
ngAfterContentInit(): void; ngAfterContentInit(): void;
ngOnChanges(changes: {}): void; ngOnChanges(changes: SimpleChanges): void;
ngOnDestroy(): void; ngOnDestroy(): void;
} }