Bootstrap はバージョン5のβ版もリリースされ(2021/01/28 時点)、グリッドを使ったページスタイルなどでは今でも人気の UI フレームワームだと思っています:
2021012803


自分自身では、この Bootstrap の Modal 機能をよく使っています。いわゆるモーダルダイアログを実現するもので、別ページに飛ばすほどではない(すぐ元のページ内容に戻るのが容易な)画面をモーダルで表示し、モーダルが消えれば元の画面に戻ります。この間の画面遷移も実質ありません。

今回チャレンジしたのは「モーダルのモジュール化」です。具体的には以下の2点を実現する方法を考えました:

  1. テンプレートエンジンを使うなどしてモーダルダイアログを実現する HTML 部分を切り出し、複数の HTML ページから同じモーダルダイアログを呼び出せるようにする
  2. 呼び出し元の HTML ごとにモーダルダイアログ終了後の処理を変える

1. はテンプレートエンジン(例えば今回は Node.js と EJS を使うので EJS で説明します)の機能を使って HTML テンプレートを分割し、モーダルダイアログに関わる部分だけを別モジュールにする、というものです。これによって異なる呼び出し元(例えば page1 と page2)から同じモーダルダイアログ(mymodal)を利用できるようにします。
2021012808


2. は 1. の更なる応用です。例えば mymodal の中にテキストフィールドが1つあって、そこに任意の文字列を入力できるものとします。
2021012803


そして page1 から mymodal を呼び出した場合、mymodal が終了して元の page2 に戻ったら、mymodal に入力された文字列が "http" で始まる URL 文字列だった場合はその URL へジャンプ、URL 文字列でなかった場合は何もしない、ものとします。
2021012804
  ↓
2021012805


一方、page2 から mymodal を呼び出した場合、mymodal が終了して元の page2 に戻ったら、mymodal に入力された文字列をそのまま画面に表示するという条件分岐を行うものとします。
2021012806
  ↓
2021012807


このように、異なる呼び出し元から同じモーダルダイアログを呼び出すのですが、モーダルダイアログ終了後の挙動は呼び出し元ごとに変えたい、というのが 2 の要件です。
2021012809

2021012810


これらが難しい理由は2つあって、(1)まずモーダルダイアログは $('#modal').modal(); で起動するのですが、モーダルダイアログ自体が終了時に値を返すわけではないので、返り値を受け取ってその値を元に次の処理に移れるわけではない点。(2)そしてモーダルダイアログ終了時の処理を予め記述しておくことはできるのですが、その方法だと異なる呼び出し元ごとに終了後の挙動を変えることができないという点です。

これらの技術的挑戦は(2)でコールバック関数を使うことで((1)とまとめて)解決することができました。つまりモーダルダイアログが終了する際に呼び出し元ごとのコールバック関数が呼ばれるようにしておくことでモーダル終了後の処理を分離することができるようになり、分離できてしまえば(1)は普通に実現できるようになるのでした。


では実際のコードを見てみます。まず Node.js 側ですが、GET /page1 で page1.ejs を、GET /page2 で page2.ejs を呼び出すようなルーティングを定義しています:
var express = require( 'express' ),
    ejs = require( 'ejs' ),
    app = express();

app.use( express.Router() );

app.set( 'views', __dirname + '/views' );
app.set( 'view engine', 'ejs' );

app.get( '/page1', async function( req, res ){
  res.render( 'page1', {} );
});

app.get( '/page2', async function( req, res ){
  res.render( 'page2', {} );
});


var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );



次に page1.ejs 側ではモーダルダイアログを呼び出す画面まではこのテンプレート内で用意しますが、EJS の include 命令を使ってモーダルダイアログ mymodal.ejs を読み込んでいます。
<body>

<div class="container">
  <h1>Page1</h1>
</div>

<div class="container">
<button class="btn btn-primary" onClick="myOpen();">モーダルダイアログ</button>
</div>

<%- include('./mymodal', {}) %>

</body>


page2.ejs 側も同様です。自分たちの画面は自身のテンプレート内に記述して、共通で使うモーダルダイアログ部分だけを外部読み込みする形のテンプレートです。
<body>

<div class="container">
  <h1>Page2</h1>
</div>

<div class="container">
<button class="btn btn-primary" onClick="myOpen();">モーダルダイアログ</button>
</div>

<%- include('./mymodal', {}) %>

</body>


page1 および page2 から(ejs の include 命令で埋め込まれて)呼び出される側の mymodal.ejs です。Bootstrap の Modal による画面定義が行われていますが、今回はシンプルに id="text" のテキストフィールドを1つ配置しているだけです。ここまでで page1 および page2 の見た目に関する部分(HTML および CSS)は準備できました。
<div class="modal bd-example-modal-lg fade" id="myModal" tabindex="-1" role="dialog" aria-labbelledby="myModal" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered modal-lg">
    <div class="modal-content">
      <div class="modal-header">
        <h4 class="modal-title" id="myModalLabel">共有ダイアログ</h4>
      </div>
      <div class="modal-body" id="mymodal-body">
        <div>
          <input type="text" name="text" class="form-control" id="text"/>
        </div>
      </div>
      <div class="modal-footer btn-center">
        <button type="button" class="btn modal_button" data-toggle="modal" onClick="myModalClose();">OK</button>
      </div>
    </div>
  </div>
</div>


次にそれぞれの挙動に関する部分を見てみます。page1 の「モーダルダイアログを呼び出すボタン」をクリックすると myOpen() という関数が実行されるように定義されています。
<button class="btn btn-primary" onClick="myOpen();">モーダルダイアログ</button>


この関数は page1.ejs 内に定義されていて、その中では modalCallback という関数をパラメータにして myModalOpen() 関数が実行されています。
<script>
function myOpen(){
  myModalOpen( modalCallback );
}

function modalCallback( text ){
  if( text.startsWith( 'http' ) ){
    window.location.href = text;
  }else{
    //. Do nothing ?
  }
}
</script>


この myModalOpen() 関数は mymodal.ejs 内に定義されていて、その内容は後述します。また modalCallback 関数は myOpen のすぐ下に定義されています。実はこれがコールバック関数になっていて、mymodal.ejs の表示が終了したタイミングで、テキストフィールドに入力された値をパラメータにして実行されます。page1 ではこの値を調べて、"http" という文字で始まっていた場合は URL とみなし、その URL へ移動するような処理が記述されています。


同様にして、page2 の同部分ではコールバックされた関数内で alert() を使って、テキストフィールドに入力された値をそのまま表示するような処理が記述されています。
<script>
function myOpen(){
  myModalOpen( modalCallback );
}

function modalCallback( text ){
  alert( text );
}
</script>


最後に mymodal.ejs 内の JavaScript を見てみます。page1, page2 からモーダルダイアログ表示時に呼び出される myModalOpen() 関数内で、まずコールバック関数を変数に退避してから $('#myModal').modal() を実行してモーダルダイアログを画面に表示しています。
モーダルダイアログの終了ボタンがクリックされると myModalClose() 関数が実行されます。この関数内ではモーダルを非表示にする(戻す)ための処理が行われ、その後テキストフィールドに入力された値を取り出し、その値をパラメータに退避していたコールバック関数が実行されます。
<script>
var __callback = null;

function myModalOpen( func ){
  __callback = func;
  $('#myModal').modal();
}

function myModalClose(){
  $('body').removeClass( 'modal-open' );
  $('.modal-backdrop').remove();
  $('#myModal').modal( 'hide' );

  var text = $('#text').val();

  __callback( text );
}
</script>

こうすることでモーダルダイアログ終了時に、呼び出し元( page1 や page2 )で myModalOpen() 実行時に指定されたコールバック関数へ処理を戻すことができるようになります。またその際にダイアログ内のテキストフィールドの値も引き渡すことができるので、ダイアログで指定した値を使ってそれぞれの処理を行うことが可能になりました。


もう少し綺麗にモジュール化できるかもしれませんが、おおまかな考え方はこんな感じで実現できました。ソースコードは以下で公開しておきます:
https://github.com/dotnsf/bootstrap_modals