【javascript】非同期な自作 confirm を作ってみた

# はじめに

https://developer.mozilla.org/ja/docs/Web/API/Window/confirm

javascript には`confirm`というメソッドが用意されており、制御フローの条件として使用することができます。
ダイアログが表示され、「OK」「キャンセル」を選ぶことで処理を実行したり中断したりできる大変便利な機能です。

- コード例

```javascript:confirm
if (confirm("よろしいですか？")) {
  alert("ＯＫが押されました。");
} else {
  alert("キャンセルが押されました。");
}
```

- UI

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213286/08ab1779-e93c-ed9b-ade5-59eb15fd5c5e.png)

見ての通り簡素な UI で、選択肢の文言を変えることなどはできません。
そこで自作の confirm 関数を作成してみました。

## 仕様

- 任意の文言を受け付ける。
- 制御フロー内で条件として使用できる。
- 処理待ちを行う関係で非同期関数とする。

表面上の仕様は至ってシンプルなものになります。
ユーザーの入力を待ち、制御フローの中で条件として使用できる。
組み込みの confirm と同様の使い心地を実現できればそれで OK です。

- コード例

```javascript:confirmAsync
if (await confirmAsync("よろしいですか？")) {
  alert("ＯＫが押されました。");
} else {
  alert("キャンセルが押されました。");
}
```

## 設計

- `dialog`要素にて UI を作成する。
- confirmAsync 関数を実行することでモーダル形式でダイアログを表示する。
- OK ボタンをクリックしたら`Promise<true>`を返却してダイアログを閉じる。
- キャンセルボタンをクリックしたら`Promise<false>`を返却してダイアログを閉じる。

ダイアログを自作する場合、一般的には`div`要素が使われているのではないでしょうか。
今回はあえて`dialog`要素を使用していこうと思います。

https://developer.mozilla.org/ja/docs/Web/HTML/Element/dialog

ブラウザーの互換性に一抹の不安が残りますが、Google Chrome で動作するので良しとします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213286/163d5dd5-d317-27c4-aa73-6a067733c666.png" width="300px">

## 実装

### ダイアログ本体

```html
<dialog id="dialog">
  <p id="message"></p>
  <button id="button-ok">ＯＫ</button>
  <button id="button-cancel">キャンセル</button>
</dialog>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213286/dc485a39-a0b0-a2c1-6e49-c4fc700d090c.png)

装飾していない生の HTML です。

### confirmAsync 関数

自分で作っておいて言うのもアレですが、結構ややこしいのでいくつかの手順に分割して紹介します。

#### 手順①

- 表示メッセージの受け取り/設定を行う、
- ダイアログを表示する。
- 固定で`Promise<true>`を返却する。

```javascript:confirmAsync
const confirmAsync = async message => {
  document.getElementById("message").innerText = message;
  document.getElementById("dialog").showModal();

  return new Promise(resolve => resolve(true));
};
```

#### 手順②

- OK ボタン、キャンセルボタンにクリックイベントを設定する。
- OK ボタンがクリックされたら`Promise<true>`を返却してダイアログを閉じる。
- キャンセルボタンがクリックされたら`Promise<false>`を返却してダイアログを閉じる。

```javascript:confirmAsync
const confirmAsync = async message => {
  document.getElementById("message").innerText = message;
  document.getElementById("dialog").showModal();

  return new Promise(resolve => {
    document.getElementById("button-ok").addEventListener("click", () => {
      document.getElementById("dialog").close();
      resolve(true);
    });

    document.getElementById("button-cancel").addEventListener("click", () => {
      document.getElementById("dialog").close();
      resolve(false);
    });
  });
};
```

この時点でそれなりに昨日は完成していますが、もう少し工夫していきます。

#### 手順③

- クリックイベントを括りだす。
- OK ボタン用、キャンセルボタン用のイベントを作成する。  
（※次の手順で removeEventListener に設定するためです。）
- OK ボタンがクリックされたら`okEvent`を実行する。
- キャンセルボタンがクリックされたら`cancelEvent`を実行する。

https://ja.wikipedia.org/wiki/%E3%82%AB%E3%83%AA%E3%83%BC%E5%8C%96

```javascript:confirmAsync
const confirmAsync = async message => {
  document.getElementById("message").innerText = message;
  document.getElementById("dialog").showModal();

  return new Promise(resolve => {
    const eventBase = flag => () => {
      document.getElementById("dialog").close();
      resolve(flag);
    };
    const okEvent = eventBase(true);
    const cancelEvent = eventBase(false);

    document.getElementById("button-ok").addEventListener("click", okEvent);
    document.getElementById("button-cancel").addEventListener("click", cancelEvent);
  });
};
```

#### 手順④

- OK ボタンからクリックイベントを削除する。
- キャンセルボタンからクリックイベントを削除する。

```javascript:confirmAsync
const confirmAsync = async message => {
  document.getElementById("message").innerText = message;
  document.getElementById("dialog").showModal();

  return new Promise(resolve => {
    const eventBase = flag => () => {
      document.getElementById("dialog").close();
      document.getElementById("button-ok").removeEventListener("click", okEvent);
      document.getElementById("button-cancel").removeEventListener("click", cancelEvent);
      resolve(flag);
    };
    const okEvent = eventBase(true);
    const cancelEvent = eventBase(false);

    document.getElementById("button-ok").addEventListener("click", okEvent);
    document.getElementById("button-cancel").addEventListener("click", cancelEvent);
  });
};
```

ダイアログの実装手順は以上となります。

## 実行例

あとは実際に呼び出すだけです。
組み込みの confirm を呼び出すときと同様の感覚で実装できます。

```javascript
const handleClicked = async () => {
  if (await confirmAsync("よろしいですか？")) {
    alert("ＯＫが押されました。");
  } else {
    alert("キャンセルが押されました。");
  }
};
```

```html
<button onclick="handleClicked()">確認</button>
```

![confirm.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213286/9cd90b9e-7af2-3bd2-436f-189c36437aa0.gif)

## 装飾など

UI 自体はただの HTML なので、通常通り css で装飾することができます。
以下は Bootstrap を用いた例です。

![confirm2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213286/f4da8b58-f249-8b9d-fa90-e20d2fb9bdcf.gif)

https://getbootstrap.jp/