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にデモページも・・・