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

![d1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/35541f5d-5d26-af53-564e-1e39ddd08279.png)
window.confirmと似た使用感の確認ダイアログを自作しました。

<br>
## 確認バージョン
* vue: 2.6.11
* vuetify: 2.3.2

<br>
## 使用例
以下のように、コンポーネントごとに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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/9671d767-aaf0-3e01-dd83-a20c21b85572.png)

<br>
## 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>
```

<br>
## 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');
```

<br>
## 実装意図
* なぜ自作のconfirmを作ったの?
    * ページ全体のデザインを統一したかったから 
* なぜvuetifyを使ったの?
    * デザイン周りの実装を最低限にしたかったから
* なぜ文字列の配列を渡すことにしたの? テンプレートリテラルではだめなの?
    * コードの見た目が、文章の見た目と近くなるから
    * 他コードとインデントを揃えられるから   
      (テンプレートリテラルで複数行メッセージを書く場合、インデントしにくいのが嫌だった)
    * 繰り返し要素に強いから   
      (`mixinConfirm(dataArray.map((data) => {return toMessage(data)})`みたいにできる)
* なぜグローバルミックスインにしないの?
    * 依存関係を明示的にしたかったから
    * mixinの乱用を避けたかったから   
      (mixinはアンチパターンという意見が多い)

<br>
## 参考
https://qiita.com/totto357/items/6e5df072fdb0ccbe8c51
https://zukucode.com/2020/04/vue-alert-confirm.html
技術的には上記2記事の完全なパクリです。
2例を組み合わせ、好みの感じにチューニングしてみた、という記事です。
先人に感謝。

<br>
## 最後に
はやくvuetifyがvue3に対応しますように。