【Angular Tips】画面遷移時のスクロールの制御について

# はじめに
AngularでSPA開発をしていた際に、以下の様な事象に遭遇しました。

　スクロールが発生するような縦長の画面を表示
→ページ下部にあるの次画面へのボタンを押下
→スクロール位置が変わらぬまま次画面が表示される :rolling_eyes:？

これまでSPAでない通常のWebアプリケーションの構築経験しかない私にとって、画面遷移時にページのトップが表示されないというのは、少々違和感のあるものでした。
調べてみると、これがAngularデフォルトの挙動であることが分かりました。
画面遷移時にはページのトップが表示されるようにしたかったので、実際に解決した内容について共有したいと思います。

### 環境
- Angular：v8.0
- TypeScript：v3.4.5


# 画面遷移時のスクロール制御設定を変更する
スクロールの制御については、Routerのオプション(`scrollPositionRestoration`)で変更することが可能です。
※なお、この機能はAngular v6.1以降の機能なので、それより前のバージョンを使用している場合はバージョンアップが必要です。
`scrollPositionRestoration`には以下の値を指定することが可能です。

- `disabled`(デフォルト設定)：何もスクロール制御を行いません(スクロール位置が次画面に引き継がれる)。
- `top`：画面遷移時にトップ(座標でいうx=0,y=0)へとスクロールします。
- `enabled`：画面遷移時にURLに`#hoge`のようなフラグメントが付いている場合、対応するIDを持つ要素があればそこまでスクロールします。※この機能を有効にするには後述の`anchorScrolling`の有効化も必要です。  
特にフラグメントがなければ、トップへとスクロールします。  
また、ブラウザバック時にスクロール位置の復元(前画面でスクロールしていた箇所までスクロール)を行います。  
将来的にはこちらがデフォルト設定になるみたいです。

私が先の事象に直面したのは、この設定を何も変更しておらず、デフォルトの`disabled`が適用されていたためというわけでした。
正直、`disabled`だとユーザビリティがよくない局面が多そうなので、なぜこれがデフォルトなのだろうという感じです...
今回は、ブラウザバックを含め画面遷移時にはトップへスクロールしてほしかったので、以下のように`top`を指定することで問題が解決しました。

```typescript:app-routing.module.ts

import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";

const routes: Routes = [
  { path: "hoge", loadChildren: () => import("./hoge/hoge.module").then(m => m.HogeModule) },
  { path: "fuga", loadChildren: () => import("./fuga/fuga.module").then(m => m.FugaModule) }
];

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

## 補足

### 特定の要素へスクロールする

画面遷移時に特定の要素へとスクロールさせる場合、`scrollPositionRestoration`を`enabled`にした上で、`anchorScrolling`に`enabled`を指定する必要があります。
`anchorScrolling`に指定できるのは、`enabled`と`disabled`(デフォルト)の2つのみです。

```typescript:app-routing.module.ts
@NgModule({
  imports: [RouterModule.forRoot(routes, { scrollPositionRestoration: "enabled", anchorScrolling: "enabled" })],
  exports: [RouterModule]
})
export class AppRoutingModule {}
```
なお、フラグメントを付与しての画面遷移の実装は以下の通り。

・テンプレート側で画面遷移を実装する場合。

```html:some.component.html
<a routerLink="/hoge" fragment="fuga">Some Link</a>
```
・コンポーネント側で画面遷移を実装する場合。

```typescript:some.component.ts
someFunc () {
  this.router.navigate(["hoge"], {fragment: "fuga"});
}
```

### スクロール位置を調整する

アプリに高さ固定のヘッダが存在する場合、前の方法で特定の要素へとスクロールをすると、要素がヘッダの裏に隠れて表示されなくなってしまうことが起こり得ます。
そのような場合には、`scrollOffset`という設定を変更することで、スクロール位置を調整することができます。
※このオプションは`scrollPositionRestoration`と`anchorScrolling`がともに`enabled`の場合に利用できます。
例えば、以下のように設定することで、画面上部に高さ50pxの固定ヘッダが存在した場合に、対象の要素を隠れない状態で表示することが可能です。

```typescript:app-routing.module.ts
@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      scrollPositionRestoration: "enabled",
      anchorScrolling: "enabled",
      scrollOffset: [0, 50]
    })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}
```

ただし、これを指定するとすべての画面遷移に対してスクロール位置の調整が適用されるので、一部の画面にはヘッダがない、またはヘッダの高さが統一されていないという場合には別の手段を考える必要がありそうです。

# まとめ
Angularの画面遷移時のスクロール制御についてまとめてみました。
設定をちょっといじるだけで、スクロールの挙動を適用できるのは、とても手軽だと感じました。

一方で、SPAの構築経験がないと、スクロールの制御というのはあまり意識しにくい部分なのかなと思いました。
通常のWebアプリケーションでは、画面遷移時にページ全体が生成されるため、特別な手をいれない限りは、ページトップが表示されるのであまり意識をしません。
実際私もSPA開発が始まるまでは気づけませんでした...
開発が始まる前に、使用するフレームワークではどのようなスクロールの制御が可能で、どういった方針にするのか、事前に決めておく必要があるということを学びました。

### 参考
Angular公式ガイド
https://angular.jp/api/router/ExtraOptions#scrollPositionRestoration