Introduction
Angular RouterLinkActive directive can be used without any difficulty to set active Bootstrap tab with multiple routes. Examples are also available on page Active RouterLinkActive with Multiple Routes.
But the case I encountered recently is kind of different. It works in a way that looks like a cross reference.
Background
The project has a header navigation bar with tabs of Dashboard, Reports, Analytics and Appointments, as shown below:
The HTML markup file nav.component.html includes this for the nav
bar:
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" [routerLinkActive]="['active']">
<a id="dashboardTab"
[routerLink]="['/dashboard']" role="tab" aria-selected="false">
<i class="icon icon-exc-dashboard-tab" aria-hidden="true"></i>
<span> {{'LINK_DASHBOARD' | translate}}</span>
</a>
</li>
<li role="presentation" [routerLinkActive]="['active']">
<a id="reportsTab"
[routerLink]="['/reports']" role="tab" aria-selected="false">
<i class="icon icon-exc-reports-tab" aria-hidden="true"></i>
<span> {{'LINK_REPORTS' | translate}}</span>
</a>
</li>
<li role="presentation" [routerLinkActive]="['active']">
<a id="analyticsTab"
[routerLink]="['/analytics']" role="tab" aria-selected="false">
<i class="icon icon-exc-chart" aria-hidden="true"></i>
<span> {{'LINK_ANALYTICS' | translate}}</span>
</a>
</li>
<li role="presentation" [routerLinkActive]="['active']">
<a id="appointmentsTab"
[routerLink]="['/appointments']" role="tab" aria-selected="false">
<i class="icon icon-exc-appointments" aria-hidden="true"></i>
<span> {{'LINK_APPOINTMENTS' | translate}}</span>
</a>
</li>
</ul>
On the Reports
page, a user can click on a report record to dig into the details which is pulled by the detail component with route of viewreport
, and the Reports tab should keep active like this.
Under the current design, no tab will be set active when the report detail page is rendered.
For Analytics, a user can click on an analytic record to get a graph illustration of the children items for this record, with route of viewanalytics
. Same as the report detail page, no tab is set active. In this case, the Analytics tab should keep active.
And when a user clicks on a child item of the analytic record, the exact same page as routed by viewreports
shows up. But in this case, the Analytics tab, instead of Report tab, should keep active. This is what I call cross reference active link.
Approaches to Fixing It
At first, I just added codes to keep the current tab active when the detail page opens.
public showReport(reportId: number): void {
let state = 'viewreports';
let activeTab = $('div.header-tab').find('li.active');
this.router.navigate(['/viewreports', {
reportIds: reportId
}], { skipLocationChange: true, state: { k: state } } ).then(
() => {
this.location.go(state);
activeTab.addClass('active');
}
);
}
This worked fine, until I clicked on another tab such as Dashboard, and surprisingly found that two tabs were set active simultaneously:
It is because I use activeTab.addClass('active')
when the detail page is showing up, and when another tab is selected, the added .active
class is not removed by the RouterLinkActive
directive.
According to Angular documentation for RouterLinkActive
, we can use it this way to realize the normal active tab switching if we don't have a cross reference link of viewreport, without using the code "activeTab.addClass('active')
" as mentioned above.
<li role="presentation" [routerLinkActive]="['active']">
<a id="reportsTab"
[routerLink]="['/reports']" role="tab" aria-selected="false">
<i class="icon icon-exc-reports-tab" aria-hidden="true"></i>
<span> {{'LINK_REPORTS' | translate}}</span>
</a>
<a [routerLink]="['/viewreports']" style="display: none"></a>
</li>
<li role="presentation" [routerLinkActive]="['active']">
<a id="analyticsTab"
[routerLink]="['/analytics']" role="tab" aria-selected="false">
<i class="icon icon-exc-chart" aria-hidden="true"></i>
<span> {{'LINK_ANALYTICS' | translate}}</span>
</a>
<a [routerLink]="['/viewanalytics']" style="display: none"></a>
<!--
<a [routerLink]="['/viewreports']" style="display: none"></a>
</li>
Since there is a viewreports
route for both Reports and Analytics tabs, my fix to the issue is keep using activeTab.addClass('active')
, remove the hidden viewreports
and viewanalytics routerlinks
, then add a router event handler to nav.component.ts (for brevity, non-related codes are eliminated):
import { Component } from '@angular/core';
import { Router, NavigationStart } from '@angular/router';
declare var $: any;
@Component({
selector: 'app-nav',
templateUrl: './nav.component.html',
styleUrls: ['./nav.component.scss']
})
export class NavComponent {
constructor(private router: Router) {
router.events.pipe(filter(e => e instanceof NavigationStart)).subscribe((e) => {
$('div.header-tab').find('li.active').removeClass('active');
});
}
}
If you prefer an Angular way (this project uses Angular 7.3.2), write the handler like this:
import { Component, ViewChild, Renderer2 } from '@angular/core';
import { Router, NavigationStart } from '@angular/router';
@Component({
selector: 'app-nav',
templateUrl: './nav.component.html',
styleUrls: ['./nav.component.scss']
})
export class NavComponent {
@ViewChild('tablist') tablist;
constructor(private router: Router, private renderer: Renderer2) {
router.events.pipe(filter(e => e instanceof NavigationStart)).subscribe((e) => {
if (this.tablist) {
Array.from(this.tablist.nativeElement.children).forEach(el => {
this.renderer.removeClass(el, 'active');
});
}
});
}
}
where tablist
is the template name added as in:
<ul class="nav nav-tabs" role="tablist" #tablist>
The idea behind this is remove any .active css class not added by RouterLinkActive
directive, giving the directive a clean playground, so that there will be no more than one active tab even if a tab is set active for a particular purpose.
Points of Interest
If only Angular could have the directive remove any .active
class added by user code to other DOM elements directive'd by RouterLinkActive
before doing its own work to set an active link, a developer would get an easier life. From the source code of RouterLinkActive, it seems that it does not care about this scenario.
History
- 25th August, 2019: Initial version