notebook

都内でWEB系エンジニアやってます。

routerLinkActiveでpathだけのマッチを行いたい

よくあるリンクがアクティブかそうじゃないかみたいなのを判断してスタイルを変える機能に関しての備忘録

AngularだとrouterLinkActiveというものがあります

  • 動作環境: Angular5.1

  • html

<a [routerLink]="['/hoge', 'fuga', 'piyo']" routerLinkActive="is-active">
  • scss
a.is-ctive {
  background-color: #CCC;
}

これでメニュー中の自分がいるページのリンクだけ背景が変わるというよくあるやつですね

ただ、このrouterLinkActiveは中身を追ったところ下記がマッチしている、もしくは含まれていることが条件になっているようです

  • queryParameterの内容
  • pathの内容

今回pathだけマッチしたらアクティブと判定させたい、という要件が発生したのでそういうオプションなどあるのかなーと思って見てみましたがありませんでした

ということでrouterLinkActiveと同様にディレクティブを実装してみました

import {
  AfterContentInit,
  ContentChildren,
  Directive,
  ElementRef,
  Input,
  OnChanges,
  OnDestroy,
  QueryList,
  Renderer2,
  SimpleChanges
} from '@angular/core';
import {
  NavigationEnd,
  Router,
  RouterEvent,
  RouterLink,
  RouterLinkWithHref,
  UrlSegment,
} from '@angular/router';
import { Subscription } from 'rxjs/Subscription';

@Directive({
  selector: '[routerPathActive]'
})
export class RouterPathActiveDirective
  implements AfterContentInit, OnChanges, OnDestroy {
  @ContentChildren(RouterLink, { descendants: true })
  links: QueryList<RouterLink>; // aタグ以外でのリンク要素
  @ContentChildren(RouterLinkWithHref, { descendants: true })
  linksWithHrefs: QueryList<RouterLinkWithHref>; // aタグでのリンク要素

  private classes: string[] = [];
  private subscription: Subscription;

  public isActive: boolean = false;

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

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

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

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

  private update() {
    if (!this.links || !this.linksWithHrefs || !this.router.navigated) return;

    Promise.resolve().then(() => {
      const urlSegments = this.router['currentUrlTree']['root']['children'][
        'primary'
      ]['segments'];
      const hasActiveLinks = this.hasActiveLinks(urlSegments);

      if (this.isActive !== hasActiveLinks) {
        this.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(
    currentSegments: UrlSegment[]
  ): (link: RouterLink | RouterLinkWithHref) => boolean {
    return (link: RouterLink | RouterLinkWithHref) => {
      const currentSegmentPaths = currentSegments.map(segment => segment.path);
      const linkSegmentPaths = link.urlTree.root.children.primary.segments.map(
        segment => segment.path
      );
      if (currentSegmentPaths.length != linkSegmentPaths.length) return false;

      return currentSegmentPaths.every((currentSegmentPath, i, _) => {
        return currentSegmentPath == linkSegmentPaths[i];
      });
    };
  }

  private hasActiveLinks(currentSegments): boolean {
    return (
      this.links.some(this.isLinkActive(currentSegments)) ||
      this.linksWithHrefs.some(this.isLinkActive(currentSegments))
    );
  }
}
  • html
<a [routerLink]="['/hoge', 'fuga', 'piyo']" routerPathActive="is-active">

これでOK

無事パスのみがマッチしている状態でスタイルを充てることができるようになりました

下記のソースを参()にしてみました

angular/router_link_active.ts at master · angular/angular

感想

オプションが用意されていないということは本来そういう風な実装にはしないと言う想定なのだと思いますがまぁ中身読んで実装できたので良かったかなと思います

コード中でRouterのprivate変数を無理やりしたりしてしまってるのでちょっと微妙ですが...

全然関係ないけど、ソース読んでるときっちり型定義していて読みやすかったです、仕事で書いてるコードでtypescript書いててもなんだかんだで結局anyのまま放置みたいなところがあるので反省してちゃんと型を定義していかないと