Omar Trejo
Omar Trejo

Reputation: 757

Angular 2 exact RouterLinkActive including fragments?

When using routerLink and routerLinkActive to apply CSS to a navigation bar, I'd like to also include the fragment information so that links are unique for sections within a homepage.

I've tried using [routerLinkActiveOptions]="{ exact: true }" wihtout any luck.

The relevant part of the navigation bar code is:

  <li routerLinkActive="active"
      [routerLinkActiveOptions]="{ exact: true }">
    <a routerLink="/sitio" fragment="inicio">Inicio</a>
  </li>
  <li routerLinkActive="active"
      [routerLinkActiveOptions]="{ exact: true }">
    <a routerLink="/sitio" fragment="invierte">Invierte</a>
  </li>
  <li routerLinkActive="active"
      [routerLinkActiveOptions]="{ exact: true }">
    <a routerLink="/sitio" fragment="contacto">Contacto</a>
  </li>

The three different URLs the above code visits are:

But when clickin any of them, all of them are marked as being active (because they correspond to the routerLink="/sitio" and the fragment=* information is not included in the check. This results in the navigation bar looking like this when clicking on any of them:

enter image description here

Any ideas on how to do this?

Upvotes: 18

Views: 8614

Answers (6)

mtpultz
mtpultz

Reputation: 18268

Angular's routerLinkActiveOptions provides fragment, queryParam, matrixParam, and path matching in order to match the active route without custom solutions.

If you look at RouteLinkActive you'll find it's a union:

routerLinkActiveOptions: { exact: boolean } | IsActiveMatchOptions;

Where IsActiveMatchOptions expands on the configuration to determine whether a router link is active by matching on any combination on fragments, queryParams, matrixParams, and paths:

export interface IsActiveMatchOptions {
  matrixParams: 'exact'|'subset'|'ignored';
  queryParams: 'exact'|'subset'|'ignored';
  paths: 'exact'|'subset';
  fragment: 'exact'|'ignored';
}

You pass them into routeLinkActiveOptions instead of { exact: true }:

<a [routerLinkActive]="linkActive"
   [routerLink]="linkCommands"
   [routerLinkActiveOptions]="linkActiveOptions"
   [fragment]="fragment">...</a>

Created a quick stackblitz to demonstrate usage using a fragment: https://stackblitz.com/edit/angular-ivy-1vekqs?file=src/app/app.component.html

Upvotes: 8

The solution that Omar suggested worked, however, when I have a fragment that contains the name of another fragment in the name, a conflict is generated.

For example, a fragment called tab and another called table, when activating the table fragment, the tab will also be activated, as it has the word tab in its name.

I made some changes to this solution and I hope it helps other people.

component.ts

import { ActivatedRoute } from '@angular/router';

constructor(private route: ActivatedRoute){}

isSectionActive(section: string): boolean {
  let element = false;
  this.route.fragment.subscribe((fragment: string) => {
    element = fragment === section.split("#").pop();
  });
  return element;
}

component.html

<button type="button" class="item" [routerLink]="['/components/tutorial']" [fragment]="fragmentName" [class.active]="isSectionActive(fragmentName)">{{fragmentName}}</button>

Upvotes: 0

lokeshjain2008
lokeshjain2008

Reputation: 1891

I struggle to work with [routerLinkActiveOptions]="{ exact: true }" even on the Angular 7.

So, I used the following trick to solve this issue. The answer is ActivatedRoute.

in the Component.

class ComponentClass{
  activeFragment = this.route.fragment.pipe(share()); 
  constructor(public route: ActivatedRoute){}
}

In the template use this as follows.

<li [class.active]="(activeFragment | async)==='inicio'">
    <a routerLink="/sitio" fragment="inicio">Inicio</a>
</li>
<li [class.active]="(activeFragment | async)==='invierte'">
    <a routerLink="/sitio" fragment="invierte">Invierte</a>
</li>
<li [class.active]="(activeFragment | async)==='contacto'">
    <a routerLink="/sitio" fragment="contacto">Contacto</a>
</li

I am not sure about the share operator here. I haven't looked into it if its multicast by default. Let me know if you have an answer to that.

Upvotes: 1

Suresh
Suresh

Reputation: 515

Making Omar Trejo's approach simpler. Rather than subscribing to the Route, I updated the isSectionActive to look at location.href to make it work.

private isSectionActive(section: string): boolean {
    return location.href.indexOf(section) !== -1;
}

Upvotes: 0

Omar Trejo
Omar Trejo

Reputation: 757

I just thought I would put how I solved it in case someone runs into the same problem: keep track of the current section in the site and use a class binding through a function in the component instead of using routerLinkActive.

The links in the template become:

<li [class.active]="isSectionActive('inicio')">
    <a routerLink="/sitio" fragment="inicio">Inicio</a>
</li>
<li [class.active]="isSectionActive('invierte')">
    <a routerLink="/sitio" fragment="invierte">Invierte</a>
</li>
<li [class.active]="isSectionActive('contacto')">
    <a routerLink="/sitio" fragment="contacto">Contacto</a>
</li>

Note the lack of use of routerLinkActive, and the use of class binding [class.active]=isSectionActive('<name-of-section>') instead.

The code that keeps track of the section we're inside of and decides whether or not to apply the CSS class in the template is:

import { Router, NavigationEnd } from '@angular/router';

// Class signature... {

    private activeSiteSection: string;

    constructor(
        private router: Router,
        private sessionService: SessionService
    ) {
        router.events.subscribe((event) => {
            if(event instanceof NavigationEnd ) {
                this.SiteURLActiveCheck(event);
            }
        });
    }

    private SiteURLActiveCheck(event: NavigationEnd): void {
        if (event.url.indexOf('#inicio') !== -1) {
            this.activeSiteSection = 'inicio';
        } else if (event.url.indexOf('#invierte') !== -1) {
            this.activeSiteSection = 'invierte';
        } else if (event.url.indexOf('#contacto') !== -1) {
            this.activeSiteSection = 'contacto';
        } else {
            this.activeSiteSection = '';
        }
    }

    private isSectionActive(section: string): boolean {
        return section === this.activeSiteSection;
    }
}

Probably overkill but I rather go this route than modify Angular 2's source. :)

Upvotes: 12

G&#252;nter Z&#246;chbauer
G&#252;nter Z&#246;chbauer

Reputation: 657318

There is an open issue to support that https://github.com/angular/angular/issues/13205

routerLinkActive is a simple directive. You might be able to create a clone yourself with that extended functionality.

Upvotes: 4

Related Questions