0
0

[vue3-jp-admin]Vue 3 と Vuetify 3 を使用した動的モーダルコンポーネントの封装

Last updated at Posted at 2024-08-11

GitHub アドレス:

はじめに

この記事では、Vue 3 と Vuetify 3 を使用して、Pinia を利用した動的なモーダルコンポーネントをどのように封装するかについて説明します。プロジェクトで再利用可能なモーダルコンポーネントを作成する際の設計と実装の詳細を共有します。

連絡先メール:

yangrongwei1996@gmail.com

Demo:

使用技術

  • Vue 3
  • Vuetify 3
  • Pinia
  • TypeScript

目的

モーダルの管理を効率化し、プロジェクト全体で統一されたモーダルコンポーネントを提供することを目的とします。具体的には、以下の機能を実現します。

  • Pinia を利用してモーダルの状態(開閉)を管理
  • モーダルの動的作成および DOM への追加
  • ユーザーの要件に応じたモーダルの外観と挙動のカスタマイズ

コンポーネントの設計

CustomModal.vue

モーダルの見た目と基本機能を持つコンポーネントです。外部から v-bind を用いてプロパティを渡し、表示内容やボタンの動作をカスタマイズできます。

<template>
  <v-dialog
    v-model="isVisible"
    :style="{ zIndex: modal?.zIndex }"
    v-bind="modal?.props"
    :fullscreen="isFullscreen"
  >
    <v-card v-if="modal">
      <v-card-title>
        <template v-if="modal.slots?.titleSlot">
          <component :is="modal.slots.titleSlot" />
        </template>
        <div class="tw-flex tw-justify-between tw-items-center" v-else>
          <div class="tw-flex-shrink-0">{{ modal?.props.title }}</div>
          <div class="tw-flex tw-space-x-2">
            <v-icon @click="toggleFullscreen" v-if="modal?.props.fullscreen">{{
              isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'
            }}</v-icon>
            <v-icon @click="closeModal(modal.id)" class="tw-place-items-end">{{
              'mdi-window-close'
            }}</v-icon>
          </div>
        </div>
      </v-card-title>
      <v-divider></v-divider>
      <v-card-text class="tw-overflow-auto tw-max-h-90">
        <component :is="modal.component" v-bind="modal.props" />
      </v-card-text>
      <v-divider></v-divider>
      <v-card-actions>
        <template v-if="modal.slots?.footSlot">
          <component :is="modal.slots.footSlot" />
        </template>
        <template v-else>
          <v-btn @click="handleCloseClick">{{
            $t('views.modal.cancel')
          }}</v-btn>
          <v-btn @click="handleConfirmClick">{{
            $t('views.modal.confirm')
          }}</v-btn>
        </template>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>

<script setup lang="tsx">
import { computed, ref } from 'vue';
import { useModalStore } from '@/store/modalStore';

const modalStore = useModalStore();
const isVisible = ref(true);

const props = defineProps<{ modalId: string }>();
const modal = computed(() =>
  modalStore.modals.find((m) => m.id === props.modalId)
);

const closeModal = (id: string) => {
  // isVisible を false に設定し、トランジション終了時に DOM 要素を削除
  isVisible.value = false;
  // トランジションが終了するまで待機してから modalStore.closeModal を実行
  setTimeout(() => {
    modalStore.closeModal(id);
  }, 300); // 300ms は Vue のトランジションのデフォルト時間で、必要に応じて調整可能
};

const isFullscreen = ref(modal.value?.props?.fullscreen);
const toggleFullscreen = () => {
  isFullscreen.value = !isFullscreen.value;
};

const handleCloseClick = async () => {
  let res = true;
  if (modal.value?.callbackMethod.onCloseCallback) {
    res = await modal.value?.callbackMethod.onCloseCallback();
    if (res) {
      closeModal(modal.value?.id);
    }
  } else {
    closeModal(modal.value?.id);
  }
};

const handleConfirmClick = async () => {
  let res = true;
  if (modal.value?.callbackMethod.onConfirmCallback) {
    res = await modal.value?.callbackMethod.onConfirmCallback();
    if (res) {
      closeModal(modal.value?.id);
    }
  } else {
    closeModal(modal.value?.id);
  }
};
</script>

<style scoped lang="scss">
.v-dialog {
  transition: none;
}
</style>

modalUtils.ts

モーダルを動的に作成し、DOM に追加するためのユーティリティです。

import { useModalStore } from '@/store/modalStore';
import { markRaw, render, h } from 'vue';
import CustomModal from '@/components/JpModal/index.vue';
import { app } from '../main.js';

// Vue アプリケーションのコンテキストを取得する関数
export function getAppContext() {
  return app._context; // _context は Vue アプリケーションインスタンスのコンテキストです
}

// モーダルを開くための関数
export function openModal({
  component = null,
  props = {},
  slots = {},
  callbackMethod = {},
}: {
  component: any; // モーダルに表示するコンポーネント
  props?: Record<string, any>; // コンポーネントに渡すプロパティ
  slots?: Record<string, any>; // コンポーネントのスロット
  callbackMethod?: Record<string, any>; // コールバックメソッド
}) {
  const modalStore = useModalStore();
  const componentRef = markRaw(component); // コンポーネントをマークして再レンダリングを防ぐ

  // モーダルストアを使ってモーダルを開く
  const id = modalStore.openModal(
    componentRef,
    { ...props },
    slots,
    callbackMethod
  );

  // 新しい div 要素を作成して body に追加する
  const modalElement = document.createElement('div');
  document.body.appendChild(modalElement);

  // CustomModal コンポーネントのインスタンスを作成する
  const modalInstance = h(CustomModal, { modalId: id });

  // Vuetify のエラーを回避するため、アプリケーションコンテキストを設定する
  modalInstance.appContext = getAppContext();

  // モーダルインスタンスを作成した div 要素にレンダリングする
  render(modalInstance, modalElement);

  return id; // モーダルの ID を返す
}

// 指定された ID のモーダルを閉じる関数
export function closeModal(id: string) {
  const modalStore = useModalStore();
  modalStore.closeModal(id);
}

// すべてのモーダルを閉じる関数
export function closeAllModals() {
  const modalStore = useModalStore();
  modalStore.closeAllModals();
}

useModalStore.ts

モーダルの状態を管理するための Pinia ストアです。

// store/modalStore.ts
import { defineStore } from 'pinia';

interface Modal {
  id: string;
  component: any;
  props?: Record<string, any>;
  slots?: Record<string, any>;
  zIndex: number;
  callbackMethod?: Record<string, Function>;
}

export const useModalStore = defineStore('modalStore', {
  state: () => ({
    modals: [] as Modal[],
    zIndexCounter: 9999,
  }),
  actions: {
    openModal(
      component: any,
      props?: Record<string, any>,
      slots?: Record<string, any>,
      callbackMethod?: Record<string, Function>
    ) {
      const id = `modal-${Date.now()}`;
      this.zIndexCounter++;
      this.modals.push({
        id,
        component,
        props,
        zIndex: this.zIndexCounter,
        slots,
        callbackMethod,
      });
      return id;
    },
    closeModal(id: string) {
      this.modals = this.modals.filter((modal) => modal.id !== id);
    },
    closeAllModals() {
      this.modals = [];
    },
  },
});

使用方法

以下のようにモーダルを動的に作成し、管理することができます。

// モーダルを開く
openModal({
    component: (
          <div class="tw-flex-col">
            <div onClick={newModal}>
              <VBtn class="tw-mb-2">
                {$t('views.modal.nestingModal.buttonText')}
              </VBtn>
            </div>
            <HighlightCode code={nestingCode} />
          </div>
    ),
    props: {
          title: $t('views.modal.nestingModal.title'),
          width: '600',
    },
    callbackMethod: {
          onCloseCallback: () => {
            return true;
          },
          onConfirmCallback: () => {
            return false;
          },
    },
    });

終わりに

このモーダルコンポーネントの封装により、プロジェクト全体で再利用可能なモーダルを作成し、管理の効率化が図れます。動的に作成されるモーダルは、柔軟性と拡張性を持ちながら、Vue 3 と Vuetify 3 の機能を最大限に活用しています。

皆さんもぜひ試してみてください。そして、フィードバックや質問があれば、コメント欄でお知らせください!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0