Angularと戯れる⑥

# はじめに
[Angularチュートリアル：Tour of Heroes](https://v17.angular.jp/tutorial)を読み進めていきます。

# 開発環境
- OS: Windows10
- Nodeバージョン: 18.18.0
- Angular CLI バージョン: 17.3.8
- エディタ: VSCode


# 5. ナビゲーションの追加
[5. ナビゲーションの追加](https://v17.angular.jp/tutorial/tour-of-heroes/toh-pt5)を読んでいきます。

## アプリ構成
![nav-diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3873589/21ca20d8-d4b6-faf6-3a02-f2ebecb00a5a.png)

https://v17.angular.jp/tutorial/tour-of-heroes/toh-pt5 に添付されている図より。

1. アプリのトップ画面にダッシュボード用のボタンとヒーロー一覧画面（こらまで作ってきたHeroesComponent）を表示する用のボタンを配置する
1. ダッシュボードからヒーロー詳細画面に遷移できる
1. ヒーロー一覧画面からヒーロー詳細画面に遷移できる
1. ヒーロー詳細画面には「戻る」ボタンを配置し、押下することで、遷移元の画面に戻ることができる

## 関連コマンド
`ng generate module app-routing --flat --module=app`

これにより以下のことが起こります。
- src/app フォルダに __app-routing.module.ts__ が生成される
    - `--flat`オプションをつけることで、固有のディr区鳥の代わりに、src/appに生成したファイルが配置される。
```TS:生成された src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: []
})
export class AppRoutingModule { }
```
- src/app/app.module.tsが、このapp-routing.module.ts をインポートする
    - `--module=app`オプションを付けることで、AppModuleのimports配列に、生成したモジュールを登録するようCLIに指示する。
```TS:src/app/app.module.ts
...
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...,
    AppRoutingModule
  ],
  ...
})

```

ルーティング定義を書くのであれば、app.module.tsに書くでもいけそうですが（構造同じですし）、ルーティングに関する記載はapp.module.tsの外に出して、分割できるのであれば分割するのがAngular流なのかなという理解をしました。

今後ルーティング定義はapp-routing.module.ts に書いていくことになります。

## ソースコードを読み解いてみる

### Routeの設定: RouterModule.forRoot()
例えば以下のように記載すると、「localhost:4200/heroes」というURLが入力されたとき、HeroesCoponentの内容が記載されるという意味になります。
```TS:src/app/app-routing.module.ts
import { RouterModule, Routes } from '@angular/router';
...

const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];
```

「URL末尾に何もなければこのURLのパターンにリダイレクトする」「ID等の数字をURLのパスのパターンとして登録する」こともできます。
```TS:src/app/app-routing.module.ts
import { RouterModule, Routes } from '@angular/router';
...

const routes: Routes = [
  // URLの末尾に「/heroes」をつけると、HeroesComponentの内容が表示される
  {path: "heroes", component: HeroesComponent},
  // URLの末尾に「/dashboard」をつけると、DashboardComponentの内容が表示される
  {path: "dashboard", component: DashboardComponent},
  // URLの末尾に何も入力されていない（空という文字列に完全一致している）場合、dashboardにリダイレクトする
  {path: "", redirectTo: "/dashboard", pathMatch: "full"},
  // URLの末尾に「/detail/%id%」をつけることで該当のHeroDetailComponentの内容が表示される
  { path: "detail/:id", component: HeroDetailComponent }
]
```

その設定を適用させます。
これにより、「このパスのパターンの場合はこれを表示する」という対応をアプリに登録することになります。
```TS:src/app/app-routing.module.ts
import { RouterModule, Routes } from '@angular/router';
...

imports: [ RouterModule.forRoot(routes) ],
```
そしてそのRouterModuleを共有できるようにexportします。
```TS:src/app/app-routing.module.ts
import { RouterModule, Routes } from '@angular/router';
...

exports: [ RouterModule ]
```

### RouterOutlet
コンポーネント内で`<router-outlet>`タグを使います。
```HTML:src/app/app.component.html
<h1>{{title}}</h1>
<router-outlet></router-outlet>
<app-messages></app-messages>
```
`<router-outlet>`を使うことで、現在ルーティングされているコンテンツを __動的に切り替えて表示__ できます。

つまり、
- `~/heroes` というURLが指定されたら、`<router-outlet>`の部分にはそれに対応するHeroesComponentの内容が表示されるし、
- `~/dashboard` というURLが指定されたら、`<router-outlet>`の部分にはそれに対応するDashboardComponsntの内容が表示される

ということです。
（`<h1>`と`<app-messages>`はルーティングとして何が指定されていようが固定で表示される）

### RouterLink
```HTML:src/app/app.component.html
...
<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/heroes">Heroes</a>
</nav>

<router-outlet></router-outlet>
<app-messages></app-messages>
```
`routerLink`属性を使ってパスを指定することで、そのパスのパターンにマッチするコンポーネントにリンクを飛ばすことができます。

たとえば、Heroesという文字列が表示されたリンクを踏むと、
1. `routerLink="/heroes"`のパターンをRouterに問い合わせる
1. app-routing.module.ts を見ると、そのパターンにマッチするコンポーネントはHeroesComponentだと分かる
1. HeroesComponentの内容を表示すべきと判断する（表示場所は`<router-outlet>`）
```TS:src/app/app-routing.module.ts
...

const routes: Routes = [
  // このパターンにマッチ
  {path: "heroes", component: HeroesComponent},
  ...
]
```

という流れで画面が表示されます。
<br>

なお、リンクを飛ばすといえば`<a href="〇〇>"`ですが、この手法だとページ全体に更新がかかります。

`routerLink`を使うことで、ページ全体ではなく該当する箇所のみに更新がかかるようになります。
SPA（Single Page Application）の特性と関連したテクニックといえるでしょう。

### RouterLink（その2）
URLにID等の数字を入れてページを表示する場合もあります。
　＞例： localhost:4200/detail/13

その数字は固定ではなく、13のときもあれば15のときもあります。
こうした動的な値を含んだURLへのリンクを飛ばすテクニックについて書いていきます。

#### ルーティングのパターンの登録
```TS:src/app/app-routing.module.ts
...
const routes: Routes = [
  ...
  // URLの末尾に「/detail/%id%」をつけることで該当のHeroDetailComponentの内容が表示される
  { path: "detail/:id", component: HeroDetailComponent }
]
```
パスの指定をよく見ると、`:id`と書いています。
コロンを付けることで、 __「idは固定の文字列ではなく変数です」という意味を持たせる__ ことができます。

#### リンクを飛ばす(RouterLinkの利用)
```HTML:src/app/dashboard.component.html
<a *ngFor="let hero of heroes"
  routerLink="/detail/{{hero.id}}">
  {{hero.name}}
</a>
```
Heroオブジェクトのidを取得し、それをダブルカーリーグレイシスを使うことで、変数idに値を動的に埋め込んでいます。

これにより、「動的な値を含むURLへのリンクを飛ばす」を実現します。

#### 補足
ここまでは「idを渡す側」の視点でのテクニックでしたが、「idを受け取った側」のテクニックも記載します。

具体的には、
- idを受け取り、
- HEROES（Heroオブジェクトの配列のモックデータ）から該当のidにマッチするHeroオブジェクトを取得して返す

ということを行います。

`@angular/router`の`ActivatedRoute`を使います。

```TS:src/app/hero-detail/hero-detail.component.ts
import { ActivatedRoute } from '@angular/router';
...
export class HeroDetailComponent {
  constructor(
    private route: ActivatedRoute,
    private heroService: HeroService,
    private location: Location
  ) {}
  
  ngOnInit(): void {
    this.getHero();
  }
  
  getHero(): void {
    const id = 
  Number(this.route.snapshot.paramMap.get('id'));
    this.heroService.getHero(id)
      .subscribe(hero => this.hero = hero);
  }
}
```
`Number(this.route.snapshot.paramMap.get('id'))`では、URLのパラメタから`id`の値を取り出し（~/detail/13 というURLが指定されたのなら"13" という値が取得される）、それをnumberに変換しているという処理をしています。

（AIにも聞いてみた）
> このコードは、Angular のルーティング機能を使用して、URL パラメータから id を取得し、それを数値に変換する処理を行っています。以下にステップバイステップで説明します：
> 
> 1.this.route.snapshot:
> - ActivatedRoute の snapshot プロパティを使用して、現在のルートの静的なスナップショットを取得します。snapshot は、ルートが変わるたびに更新されるオブジェクトです。
> 
> 2.paramMap:
> - snapshot.paramMap は、現在のルートに関連するすべてのパラメータを含む ParamMap オブジェクトを返します。ParamMap は、ルートパラメータをキーと値のペアとして管理します。
>
> 3.get('id'):
> - paramMap.get('id') は、ParamMap から id パラメータの値を取得します。この値は文字列として返されます。
> 
> 4.Number(...):
> - Number(this.route.snapshot.paramMap.get('id')) は、取得した id パラメータの値を数値に変換します。これにより、id が数値として扱われるようになります。
> 
> まとめ
> このコードは、URL パラメータから id を取得し、それを数値に変換して id 変数に格納する処理を行っています。これにより、例えば /heroes/1 のような URL から 1 という数値を取得することができます。


### 遷移元の画面に戻る
`@angular/common`の`Location`を使います。

#### ボタン配置
```HTML:src/app/hero-detail/hero-detail.component.html
...
<button type="button" (click)="goBack()">Go Back</button>
```
「Go Back」と表示されているボタンを押下すると、goBack() メソッドが実行されます。
イベントバインディングの仕組みですね。

#### 戻る処理の実装
```TS:src/app/hero-detail/hero-detail.component.ts
import { Location } from '@angular/common';
...

export class HeroDetailComponent {
  ...

  constructor(
    ...,
    private location: Location
  ) {}

  ...

  goBack(): void {
    this.location.back();
  }
}

```
`@angular/common`の`Location`が提供している`back()` メソッドを実行することで、遷移元の画面に戻ることができます。

つまり、
- 「ダッシュボードからヒーロー詳細画面に遷移したのであれば、戻るボタンを押下すると、ダッシュボード画面が表示される」し、
- 「ヒーロー一覧画面からヒーロー詳細画面に遷移したのであれば、戻るボタンを押下すると、ヒーロー一覧画面が表示される」

ということです。