[Angular]ページ遷移にアニメーションをつける

## 概要

- ページ遷移する時にスライドするようなアニメーションを入れてみます
- Angularにアニメーションの機能も搭載されているのでそれを使います

## サンプル

### ベースのアプリ作成

- まずは適当な3つのページを用意します

```
% tree src/app
src/app
├── animations.ts
├── app-routing.module.ts
├── app.component.ts
├── app.module.ts
├── page1
│   └── page1.component.ts
├── page2
│   └── page2.component.ts
└── page3
    └── page3.component.ts
```

- 各ページは背景色が違うだけでほとんど同じ内容です

```ts:src/app/page1/page1.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-page1',
  template: `
    <style>
      main {
        height: 100vh;
        width: 100vw;
        background-color: lightblue;
        display: flex;
        justify-content: center;
        align-items: center;
        flex-direction: column;
      }
    </style>
    <main>
      <p>page1 works!</p>
      <p>
        <span>Prev</span> | <a routerLink="/page2">Next</a>
      </p>
    </main>
  `,
})
export class Page1Component {}
```

- styleが多いですが真ん中中央寄せにしているだけです

![before.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/efe736f2-d89c-e07f-5bb7-3ef5ff46b3d6.gif)

### アニメーションを追加

- `app.module.ts`にアニメーションを使うための`BrowserAnimationsModule`を追加します

```ts:src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
// importを追加
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { Page1Component } from './page1/page1.component';
import { Page2Component } from './page2/page2.component';
import { Page3Component } from './page3/page3.component';

@NgModule({
  declarations: [AppComponent, Page1Component, Page2Component, Page3Component],
  // BrowserAnimationsModuleを追加
  imports: [BrowserModule, BrowserAnimationsModule, AppRoutingModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}
```

- routingの設定に識別子を追加します
- これはこの後で、どのページからどのページに遷移した時にどのアニメーションを適用する、といった感じの定義をする時に使います

```ts:src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Page1Component } from './page1/page1.component';
import { Page2Component } from './page2/page2.component';
import { Page3Component } from './page3/page3.component';

const routes: Routes = [
  { path: '', redirectTo: 'page1', pathMatch: 'full' },
  // それぞれdataプロパティを追加
  { path: 'page1', component: Page1Component, data: { animation: 'Page1' } },
  { path: 'page2', component: Page2Component, data: { animation: 'Page2' } },
  { path: 'page3', component: Page3Component, data: { animation: 'Page3' } },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}
```

- `<router-outlet>`を定義している部分に処理を追加します

```ts:src/app/app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  // <router-outlet>をdivで囲ってアニメーションの設定を追加
  template: `
    <div [@routeAnimations]="prepareRoute(outlet)">
      <router-outlet #outlet="outlet"></router-outlet>
    </div>
  `,
})
export class AppComponent {
  title = 'ng-animation-sample';

  prepareRoute(outlet: RouterOutlet) {
    return (
      outlet && outlet.activatedRouteData && outlet.activatedRouteData.animation
    );
  }
}
```

- `routeAnimations`はこの後定義します
- `prepareRoute`実行時に`<router-outlet>`の情報を渡すことで今表示しているページの情報を取得できます
    - `outlet.activatedRouteData.animation`で`app-routing.module.ts`で設定した`animation: 'Page1'`といったデータを取り出してreturnしています
- 最後に`animation.ts`にアニメーションの情報を定義します

```ts:src/app/animation.ts
import {
  trigger,
  animate,
  transition,
  query,
  style,
  group,
  animateChild,
} from '@angular/animations';

export const slideInAnimation = trigger('routeAnimations', [
  transition('Page1 => Page2, Page2 => Page3', [
    style({ position: 'relative' }),
    query(':enter, :leave', [
      style({
        position: 'absolute',
        top: 0,
        right: 0,
        width: '100%',
      }),
    ]),
    query(':enter', [style({ right: '-100%' })]),
    query(':leave', animateChild()),
    group([
      query(':leave', [animate('300ms ease-out', style({ right: '100%' }))]),
      query(':enter', [animate('300ms ease-out', style({ right: '0%' }))]),
    ]),
    query(':enter', animateChild()),
  ]),
  transition('Page3 => Page2, Page2 => Page1', [
    style({ position: 'relative' }),
    query(':enter, :leave', [
      style({
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%',
      }),
    ]),
    query(':enter', [style({ left: '-100%' })]),
    query(':leave', animateChild()),
    group([
      query(':leave', [animate('300ms ease-out', style({ left: '100%' }))]),
      query(':enter', [animate('300ms ease-out', style({ left: '0%' }))]),
    ]),
    query(':enter', animateChild()),
  ]),
]);
```

- この内容は[公式サイトの内容](https://angular.jp/guide/route-animations#%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E5%AE%9A%E7%BE%A9)をベースにしています
- `transition`の定義が2つあります
    - `transition('Page1 => Page2, Page2 => Page3'`はPage1からPage2への遷移かPage2からPage3への遷移の際に発動します
    - `transition('Page3 => Page2, Page2 => Page1'`はPage3からPage2への遷移かPage2からPage1への遷移の際に発動します
    - これは先程の`[@routeAnimations]="prepareRoute(outlet)"`の部分でページの情報を随時拾っているので判断できるというわけです
- 最後にこの定義を反映させます

```ts:src/app/app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
// importを追加
import { slideInAnimation } from './animations';

@Component({
  selector: 'app-root',
  template: `
    <div [@routeAnimations]="prepareRoute(outlet)">
      <router-outlet #outlet="outlet"></router-outlet>
    </div>
  `,
  // アニメーションの設定を追加
  animations: [slideInAnimation],
})
export class AppComponent {
  title = 'ng-animation-sample';

  prepareRoute(outlet: RouterOutlet) {
    return (
      outlet && outlet.activatedRouteData && outlet.activatedRouteData.animation
    );
  }
}
```

- `animations: [slideInAnimation]`の部分でアニメーションを適用しています
- これで完成しているはずなので動かしてみます

![after.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/a1ac2ce6-58a9-cf99-bd0e-47d0eb5c312e.gif)

- うまくいきました！

### 感想

- Angular以外のライブラリを使っている場合アニメーションをやろうとするとライブラリ選定から悩むことになりますが、3rdパーティライブラリを使わずできちゃうところがいいですね
- 公式サイトのドキュメントも親切でした
    - https://angular.jp/guide/route-animations