Modal 在前端是很基本的東西,通常基本作法是設定一個 html tag css position 屬性為 absolute ,再配合 top, bottom, left, right 決定位置
但很容易會被其他高 z-index 的 tag 遮蓋住,並且用 tab 按鈕還可以繼續點選 modal 的顯示按鈕,所以這個做法並不怎麼優雅
而目前 html 已經提供 <dialog> tag 可以有效處理這些問題,這邊記錄一些關於 vue 配合 dialog 的作法
vue3 製作 dialog tag modal
傳統 vue3 製作 modal 方法
主要用 transition tag 配合 v-if 顯示
<template>
<Teleport to="body">
<Transition name="modal">
<div v-if="modelValue" class="modal-mask">
<div class="modal-container">
<div class="header">
<slot name="header">this is header</slot>
</div>
<div class="body">
<slot
content
</slot>
</div>
<div class="footer">
<slot>
<button class="btn" @click="onClose">CLOSE</button>
</slot>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
interface Props {
closable?: boolean
modelValue?: boolean
}
interface Emits {
(e: 'update:model-value', value: boolean): void
}
defineProps<Props>()
const emit = defineEmits<Emits>()
const onClose = () => emit('update:model-value', false)
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
使用 Dialog tag
Dialog 在畫面上已經是置頂了,所以不需要 teleport 傳到 body tag 上,另外也不需要做一個背景 element,因為本身有偽元素 ::backdrop 可以使用
<template>
<transition name="modal" @after-leave="afterLeave">
<dialog ref="modal" class="modal-container" v-show="_modalValue" @click="handleClickOutSide">
<div class="header">
<slot name="header">this is header</slot>
</div>
<div class="body">
<slot>
content
</slot>
</div>
<div class="footer">
<slot>
<button class="btn" @click="_modalValue = false">CLOSE</button>
</slot>
</div>
</dialog>
</transition>
</template>
但如果要搭配 transition tag,需要使用 v-show,而不是 v-if ,否則會破壞 dialog 的 css 屬性
此外為了讓 modal 可以配合 transition 動畫,需要一些特別處理
- 顯示 modal 要使用 dialog tag 提供的 showModal 方法 ( 如果是用 show 會變成顯示 dialog,這點可以看 mdn 說明)
const modal = ref<HTMLDialogElement>()
onMounted(() => {
if (_modalValue.value) modal.value?.showModal()
})
watch(_modalValue, (isOpen) => {
if (isOpen) modal.value?.showModal()
})
2. 關閉 modal 時,因為用 v-show 操作顯示,為了讓 transition 動畫顯示,需要先把 v-show 的 model-value 轉為 false,再用 afterLeave 事件關閉 modal
<template>
<transition @after-leave="afterLeave">
<dialog>
...
</dialog>
</transition>
</template>
const modal = ref<HTMLDialogElement>()
const afterLeave = () => modal.value?.close()
到這邊基本的 modal 就完成了,但還有個小細節,如果想要讓點選 modal 外就關閉 modal,目前是沒有事件支援的,所以要自製方法
<template>
<transition name="modal" @after-leave="afterLeave">
<dialog @click="handleClickOutSide">
...
</dialog>
</transition>
</template>
const handleClickOutSide = ({ clientX: x, clientY: y }: MouseEvent) => {
if (!modal.value) return
const { left, right, top, bottom } = modal.value.getBoundingClientRect()
if (x < left || x > right || y < top || y > bottom) _modalValue.value = false
}
另外如果想要指定位置,需要先把 dialog 的 inset 改為 auto 再指定位置
.position-setting {
inset: auto;
top: 50px;
right: 50px;
}
到這邊一個簡單好用的 dialog 就完成了,如果要看完整程式碼,可以點我看看完整的程式碼。