LoginSignup
1
1

More than 3 years have passed since last update.

【Vue.js】window.confirmを自前で作る

Posted at

d1.png
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>

q2.png


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に対応しますように。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1