AngularのAuxillary Routes（副次パス）で綺麗にモーダルをパスに含める方法

はいさい！オースティンやいびーん！

# 概要

Angular Routerを使って、モーダル、ダイアログ、サイドバーメニューの画面遷移をパスに含める方法を紹介します！

この機能を**Auxillary Routes**といいます。日本語だと **「副次パス」** と訳すのでしょうか？

### 背景

モーダル、ダイアログはUIとして頻繁に使う便利な手法です。

しかし、モーダル内の遷移があると一気に複雑になりがち。

しかも、ページを更新したりすると、その遷移の情報が消えてしまい、とても不便です。

そこでAngularが解決してくれるのです。それがAuxillary Routesです。

Angularはいつも、何か手法を持っているので、Angularを信じましょう。

### 関連記事

- [Angularの正式ドキュメント](https://angular.io/guide/router-tutorial-toh#displaying-multiple-routes-in-named-outlets)

# 実装説明

筆者が作っている記録アプリのモーダルを副次的パスに移動させたいと思います。そうすると、書きかけの情報があったら閉じるときに確認モーダルを出せるようにしたいんです。

https://github.com/tronicboy1/farm-logger

https://farm-logger.web.app/ja/auth

一緒に実装しながら解説していきます！行くどー！

## 現状のRouting



このアプリケーションのapp-routing.module.tsは以下のようです。

```typescript:src/app/app-routing.module.ts
const routes: Routes = [
  { path: 'auth', component: AuthComponent },
  { path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
  {
    path: 'farms/:farmId',
    component: FarmComponent,
    canActivate: [AuthGuard, MemberGuard],
    children: [
      { path: 'manage', component: ManageComponent, data: { title: '農園管理' } },
      { path: 'areas', component: AreasComponent, data: { title: '区域一覧' } },
      {
        path: 'areas/:areaId',
        component: AreaComponent,
        children: [
          {
            path: 'trees',
            loadChildren: () => import('./page-modules/tree-pages/tree-pages.module').then((m) => m.TreePagesModule),
          },
          {
            path: 'strawberries',
            loadChildren: () =>
              import('./page-modules/strawberry-pages/strawberry-pages.module').then((m) => m.StrawberryPagesModule),
            canActivate: [CanLoadStrawberryGuard],
          },
          { path: 'fertilizer', component: FertilizerComponent },
          { path: 'cropdust', component: CropdustComponent },
          { path: '', component: AreaIndexComponent, pathMatch: 'full' },
        ],
      },
      { path: 'environment', component: EnvironmentComponent },
      { path: '', redirectTo: 'areas', pathMatch: 'full' },
    ],
  },
  { path: '', redirectTo: 'auth', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent },
];
```

本記事では、記録追加、植物追加のモーダルをAuxillary Routeに落としていきたいと思います。

## モーダルのルートを追加する

上記のルートの`areas`の中に、`children`を追加してモーダルのパスを入れましょう。

Auxillary Routesにするためには、`outlet`のプロパティを指定します。

```typescript:src/app/app-routing.module.ts
/***/
    path: 'farms/:farmId',
    component: FarmComponent,
    canActivate: [AuthGuard, MemberGuard],
    children: [
      { path: 'manage', component: ManageComponent, data: { title: '農園管理' } },
      {
        path: 'areas',
        component: AreasComponent,
        data: { title: '区域一覧' },
        // ここ！
        children: [{ path: 'new', outlet: 'modals', component: NewAreaModalComponent }],
      },
/***/
```

普通にパスを指定するのとほぼ同じですが、`outlet`を`modals`に指定しています。

これはAuxillary Routeをどこの`<router-outlet>`に出すかを設定する効果があるのです。

>どこの`<router-outlet>`

ここで顔をしかめた方は続きを読んでいただければと思いますが、そう、複数の`を`<router-outlet>`を作れるのです。

## Auxillary Route用の`<router-outlet>`を追加する

`AreasComponent`の`children`にAuxillary Routeとして新規作成モーダルを追加したのですが、その`<router-outlet>`も追加しないといけます。

`name="modals"`という属性を付与していることにご注目を！

```html:src/app/pages/farm/areas/areas.component.html
...

<router-outlet name="modals"></router-outlet>

...
```

これでapp-routing-moduleで指定した`outlet: 'modals'`がDOMにレンダーされるようになります！

## Auxillary Routeに遷移するためのリンクを追加する

さらに、そこに遷移できるように`routerLink`も追加しましょう。

Auxillary Routeの場合、パスの指定がちょっと独特！

```html
  <a class="card round" [routerLink]="[{ outlets: { modals: ['new'] } }]">
    <svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0,0,48,48">
      <path d="M22.5 38V25.5H10v-3h12.5V10h3v12.5H38v3H25.5V38Z" />
    </svg>
  </a>
```

細かくみていきます！

```javascript
[ // 通常のrouter.navigate([])で使う配列
 { // 文字列ではなくオブジェクト！
  outlets: {　// <router-outlet name='...'>の子たち
　　　　　modals:　 // 'modals'と指定した副次的パスを指定
    ['new'] // 'modals'の副次的パスのどのパスに行くかを指定
　　　} 
 }
]
```

本流のパスとはかけ離れた遷移ロジックを持つAuxillary Routeなので、リンク指定が若干ややこしい！

クリックしてみると無事にモーダルは表示されます！

![ezgif.com-video-to-gif (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/372bc7d7-f90a-f2f9-9db4-fe1262022067.gif)


## Auxillary Routeを閉じるための遷移ロジックを追加する

モーダルは開けたのですが、どうやって閉じましょうか？

ちょっとした技が必要です。 `NewAreaModalComponent`で見てみましょう！

```typescript:src/app/pages/farm/areas/new-area-modal/new-area-modal.component.ts
@Component(/**/)
export class NewAreaModalComponent {
  private router = inject(Router);
  private route = inject(ActivatedRoute);

  handleModalClose() {
    this.router.navigate([
      { outlets: { modals: null } } // modalsをnullにする
    ],
    { relativeTo: this.route.parent }　// ここが重要！
    );
  }
}
```

最後の`relativeTo`プロパティが非常に重要で、これがないと親部品のRouterの中の`modals`を指定できなくなります。

先ほども言いましたが、この部品は、独自の遷移を持ったので、ActivatedRouteも親から枝分かれした子パスになります。

ちなみに同じことを`routerLink`で実現するためには`../`を使うといいですよ！

```html:src/app/pages/farm/areas/new-area-modal/new-area-modal.component.html
<base-modal [show]="true" i18n-modal-title modal-title="Add a New Area" (modal-closed)="this.handleModalClose()">
  <p [routerLink]="['../', '../', { outlets: { modals: null } }]">CLOSE</p>
  <app-new-area-form></app-new-area-form>
</base-modal>
```

試してみるとどれもうまくいきます！（ここまでの長い試行錯誤を省いていますが:sweat_smile:）

![ezgif.com-video-to-gif (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/40a8811b-d57b-80a7-e399-e876c4e1287b.gif)

# まとめ

いかがでしょうか？筆者と同様にワクワクしましたか？

現状、仕事で負債回収していて、こういうダイアログを全て独自実装して、閉じる時に「書きかけの情報があれば遷移を止める」のようなロジックがたくさんあるのですが、独自実装なので可読性がゼロに近く、バグが大量に起きていて、それをどうにか直せないかと調べたところ、このAuxillary Routesの機能に出会ったのです。

Angularは色々と嫌われていますが、i18n（多言語機能）といい、モジュールといい、Angular Routerといい、他のフレームワークにないありがたい機能が山盛りなので、過小評価されているのは残念です。筆者がAngularに同情してしまいます。

しかし、そのAngularでも最近は人気を取り戻すために、始めるための学習ハードルを下げようといくつか改善もしています。

最近、Reactive Primitive（React.jsならuseState）をAngularに入れて、パフォーマンスの悪いZone.jsを廃止していくという方針が決まりそうです。

それができれば、RxJSとの相性も良くなるし、パフォーマンスも良くなるので待ち遠しいです！