【Angular】ルーティングするダイアログを作った

需要は無いかも知れませんが、ルーティングするダイアログを作りましたので残しておきます

## どういうこと？
普通ダイアログは開いてもURL変わりませんよね
angular materialのダイアログも変えられるようなものはありませんでした
ダイアログを開いた時閉じた時にルーティングされURLが変わるものを作ろうと言う話です

例えばログインのダイアログを出したときは
`http://hogehoge/` → `http://hogehoge/login/` に変わるようにします。
閉じたときは
`http://hogehoge/login/` → `http://hogehoge/` に戻ります

## なんのため？
・ダイアログを閉じる時にブラウザのバックが使える（操作性が向上する）
・ルーティングされているため、URL直で書いてダイアログが表示された状態で出てくる
・つまりブラウザリロードしてもダイアログが消えない
・ダイアログで遅延ローディングが使える！

ダイアログに通常のサイトのページ単位の内容があり、更に何個も重ねて行くようなアプリを作成したいと思いました
ダイアログと言うよりOSなどのウインドウシステムに近いUIです
その際ルーティングを使えば操作性がよくなるのではないかと思い作り始めました

## ダイアログ表示用コンポーネントの作成
先程出した例のように開いた時はURL上で1階層下に遷移し、閉じたときは1階層上に遷移する仕様で作ります
ダイアログはみんな大好きなangular materialのものを使います

```dialog.component.ts
export enum ModalSize {
    large,
    medium,
    small
}

export interface DialogOption {
    size: ModalSize;
}

@Component({
    selector: 'app-dialog',
    templateUrl: './dialog.component.html',
    styleUrls: ['./dialog.component.scss']
})

export class DialogComponent implements OnInit, OnDestroy {

    private routerUrl: string;
    private dialogRef: MatDialogRef<any, any>;

    constructor(private overlay: Overlay, private route: ActivatedRoute, private router: Router, private dialog: MatDialog) {
    }

    ngOnInit() {
        this.route.data.subscribe(data => {
            this.openModal(data.dialogComponent, data.dialogOption);
        });
    }

    ngOnDestroy() {
        this.dialogRef.close();
    }

    openModal(dialogComponent: ComponentType<any>, dialogOption: DialogOption) {
        this.routerUrl = this.router.url;
        this.dialogRef = this.dialog.open(dialogComponent, {
            width: this.dialogSizeToPixel(dialogOption.size),
            closeOnNavigation: false,
            autoFocus: true,
            scrollStrategy: this.overlay.scrollStrategies.block()
        });
        this.dialogRef.beforeClose().subscribe(_ => {
            this.closeAndReturnRoute();
        });
    }

    private dialogSizeToPixel(modalSize: ModalSize): string {
        switch (modalSize) {
            case ModalSize.large:
                return '1024px';
            case ModalSize.medium:
                return '800px';
            default:
                return '640px';
        }
    }

    private closeAndReturnRoute() {
        const routerPath = this.router.url;
        // 最初に開いたときと異なるUrlの場合は何もしない
        // 主にルーティングによりUrlが変わった場合
        if (this.routerUrl !== routerPath) {
            return;
        }
        this.dialogRef.close();
        // https://github.com/angular/angular/issues/17957
        // 仕方ないので自力で最後のスラッシュ以下を消す
        const parentRoutePath = routerPath.substr(0, routerPath.lastIndexOf('/'));
        this.router.navigate([parentRoutePath], {relativeTo: this.route});
    }
}
```

```dialog.component.html
<router-outlet></router-outlet>
```

## 解説

``` ts
    ngOnInit() {
        this.route.data.subscribe(data => {
            this.openModal(data.dialogComponent, data.dialogOption);
        });
    }
```

ngOnInitでコンポーネントが作成された時に、ルーティングのパラメーターから表示するダイアログの種類を渡して表示します

```ts
            closeOnNavigation: false,
```
angular materialのダイアログはデフォルトではブラウザの履歴が変わった場合に閉じてしまいます
この挙動があるとルーティングした途端ダイアログが消えてしまい表示されません
closeOnNavigationをfalseにすることにより無効化します

```ts
        if (this.routerUrl !== routerPath) {
            return;
        }
```
このダイアログが閉じられるパターンは2個あります
一つは閉じるボタンが押された場合やバックドロップがクリックされた場合などです
この場合はURLの階層を1個上に遷移させる必要があります
もう一つはルーティングが変更された場合です（ngOnDestroyを通ります）
この場合はすでにURLが変更されているためURLを変える必要がありません
この2つのパターンを開いた時のURLと現在のURLとが同じかどうか比較することで判断します

```ts
        const parentRoutePath = routerPath.substr(0, routerPath.lastIndexOf('/'));
        this.router.navigate([parentRoutePath], {relativeTo: this.route});
```
遅延ローディングを使った場合には相対パスは使えないようです
URLの末尾の/を消すことにより一個前のルーティングに移動します
参考:https://github.com/angular/angular/issues/17957


```dialog.component.html
<router-outlet></router-outlet>
```
2個以上ダイアログを開く時のために新しいrouter-outletを作ります

## 使い方

dialogを開くコンポーネントを作成します
`<router-outlet>`を用意し、以下のようなルーティングを定義します

```ts
        {
            path: 'setting',
            component: DialogComponent,
            data: {dialogComponent: SettingComponent, dialogOption: {size: ModalSize.large}},
            children: [{
                path: 'image-upload',
                component: DialogComponent,
                data: {dialogComponent: ImageUploadComponent, dialogOption: {size: ModalSize.small}},
            }]
        }
```
ルーティングパラメーターの`dialogComponent`で開くダイアログのコンポーネントを指定します

## 出来上がったもの

ブラウザバックで閉じる
![nzsyd-cid3g.gif](https://qiita-image-store.s3.amazonaws.com/0/252031/47e35990-cb9b-1bf4-8639-77f6ded92e6a.gif)

リロードしてもダイアログが残ったままになる
![ym6lt-mhk2r.gif](https://qiita-image-store.s3.amazonaws.com/0/252031/6cacc66b-6963-0843-80d7-bdade88edb80.gif)

5ボタンマウスやスマホだと操作が気持ち良くなります