内藤 裕二/ 2024年 8月 22日/ 技術

こんにちは!内藤です!
北海道はお盆を過ぎると、夜の気温がぐっと下がります。
台風も過ぎ、昼間の湿度も下がってようやく「北海道の夏」らしい気候になりました。

今回は、長年避けて通ってきたフロント側でコードを書く機会があったので、まとめます。
BootstrapのModalのお話です。
Modalを重ねて表示する記事はよく目にしますが、Modalの切り替えでネストさせる必要があったので実験してみました。

やりたいこと

Modalのネストって何言ってるかわからないですよね・・・
CodePenで書いてみました。

https://codepen.io/y-naito-nt/pen/gONeLVX

  • 1つめのModal上のボタンを押すと、2つめのModalが表示される
  • 2つめのModalを閉じると、再度1つめのModalが表示される

上記サンプルでは2つまでのネストですが、いくつ重ねても大丈夫(なはず)です。

実現方法

startModal関数にModalのIDと、表示する際の初期化関数およびその引数を渡すと、うまいことやってくれます。
コード見てもらえれば一目瞭然なのですが、いちおう解説します。

表示対象のModal要素の存在チェック

そもそも表示しようとするModalが存在するかチェックします。

  // Modal要素が存在しなかったら何もしない
  const modal_elm = document.getElementById(modal_id);
  if (!modal_elm) {
    return;
  }

表示するModalの初期化関数を設定する

「shown」イベントに初期化関数を設定します

  // 表示対象のModalを生成・取得する
  const modal = bootstrap.Modal.getOrCreateInstance(modal_elm);
  // 対象のModalが表示された時に、初期化関数を実行する
  setOneShotEventHandler(modal._element, "shown.bs.modal", function () {
    if (initFunc) {
      initFunc(...params);
    }
  });

単純にaddEventListenerしちゃうと、実行するたびに同じようなイベントハンドラがどんどん設定されちゃいそうなので、実行した後自身を削除するイベントハンドラにするために、setOneShotEventHandlerという関数を作成しました。
この辺はJavaScriptのエンジン側でうまいことやってくれるのかもしれません。

setOneShotEventHandler関数は、下記コード。

// 一度だけ実行するイベントハンドラを設定する
function setOneShotEventHandler(target, eventName, handler) {
  // 実行後に自身を解除するイベントハンドラを作成
  const oneShotHandler = function (e) {
    // 自身を解除
    target.removeEventListener(eventName, oneShotHandler);
    // 本来実行したかった関数を実行
    handler(e);
  };
  target.addEventListener(eventName, oneShotHandler);
}

設定したい関数オブジェクトを、実行したらremoveEventHandlerするようなラッパー関数で包んで、イベントハンドラに設定します。

現在表示中のModalを取得する

getCurrentModalという関数にしています。

// 現在表示中のModalインスタンスを取得する
function getCurrentModal() {
  const modal_elm = document.querySelector(".modal.show");
  if (!modal_elm) {
    return null;
  }
  return bootstrap.Modal.getInstance(modal_elm);
}

CSSクラスに「modal」「show」両方を持つエレメントを取ってきているだけです。

表示中のModalが存在する場合

現在表示中のModalの「hidden」イベントに、本来表示したいModalを表示するようなイベントハンドラを設定します。
また、本来表示したいModalの「hidden」イベントに、現在表示中のModalを再表示するようなイベントハンドラを設定します。

この時も、一回実行したらイベントハンドラが削除されるようにしておきます。
setOneShotEventHandler関数を使用する)

上記が設定できたら、現在表示中のModalを非表示にします。

  // 現在表示中のModalを取得
  const currentModal = getCurrentModal();
  if (currentModal) {
    // 現在表示中のModalがある場合
    // イベントハンドラを設定する
    setOneShotEventHandler(
      currentModal._element,
      "hidden.bs.modal",
      function () {
        // 本来表示したいModalを表示する
        modal.show();
      }
    );
    setOneShotEventHandler(modal._element, "hidden.bs.modal", function () {
      // もともと表示されていたModalを表示する
      currentModal.show();
    });
    // 現在表示中のModalを非表示にして、イベントの連鎖を開始
    currentModal.hide();
  }

これで、下記が実現できるようになります。

  • 現在表示中のModalが非表示になった段階で、本来表示したいModalが表示される
  • 本来表示したいModalが非表示になった段階で、現在表示中のModalが再表示される

表示中のModalが存在しなかった場合

本来表示したいModalを表示するだけです。

制限事項

ネストしたModalは、必ずスタックの深い方から閉じていく必要があります。
そうしないと、setOneShotEventHandler関数の仕様上、イベントハンドラが残ってしまいます。

終わりに

フロント側は不慣れなので、試行錯誤しながら実装しています。
コードの全体は、CodePenからどうぞ!

https://codepen.io/y-naito-nt/pen/gONeLVX