window.confirmと似た使用感の確認ダイアログを自作しました。
確認バージョン
- vue: 2.6.11
- vuetify: 2.3.2
使用例
以下のように、コンポーネントごとにmixinを読み込んで使います。
文字列の配列を渡すと、複数行のメッセージとして表示される仕組みになっています。
@/components/MyComponent.vue
<template>
<div class="container">
<h2>氏名変更</h2>
<v-btn small class="button primary" @click="changeName">実行</v-btn>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import ConfirmDialogMixin from '@/mixins/ConfirmDialogMixin.ts';
export default Vue.extend({
mixins: [ConfirmDialogMixin],
data() {
return {
userAccountBefore: {jpName: '田中 一郎', enName: 'Tnaka Ichiro'},
userAccountAfter: {jpName: '田中 二郎', enName: 'Tnaka Jiro'},
};
},
methods: {
async changeName(): Promise<void> {
// こんな感じで呼び出す
const isUserClickOK = await this.mixinConfirm([
'以下の通り変更します。よろしいでしょうか?',
'■氏名',
`変更前: ${this.userAccountBefore.jpName}`,
`変更後: ${this.userAccountAfter.jpName}`,
'■English Name',
`変更前: ${this.userAccountBefore.enName}`,
`変更後: ${this.userAccountAfter.enName}`,
]);
if (isUserClickOK) {
// OKを押した時の処理
}
},
},
});
</script>
<style scoped lang="scss">
.container {
padding: 20px;
}
</style>
confirm部分のコード
ConfirmDialogMixin.tsにて非グローバルのmixinを定義。
mixin関数を呼ぶとConfirmDialog.vueのインスタンスを作成し、OK/キャンセルを押すと破棄する作りです。
@/mixins/ConfirmDialogMixin.ts
import Vue from 'vue';
import vuetify from '@/plugins/vuetify';
import ConfirmDialog from '@/components/generalParts/ConfirmDialog.vue';
declare module 'vue/types/vue' {
interface Vue {
mixinConfirm(messages: string[]): Promise<boolean>;
}
}
export default {
methods: {
// window.confirm()と似た感じで使うconfirm用の共通関数
mixinConfirm: (messages: string[]): Promise<boolean> => {
return new Promise((resolve) => {
const VM = Vue.extend(ConfirmDialog);
new VM({
vuetify, // <- これをつけなくても動くが、consoleエラーが出まくる
propsData: {
messages,
onClickOK: () => {
return resolve(true);
},
onClickClose: () => {
return resolve(false);
},
},
});
});
},
},
};
@/components/generalParts/ConfirmDialog.vue
<template>
<v-dialog :value="isDialogActive" max-width="600" persistent>
<div class="container">
<div class="messages">
<div v-for="(message, index) in messages" :key="index">
{{ message }}
</div>
</div>
<v-btn small class="button primary" @click="ok">OK</v-btn>
<v-btn small class="button secondary" @click="cancel">キャンセル</v-btn>
</div>
</v-dialog>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue';
export default Vue.extend({
props: {
messages: {
type: Array as PropType<string[]>,
required: true,
},
onClickOK: {
type: Function,
required: true,
},
onClickClose: {
type: Function,
required: true,
},
},
data() {
return {
isDialogActive: false,
};
},
created() {
this.$mount();
document.body.appendChild(this.$el);
this.isDialogActive = true;
},
methods: {
ok() {
this.onClickOK();
this.close();
},
cancel() {
this.onClickClose();
this.close();
},
close() {
this.isDialogActive = false;
// アニメーションが見えるよう若干待つ
setTimeout(() => {
if (document.body.contains(this.$el)) {
document.body.removeChild(this.$el);
}
this.$destroy();
}, 200);
},
},
});
</script>
<style lang="scss" scoped>
$spacingNormal: 8px;
.container {
border: solid 2px;
border-radius: 4px;
background-color: #F2F7F2;
padding: $spacingNormal;
}
.messages {
max-height: 300px; // メッセージが長くてもこの高さが最大
overflow: scroll; // はみ出た分はスクロールさせる
border-radius: 4px;
background-color: #FFFFFF;
padding: $spacingNormal;
line-height: 1.25;
}
.button {
width: 120px;
margin-top: $spacingNormal;
margin-right: $spacingNormal;
}
</style>
vuetifyを使えるようにするコード
一応、vuetifyを使えるようにするコードも載せておきます。
main.tsと別ファイルに書くことで、main/test/mixinで同じ定義を使える利点があります。
@plugin/vuetify.ts
import Vue from 'vue';
import Vuetify from 'vuetify';
import 'vuetify/dist/vuetify.min.css';
Vue.use(Vuetify);
export default new Vuetify({
theme: {
dark: false,
themes: {
light: {
primary: '#EA9034',
secondary: '#564C46',
},
},
},
});
main.ts
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import vuetify from './plugins/vuetify';
new Vue({
router,
vuetify,
render: (h) => {
return h(App);
},
}).$mount('#app');
実装意図
- なぜ自作のconfirmを作ったの?
- ページ全体のデザインを統一したかったから
- なぜvuetifyを使ったの?
- デザイン周りの実装を最低限にしたかったから
- なぜ文字列の配列を渡すことにしたの? テンプレートリテラルではだめなの?
- コードの見た目が、文章の見た目と近くなるから
- 他コードとインデントを揃えられるから
(テンプレートリテラルで複数行メッセージを書く場合、インデントしにくいのが嫌だった) - 繰り返し要素に強いから
(mixinConfirm(dataArray.map((data) => {return toMessage(data)})
みたいにできる)
- なぜグローバルミックスインにしないの?
- 依存関係を明示的にしたかったから
- mixinの乱用を避けたかったから
(mixinはアンチパターンという意見が多い)
参考
https://qiita.com/totto357/items/6e5df072fdb0ccbe8c51
https://zukucode.com/2020/04/vue-alert-confirm.html
技術的には上記2記事の完全なパクリです。
2例を組み合わせ、好みの感じにチューニングしてみた、という記事です。
先人に感謝。
最後に
はやくvuetifyがvue3に対応しますように。