カテゴリー
コンピューター

Laravel 6 に構築した Vue.js 3 SPA で、 Bootstrap 4 のモーダルをラッパー SFC (シングルファイルコンポーネント) 化した例

はじめる前に

本投稿は時系列としては Laravel 6 、 Vue.js 3 の SPA で npm run production が失敗する問題を修正する – oki2a24 の続きとなります。

作ったものの構成

  • モーダルを呼び出すページを用意する。
  • モーダルに表示するコンテンツ部分を 1 つの別のコンポーネントとして作成する。このモーダルコンテンツコンポーネントから、基底となるモーダルコンポーネントを呼び出す。
  • モーダルの外側を 1 つのコンポーネントとして作成する。基底モーダルコンポーネントと呼ぶ。ここが Bootstrap 4 のモーダルをラップした部分。

使うときのポイント

  • モーダルがマウントされるのは、モーダル呼び出し元のページがマウントされる時と同じタイミングとなる点に注意。モーダル表示のタイミングで実行したいことがあるならば、モーダルのイベントを使う必要がある。
  • モーダル SFC を使うコンテンツコンポーネントで設定した HTML の id は props で与えないような構造としてしまってもよいと思った。ただし、そうするとモーダルを呼び出す側がコンテンツコンポーネントの内部に書かれている HTML id を調べに行く必要が出てくるので、やりづらさを感じる。

完成したコード

Vue Router でモーダルを呼び出す側のページを定義しました。

$ git diff laravel/resources/js/router/index.js
diff --git a/laravel/resources/js/router/index.js b/laravel/resources/js/router/index.js
index a1b914d..bc9d99a 100644
--- a/laravel/resources/js/router/index.js
+++ b/laravel/resources/js/router/index.js
@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from "vue-router";
 import ExampleComponent from "../components/ExampleComponent.vue";
 import SampleDropzone from "../views/SampleDropzone.vue";
 import SampleFlatpickr from "../views/SampleFlatpickr.vue";
+import SampleModal from "../views/SampleModal.vue";
 import SampleVueFlatpickr from "../views/SampleVueFlatpickr.vue";
 import SampleSelect2 from "../views/SampleSelect2.vue";
 import Vue3Dropzone from "../views/Vue3Dropzone.vue";
@@ -34,6 +35,11 @@ const routes = [
     name: "SampleFlatpickr",
     component: SampleFlatpickr,
   },
+  {
+    path: "/sample_modal",
+    name: "SampleModal",
+    component: SampleModal,
+  },
   {
     path: "/sample_select2",
     name: "SampleSelect2",
$

モーダルを呼び出すページ、 laravel/resources/js/views/SampleModal.vue 、です。

<template>
  <div class="container">
    <h1>モーダルのサンプル</h1>
    <button
      type="button"
      class="btn btn-primary"
      data-toggle="modal"
      data-target="#sampleModal1"
    >
      モーダルサンプル1
    </button>
    <sample-modal-content-1 :id="'sampleModal1'"> </sample-modal-content-1>
  </div>
</template>

<script>
import SampleModalContent1 from "./SampleModalContent1.vue";

export default {
  name: "SampleModal",
  components: { SampleModalContent1 },
  setup() {},
};
</script>

laravel/resources/js/views/SampleModalContent1.vue です。モーダルのコンテンツ部分です。このコンポーネントから、基底となるモーダルコンポーネントを呼び出しています。

  • setup 内の処理は全て動作を確認するためのログ出力となる。これにより、次のことがわかる。
    • 呼び出し元ページがマウントされた時に、このモーダルもマウントされる。
    • モーダル表示時に実行したい処理がある場合は基底モーダルの show, shown イベントを購読する必要があり、モーダル非表示時に実行したい処理がある場合は基底モーダルの hide, hidden イベントを購読する必要がある。
  • コンテンツモーダルで id を props として受け取るようにしたが、このモーダルを 1 つのページで複数回呼び出すことがなければ不要と思う。
  • モーダルは header, body, foter に分かれており、それぞれのコンテンツをそれぞれのスロットに差し込む形で利用している。
<template>
  <base-modal :id="id" @show="onShow" @hide="onHide">
    <!-- modal header -->
    <template #header>
      <h5 :id="`${id}Label`" class="modal-title">Modal title</h5>
      <button
        type="button"
        class="close"
        data-dismiss="modal"
        aria-label="Close"
      >
        <span aria-hidden="true">&times;</span>
      </button>
    </template>

    <!-- modal body -->
    <template #body>モーダルボディ </template>

    <!-- modal footer -->
    <template #footer>
      <button type="button" class="btn btn-secondary" data-dismiss="modal">
        Close
      </button>
      <button type="button" class="btn btn-primary">Save changes</button>
    </template>
  </base-modal>
</template>

<script>
import { onBeforeMount, onMounted, onBeforeUnmount, onUnmounted } from "vue";
import BaseModal from "../components/BaseModal.vue";

export default {
  name: "SampleModalContent1",
  components: { BaseModal },
  props: {
    id: {
      default: "",
      require: true,
      type: String,
    },
  },
  setup() {
    // モーダル表示時や非表示に実行されない。モーダルを埋め込んだページへ移動した時や去る時に実行される点に注意
    onBeforeMount(() => console.log("SampleModalContent1 onBeforeMount"));
    onMounted(() => console.log("SampleModalContent1 onMounted"));
    onBeforeUnmount(() => console.log("SampleModalContent1 onBeforeUnmount"));
    onUnmounted(() => console.log("SampleModalContent1 onUnmounted"));

    // したがって、もしモーダル表示時等に処理をしたい場合は、モーダルのイベントでその処理を行う必要がある。
    const onShow = () => console.log("SampleModalContent1 モーダル表示");
    const onHide = () => console.log("SampleModalContent1 モーダル非表示");

    return { onHide, onShow };
  },
};
</script>

最後に基底モーダルとなる laravel/resources/js/components/BaseModal.vue です。

  • id を props で受け取る。
  • header, body, footer をスロットとしたが、基底モーダルに含めるほどでもなかったかもしれない。コンテンツモーダルで都度書くようにしもよかったかもしれない。
  • モーダルのイベントを全てラップしているのが、このコンポーネントの意義である。このコンポーネントを使う側は、このコンポーネントのイベントを購読しさえすればよい。
<template>
  <div
    :id="id"
    ref="root"
    class="modal fade"
    tabindex="-1"
    role="dialog"
    :aria-labelledby="`${id}Label`"
    aria-hidden="true"
  >
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header"><slot name="header" /></div>

        <div class="modal-body"><slot name="body" /></div>

        <div class="modal-footer"><slot name="footer" /></div>
      </div>
    </div>
  </div>
</template>

<script>
import { ref, onMounted, onUnmounted } from "vue";
import $ from "jquery";

export default {
  name: "BaseModal",
  props: {
    id: {
      default: "",
      require: true,
      type: String,
    },
  },
  emits: ["show", "shown", "hide", "hidden"],
  setup(props, { emit }) {
    const root = ref(null);

    onMounted(() => {
      $(root.value)
        .on("show.bs.modal", (e) => {
          emit("show", e);
        })
        .on("shown.bs.modal", (e) => {
          emit("shown", e);
        })
        .on("hide.bs.modal", (e) => {
          emit("hide", e);
        })
        .on("hiddenbsmodal", (e) => {
          emit("hidden", e);
        });
    });

    onUnmounted(() => {
      $(root.value).modal("dispose");
    });

    return { root };
  },
};
</script>

おわりに

モーダルの SFC 化はもっともっと面倒で大変かと予想しておりましたけれども、意外と簡単でした。とはいえ、実際に使う際にはモーダルのこと自体を理解しておく必要があるので、やっぱりモーダルは難しい><、と感じました。

以上です。

コメントを残す