[javascript]Promiseを使ってconfirmライクな独自ポップアップを作る

js標準関数であるalertやconfirm


jsの標準関数に、alert,confirmといった関数があります。
これらは表示されている間にブラウザの他のUIが停止するという特徴があります。
特にconfirm関数はOKボタンとキャンセルボタンを備え、ユーザーに確認をとる使い方をしますので、確認するまで次の動作がブロックされるのは都合がよい場合も多いと思います。
しかしデザインのカスタマイズはできませんし、他のあらゆるjsがブロックされるのは都合が悪い場合もあります。また、ブラウザによっては挙動が異なる場合もあります。

そこで、独自のポップアップを作ってconfirm関数のような振る舞いをさせてみました。

独自ポップアップ


独自ポップアップはfixedなHTMLを作り、それの表示/非表示を切り替えることで実装します。
例えば以下のような形になるかと思います。

<div data-popup="my_confirm">
    <div
        class="hidden fixed top-0 left-0 right-0 bottom-0 w-full h-full bg-black opacity-50"
        data-popup_cancel
    >
        <div class="absolute top-0 left-0 right-0 bottom-0 m-auto rounded p-8 bg-white w-max h-max">
            <p data-message>確定します。よろしいですか?</p>
            <div class="flex justify-center items-center gap-[8px]">
                <div
                    class="rounded-full p-8 border border-solid border-blue-500 text-blue-500"
                    data-popup_cancel
                >キャンセル</div>
                <div
                    class="rounded-full p-8 border border-solid border-blue-500 bg-blue-500 text-white"
                    data-popup_ok
                >OK</div>
            </div>
        </div>
    </div>
</div>

表示/非表示の切り替え


上記のポップアップを表示・非表示する処理をjsで実装します。

function myConfirm(message) {
    return new Promise((resolve, reject) => {
        const myConfirmPopup = document.querySelector('[data-popup=my_confirm]');

        myConfirmPopup.querySelector('[data-message]').innerText = message;

        myConfirmPopup.querySelectorAll('[data-popup_cancel]').forEach(dom => dom.addEventListener('click', event => {
            myConfirmPopup.removeAttribute('style');
            resolve(false);
        }));

        myConfirmPopup.querySelectorAll('[data-popup_ok]').forEach(dom => dom.addEventListener('click', event => {
            myConfirmPopup.removeAttribute('style');
            resolve(true);
        }));

        myConfirmPopup.style.display = 'block';
    });
}

ここで、関数が呼び出されたらすぐにPromiseオブジェクトを返しています。こうすることで、ポップアップ表示→ユーザーの選択を待つ→ポップアップを閉じる→選択に応じた後続処理、を実行できます。
Promiseオブジェクトは、await演算子を使って完了を待つことができるオブジェクトです。Promiseの完了とは、上記のmyConfirm関数でいうところのresolve()が実行されたタイミングを指します。
このresolve()に、戻り値となるパラメータを渡すことで、awaitしている呼び出し元に処理の完了と結果の戻り値を伝えることができます。

独自ポップアップの利用


このmyConfirm関数を使う場合、例えばクリックイベントで以下のように使うことができます。

document.querySelector('.confirm_button').forEach(dom => {
    dom.addOnEventListener('click', async event => {
        const result = await myConfirm();
        if (result) {
            // OKボタンを押された場合の処理
        } else {
            // キャンセルボタンを押された場合の処理
        }
    });
});

これを実行すると、独自デザインでOKボタンとキャンセルボタンを備えたオリジナルのポップアップが表示され、OKボタンを押したかキャンセルボタンを押したかで処理を分岐させることができます。
その際、OK/キャンセルボタンを押すまで呼び出し元のプログラムを停止させておくことができ、confirm関数のように確実に結果を取得できます。
さらに重要な利点として、この処理は非同期で実行されるため、他のjsをブロックしません。confirm関数では画面がハングして見えてしまいますが、これならその問題を回避できます。

なお、呼び出し元の処理でawait演算子を使うために、クリックイベントのコールバック関数にasyncキーワードが付いています。
これにより関数の実行が非同期で行われるようになり、awaitキーワードが使えるようになります。(asyncでない関数の中でawaitキーワードを使おうとするとjsのエラーになってしまいます)

さいごに

このように、Promiseオブジェクトとasync/awaitを使うことで、独自のポップアップを表示することができました。
この手法は通常のconfirm関数を使うよりもメリットが多くあるため、積極的に活用していきたいと思いました。
ただし、Promiseオブジェクトを返す手法は単純にasync/awaitを利用する場合に比べて実装が複雑になるため、不必要に使いすぎないよう注意が必要だと思いました。