使用 vue 3 與 html dialog 製作 modal

學習Blog
7 min readSep 29, 2023

--

Bootstrap 提供的 modal

在計算機應用程序的用戶界面設計中,模式窗口 (Modal )是從屬於應用程序主窗口的圖形控制元素。模態窗口創建一種模式,該模式禁止用戶與主窗口進行交互,但保持其可見,並且模態窗口作為其前面的子窗口。用戶必須先與模式窗口進行交互,然後才能返回父窗口。這樣可以避免中斷主窗口上的工作流程。 Wiki

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 動畫,需要一些特別處理

  1. 顯示 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 就完成了,如果要看完整程式碼,可以點我看看完整的程式碼。

--

--