【Angular8】Routingに合わせてtitleタグとmetaタグを更新する

Angularでシングルページアプリケーションを作る時に「titleタグにもブラケット記法でデータバインディングしたろ!」と思ったもののうまくいかずに悩んだことが誰しもあるかと思います。

私もご多分に洩れず悩んだので、ログとして残しておきます。

私がAngular, RxJS のバージョンは以下の通りです。 Angular: 8.2.13 rxjs: 6.4.0

titleタグとmetaタグを更新する方法

titleタグの更新方法はAngular公式サイトにて、基礎ガイドとして紹介されています。

まず、なぜ「titleタグにもブラケット記法でデータバインディングしたろ!」がうまくいかないかを以下に引用します。


わかりやすい方法は、次のようにHTMLの<title>コンポーネントのプロパティをバインドする方法です。
<title>{{This_Does_Not_Work}}</title>

すみません、これは動作しません。 アプリケーションのルートコンポーネントは <body>タグ内に含まれる要素です。
HTMLの<title>は、ドキュメントの<head>の中にあり、ボディの外側にあるため、Angularのデータバインディングにはアクセスできません。 引用元: Angular - ドキュメントのタイトルの設定

なるほどですねー。bodyタグの中がルートコンポーネントって、言われてみたらそりゃそうですね。

さて、今度はtitleタグを更新する方法ですが、Angular公式サイトではTitleサービスを使うようにと説明されています。

幸いなことに、Angularは ブラウザプラットフォーム の一部として Titleサービスを提供することで隔たりを埋めます。
そのTitleサービスは、現在のHTMLドキュメントのタイトルを取得および設定するためのAPIを提供する単純なクラスです。
    export class AppComponent {
      public constructor(private titleService: Title ) { }
    
      public setTitle( newTitle: string) {
        this.titleService.setTitle( newTitle );
      }
    }
  
引用元: Angular - ドキュメントのタイトルの設定

titleタグの更新方法がわかりましたね。

metaタグも同じように Metaサービスを使うことで更新することができるので、同じ要領で対応できます。 Angular - Meta

Routingに合わせて更新する

titleタグとmetaタグを更新するのは公式サイトをみれば、なんとなくできるような気がしましたが、画面が切り替わるタイミングにそれをしようとすると頭が「???」となってしまいます。

私はAngular公式サイトの 「Angular - ルーティングと画面遷移」をみても「なるほど!かんたん!」とは思えませんでした。

しかし、ありがたいことに、まさにやりたいことを実現してくれている先駆者がいましたので、それを参考にすることで、なんとか対応することができました。 Dynamic page titles in Angular 2 with router events

ルーティングを行うために path, component をまとめた配列を作っているかと思いますが、そこに data オブジェクトを追記します。

以下のような形です。

  // app-routing.module.ts 

  const routes: Routes = [
  // デフォルトページ
  { path: "", redirectTo: "/top-page", pathMatch: "full" },
  // トップページ
  {
    path: "top-page",
    component: TopPageComponent,
    // ルーティング時に参照することができる data オブジェクト
    data: {
      title: "titleタグ用",
      description: "metaタグdescription用"
    }
  }
];

上記のように書くことで、画面遷移した時に data オブジェクトの内容を参照して、titleタグとmetaタグの値の更新に利用することができます。

次は、画面遷移した時にdataオブジェクトを利用する処理です。

とはいえ、ほぼ参考サイトのコードそのままです。 RxJS のバージョンが違うので、そこだけが調整しています。

どのような処理をしているかを、コード内のコメントとして記載しました。

  // app.component.ts 

  import { Component, OnInit } from "@angular/core";
  import { Title, Meta } from "@angular/platform-browser";
  import { Router, NavigationEnd, ActivatedRoute } from "@angular/router";
  import { filter, map, mergeMap } from "rxjs/operators";
  
  @Component({
    selector: "app-root",
    templateUrl: "./app.component.html",
    styleUrls: ["./app.component.scss"]
  })
  export class AppComponent implements OnInit {
    constructor(
      private router: Router,
      private activatedRoute: ActivatedRoute,
      private titleService: Title,
      private metaService: Meta
    ) {}
  
    ngOnInit() {
      // ルーターイベント
      this.router.events
        .pipe(
          // ルーターイベントから NavigationEnd のみを抽出
          // NavigationEnd - 画面遷移が正常に完了したときに発火するイベント
          filter(event => event instanceof NavigationEnd),
          // activatedRoute - 個々のルートコンポーネントに提供されるサービス
          map(() => this.activatedRoute),
          map(route => {
            // firstChild - このルートの子ルートの中で最初の ActivatedRoute
            // 入れ子になっているので while で回す(firstChild: null になるまで)
            while (route.firstChild) route = route.firstChild;
            return route;
          }),
          // ルートを描画するのに使われる RouterOutlet の名前
          // 無名のアウトレットは primary という名前になる
          filter(route => route.outlet === "primary"),
          // ルートから提供される data オブジェクトを含む Observable
          mergeMap(route => route.data)
        )
        .subscribe(event => {
          // Routes の配列で指定した data オブジェクトが使える
          // Titleサービスで title タグを更新
          this.titleService.setTitle(event["title"]);
          // Metaサービスで meta タグを更新
          this.metaService.updateTag({
            name: "description",
            content: event["description"]
          });
        });
    }
  }  

人気記事すべて表示

WEBすべて表示