Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Multiple Links Share One Bootstrap Nav Tabs under Angular

5.00/5 (2 votes)
25 Aug 2019CPOL3 min read 8.4K  
This article shows approaches to set active Bootstrap nav tab under a specific condition.

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:

Image 1

The HTML markup file nav.component.html includes this for the nav bar:

HTML
<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>&nbsp;{{'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>&nbsp;{{'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>&nbsp;{{'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>&nbsp;{{'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.

Image 2

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.

JavaScript
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:

Image 3

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.

HTML
<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>&nbsp;{{'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>&nbsp;{{'LINK_ANALYTICS' | translate}}</span>
   </a>
   <a [routerLink]="['/viewanalytics']" style="display: none"></a>

   <!-- we cannot add this here. -->
   <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):

JavaScript
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:

JavaScript
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:

HTML
<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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)