vue3ではTeleportを使ったモーダルを作成できます。
vue2ではvue-confirm-dialogのAPI機能を使ってモーダルを作成していました(関数内からモーダルを呼べて便利)。
ですが、vue3に対応していなかったのでAPI機能を使いたかったので一部機能を自作してみました。
ディレクトリ構成
ディレクトリ内
※ 使用していないものは省略しています
├─ public/
│ └── index.html
└─ src/
├── components/
│ └ ConfirmDialog.vue
├── mixins/
│ └ index.js
│
├── App.vue
└── main.js
確認画面の動作の流れ
- $confirm関数を実行
- index.htmlの
<div id="mount-point"></div>
にConfirmDialog.vue
をマウント - モーダルの
ok
またはcancel
のコマンドを押すと渡されたコマンドを実行 - 表示中のモーダルをアンマウント
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
+ <div id="mount-point"></div>
<!-- built files will be auto injected -->
</body>
</html>
main.jsにmixinを追加
import { createApp } from 'vue'
import App from './App.vue'
import Mixin from '@/mixins'
- createApp(App).mount('#app')
+ createApp(App).mixin(Mixin).mount('#app')
Mixinsに$confirm関数を作成
デフォルトでokAction
とcancelAction
にはBoolean
を返すように設定してます。
createApp()の第一引数にコンポーネント、第二引数にpropsを渡して#mount-point
にマウントしています。
import * as Vue from 'vue'
import ConfirmDialog from '@/components/ConfirmDialog'
export default {
methods: {
async $confirm(payload) {
return new Promise((resolve) => {
const defaultOption = {
okAction: () => resolve(true),
cancelAction: () => resolve(false),
}
const Option = Object.assign(defaultOption, payload)
Vue.createApp(ConfirmDialog, Option).mount('#mount-point')
}).catch((err) => {
throw err;
})
},
}
}
プロパティ
Attribute | Type | Default | Description |
---|---|---|---|
message | String | メッセージ |
メッセージ表示内容 |
title | String | タイトル |
タイトルの表示内容 |
button | Object | { yes: 'Yes', no: 'No' } |
OKの時とNGの時の表示内容 |
okAction | Function | ()=>{} |
OK時の処理 |
cancelAction | Function | ()=>{} |
キャンセル時の処理 |
ボタンを押して、処理が終わった後にthis.$.appContext.app.unmount()
を実行するとアンマウントすることができます。
参考サイト:How to destroy/unmount vue.js 3 components?
vue-confirm-dialogからhtmlとcssを拝借してます。
<template>
<div id="vueConfirm" class="vc-overlay">
<div class="vc-container">
<span class="vc-text-grid">
<h4 class="vc-title">{{ message }}</h4>
<p class="vc-text">{{ title }}</p>
</span>
<div class="vc-btn-grid">
<button class="vc-btn left" @click="ok" v-if="button.yes">{{ button.yes }}</button>
<button class="vc-btn" @click="cancel" v-if="button.no">{{ button.no }}</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ConfirmDialog',
props: {
message: {
type: String,
required: true,
default: 'メッセージ',
},
title: {
type: String,
required: true,
default: 'タイトル',
},
button: {
type: Object,
required: true,
default: function () {
return { yes: 'Yes', no: 'No' }
},
},
okAction: {
type: Function,
required: true,
default: () => {},
},
cancelAction: {
type: Function,
required: true,
default: () => {},
},
},
methods: {
ok() {
this.okAction()
this.$.appContext.app.unmount()
},
cancel() {
this.cancelAction()
this.$.appContext.app.unmount()
},
},
}
</script>
<style>
:root {
--title-color: #000;
--message-color: #000;
--overlay-background-color: #0000004a;
--container-box-shadow: #0000004a 0px 3px 8px 0px;
--base-background-color: #fff;
--button-color: #4083ff;
--button-background-color: #fff;
--button-border-color: #e0e0e0;
--button-background-color-disabled: #f5f5f5;
--button-background-color-hover: #f5f5f5;
--button-box-shadow-active: inset 0 2px 0px 0px #00000014;
--input-background-color: #ebebeb;
--input-background-color-hover: #dfdfdf;
--font-size-m: 16px;
--font-size-s: 14px;
--font-weight-black: 900;
--font-weight-bold: 700;
--font-weight-medium: 500;
--font-weight-normal: 400;
--font-weight-light: 300;
}
.vc-overlay *,
.vc-overlay *:before,
.vc-overlay *:after {
-webkit-box-sizing: border-box;
box-sizing: border-box;
text-decoration: none;
-webkit-touch-callout: none;
-moz-osx-font-smoothing: grayscale;
margin: 0;
padding: 0;
}
.vc-title {
color: var(--title-color);
padding: 0 1rem;
width: 100%;
font-weight: var(--font-weight-black);
text-align: center;
font-size: var(--font-size-m);
line-height: initial;
margin-bottom: 5px;
}
.vc-text {
color: var(--message-color);
padding: 0 1rem;
width: 100%;
font-weight: var(--font-weight-medium);
text-align: center;
font-size: var(--font-size-s);
line-height: initial;
}
.vc-overlay {
background-color: var(--overlay-background-color);
width: 100%;
height: 100%;
transition: all 0.1s ease-in;
left: 0;
top: 0;
z-index: 999999999999;
position: fixed;
display: flex;
justify-content: center;
align-items: center;
align-content: baseline;
}
.vc-container {
background-color: var(--base-background-color);
border-radius: 1rem;
width: 286px;
height: auto;
display: grid;
grid-template-rows: 1fr max-content;
box-shadow: var(--container-box-shadow);
}
.vc-text-grid {
padding: 1rem;
}
.vc-btn-grid {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
border-radius: 0 0 1rem 1rem;
overflow: hidden;
}
.vc-btn-grid.isMono {
grid-template-columns: 1fr;
}
.vc-btn {
border-radius: 0 0 1rem 0;
color: var(--button-color);
background-color: var(--button-background-color);
border: 0;
font-size: 1rem;
border-top: 1px solid var(--button-border-color);
cursor: pointer;
font-weight: var(--font-weight-bold);
outline: none;
min-height: 50px;
}
.vc-btn:hover {
background-color: var(--button-background-color-hover);
}
.vc-btn:disabled {
background-color: var(--button-background-color-disabled);
}
.vc-btn:active {
box-shadow: var(--button-box-shadow-active);
}
.vc-btn.left {
border-radius: 0;
border-right: 1px solid var(--button-border-color);
}
.vc-input[type='password'] {
width: 100%;
outline: none;
border-radius: 8px;
height: 35px;
border: 0;
margin: 5px 0;
background-color: var(--input-background-color);
padding: 0 0.5rem;
font-size: var(--font-size-m);
transition: 0.21s ease;
}
.vc-input[type='password']:hover,
.vc-input[type='password']:focus {
background-color: var(--input-background-color-hover);
}
/**
* Transition
*/
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.21s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.zoom-enter-active,
.zoom-leave-active {
animation-duration: 0.21s;
animation-fill-mode: both;
animation-name: zoom;
}
.zoom-leave-active {
animation-direction: reverse;
}
@keyframes zoom {
from {
opacity: 0;
transform: scale3d(1.1, 1.1, 1.1);
}
100% {
opacity: 1;
transform: scale3d(1, 1, 1);
}
}
</style>
確認画面から削除する処理をApp.vueに記述
<template>
<table>
<tr>
<th>タイトル</th>
<th>操作</th>
</tr>
<tr v-for="(item, index) in this.data" :key="index">
<td>{{ item.name }}</td>
<td><button @click="DelData(index)">削除</button></td>
</tr>
</table>
</template>
<script>
export default {
name: 'App',
data() {
return {
data: [],
}
},
mounted() {
for (let i = 0; 30 > this.data.length; i++) {
this.data.push({
name: 'test' + i,
})
}
},
methods: {
async DelData(index) {
this.$confirm({
message: 'メッセージが入ります:' + index,
title: 'タイトルが入ります:' + index,
button: {
yes: '削除する',
no: 'キャンセル',
},
okAction: () => {
// データ削除
this.data.splice(index, 1)
console.log('ok')
},
cancelAction: () => {
console.log('ng')
},
})
},
},
}
</script>
<style>
table {
border-collapse: collapse;
margin: 0 auto;
}
td,
th {
padding: 10px;
}
th {
color: #fff;
background: #005ab3;
}
table tr:nth-child(odd) {
background: #e6f2ff;
}
</style>
実際の動き
trueの時しか処理しない場合はthen()関数でブール値を判別できます。
async hoge() {
await this.$confirm().then((value) => {
if (value) console.log('YESが押されました!')
})
}
おわり
vue3系で便利になったこともあれば、プラグインが対応しきっていないことがあり思わぬところで時間を使ってしまいます。
vue-confirm-dialogがvue3に対応されたら、すぐ使うんですけどね・・・
vue3-confirm-dialogができたみたいです