0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Vueで使用しているModalの開閉を管理するオブジェクト

Last updated at Posted at 2023-03-17

Vueで使用しているModalの開閉を管理するオブジェクト(備忘録)

目的

  • モーダルの開閉制御
  • モーダル開閉処理の前後に処理を挟む場合
  • モーダル開閉時に特定の条件での強制中止機能の実装
  • ただ・・・ただ・それだけ。

どんなの?

modal-control.ts

modal-control.ts
export interface ModalControl<T = any> {
  /**
   * showBefre,showAfter がasync付きならasync付き関数になります。
   */
  show: (() => void) | (() => Promise<void>);
  /**
   * showBefore モーダルが開く前に実行される関数
   * - true をreturnするとモーダルを開く処理は中止される
   */
  showBefore: OptionalFunction<T>;
  showAfter: OptionalFunction<T>;
  /**
   * closeBefore,closeAfter がasync付きならasync付き関数になります。
   */
  close: () => void;
  /**
   * closeBefore モーダルが閉じる前に実行される関数
   * - true をreturnするとモーダルを閉じる処理は中止される
   */
  closeBefore: OptionalFunction<T>;
  closeAfter: OptionalFunction<T>;
  //状態管理
  isShow: boolean;
  //データ保持
  state: T;
}

type OptionalFunction<T> = ((state: T) => void | boolean) | ((state: T) => Promise<void | boolean>) | null;

export const InitModalControl = <T = any>(arg?: {
  state?: T;
  closeBefore?: OptionalFunction<T>;
  closeAfter?: OptionalFunction<T>;
  showBefore?: OptionalFunction<T>;
  showAfter?: OptionalFunction<T>;
}): ModalControl<T> => {
  // if (arg === undefined) arg = {};
  const noneInitMessage = 'が初期化されていません';
  return {
    show: () => console.log('[show]' + noneInitMessage),
    showBefore: null,
    showAfter: null,
    close: () => console.log('[close]' + noneInitMessage),
    closeBefore: null,
    closeAfter: null,
    isShow: false,
    state: null,
    ...arg,
  } as ModalControl;
};

const isAsync = (func: any) => {
  try {
    if (func === null) return false;
    if (func === undefined) return false;
    if (!('constructor' in func)) return false;
    return func.constructor.name === 'AsyncFunction';
  } catch {
    return false;
  }
};

/**
 * modalオブジェクトの初期化
 * @param modal ModalControlが含まれるReactiveオブジェクト
 * @param nextTick Vue の nextTick関数
 */
export const InitModals = (modal: any, nextTick: any) => {
  Object.keys(modal).forEach((key) => {
    const m: ModalControl = (modal as any)[key];
    m.show = async () => {
      if (m.showBefore) {
        if (isAsync(m.showBefore)) {
          if ((await m.showBefore(m.state)) === true) {
            console.info('モーダルのShow動作はキャンセルされました');
            return;
          }
        } else {
          if (m.showBefore(m.state) === true) {
            console.info('モーダルのShow動作はキャンセルされました');
            return;
          }
        }
      }
      m.isShow = true;
      if (!m.showAfter) return;
      await nextTick();
      if (isAsync(m.showAfter)) {
        await m.showAfter(m.state);
      } else {
        m.showAfter(m.state);
      }
    };
    m.close = async () => {
      if (m.closeBefore) {
        if (isAsync(m.closeBefore)) {
          if ((await m.closeBefore(m.state)) === true) {
            console.info('モーダルのClose動作はキャンセルされました');
            return;
          }
        } else {
          if (m.closeBefore(m.state) === true) {
            console.info('モーダルのClose動作はキャンセルされました');
            return;
          }
        }
      }
      m.isShow = false;
      await nextTick();
      if (!m.closeAfter) return;
      if (isAsync(m.closeAfter)) {
        await m.closeAfter(m.state);
      } else {
        m.closeAfter(m.state);
      }
    };
  });
};

使い方

モーダル開閉を管理するReactiveオブジェクトの型定義

root-view.vue
import { ModalControl, InitModalControl, InitModals } from './modal-control';
interface Modal {
  test1: ModalControl<{
    isLock: boolean;
    id: number;
  }>;
  //test2子コンポーネント側で閉じる部分の制御する
  test2: ModalControl;
}

モーダル開閉を管理するReactiveオブジェクト

root-view.vue
const modal = reactive<Modal>({
  test1: InitModalControl<Modal['test1']['state']>({
    state: { isLock: false, id: 1 },
    //asyncもOK
    showBefore: (state) => {
      console.log('showBefore', state);
      //ここに制御を入れてtrueを返せば中断できる
      return state.isLock;
    },
    showAfter: (state) => {
      console.log('showAfter', state);
    },
    //asyncもOK
    closeBefore: (state) => {
      console.log('closeBefore', state);
      //ここに制御を入れてtrueを返せば中断できる
      return state.isLock;
    },
    closeAfter: (state) => {
      console.log('closeAfter', state);
    },
  }),
  test2: InitModalControl(),
});

コンポーネントがマウントされたらModalの関数登録用処理を実行

root-view.vue
onMounted(() => {
  InitModals(modal, nextTick);
});

Elementを捕捉して開く直前にモーダル要素側のスクロール制御を入れることがあるので
onMountedで実行するように統一して使っている。けどrefの組み合わせで気にしなくても良いかも

テンプレート側のModal部分

root-view.vue
<!-- 省略 -->
 <teleport to="#teleport">
    <div class="modal-container" :class="{ isShow: modal.test1.isShow }" @click="modal.test1.close()">
      <div class="card" @click.stop>
        <div class="card-header">モーダル</div>
        <div class="card-body">
          <div class="text-center mb-2">id={{ modal.test1.state.id }}</div>
          <div class="">
            <div class="btn btn-danger me-1" @click="modal.test1.state.isLock = !modal.test1.state.isLock">change</div>
            <span class="ms-1">isLock={{ modal.test1.state.isLock }}</span>
          </div>
        </div>
        <div class="card-footer">
          <div class="btn btn-outline-secondary w-100" @click="modal.test1.close()">modal閉じる</div>
        </div>
      </div>
    </div>
    <div class="modal-container" :class="{ isShow: modal.test2.isShow }" @click="modal.test2.close()">
      <VcModal
        :isShow="modal.test2.isShow"
        @close-before-func="(func) => (modal.test2.closeBefore = func)"
        @close="modal.test2.close()"
      >
      </VcModal>
    </div>
  </teleport>

@close-before-func="(func) => (modal.test2.closeBefore = func)"

ちなみにtest2は子コンポーネント側でcloseBefore関数を定義して制御している
※emitで関数を子コンポーネントから受け取ってModalの管理オブジェクトに設定している

念のため子コンポーネント側SFC

vc-modal.vue
<script setup lang="ts">
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap';
import { ref, watch } from 'vue';
type Props = {
  isShow: boolean;
};
const props = defineProps<Props>();
type Emits = {
  (e: 'close'): void;
  (e: 'close-before-func', func: () => void): void;
};
const emit = defineEmits<Emits>();
const target = ref();
const { activate, deactivate } = useFocusTrap(target);

watch(
  () => props.isShow,
  () => {
    if (props.isShow) {
      message.value.length = 0;
      activate();
    } else {
      deactivate();
    }
  }
);
const closeBefore = () => {
  if (isLock.value) {
    message.value.push('isLock=trueで閉じません');
  }
  return isLock.value;
};
emit('close-before-func', closeBefore);
const message = ref<string[]>([]);
const isLock = ref(false);
</script>
<template>
  <div class="card" @click.stop>
    <div class="card-header">モーダル</div>
    <div class="card-body py-1">
      <div class="d-flex justify-content-between fw-bold fs-5 w-100">
        <div class=""></div>
        <div class="">領域外クリックで閉じれる</div>
        <div class=""></div>
      </div>
      <div class="text-center mb-2">でもisLock=trueだと閉じれない</div>
      <div class="">
        <div class="btn btn-danger me-1" @click="isLock = !isLock">change</div>
        <span class="ms-1">isLock={{ isLock }}</span>
      </div>
      <div class="message">
        {{ message.join('\n') }}
      </div>
    </div>
    <div class="card-footer">
      <div class="btn btn-outline-secondary w-100" @click="emit('close')">modal閉じる</div>
    </div>
  </div>
</template>
<style lang="scss" scoped>
//省略
</style>

ちなみにVue.jsとVueUseでフォーカスの逃げないModalを作るメモを使ってフォーカスロック付き

#GitHubにサンプル上げてます

#ついでにGitHubにデモページも・・・

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?