有用過 sweetalert2 的話,應該會喜歡可以同步等待對話框回傳值的方式, 這裡做一個 Vue2 元件,呼叫該元件的方法會彈出對話框等待使用者輸入,並且回傳 Promise, 如此一來就能夠在同一個函式當中處理使用者輸入值。

Dialog 元件設計原理:

  1. 元件方法 GetConfirm() 顯示 Dialog 元件並回傳一個 Promise,。
  2. 設置watcher讓元件取得使用者輸入後 resolve promise

得利於上述元件的設計,實際上的效益是將複雜度封裝到子元件裡面(watcher移動到元件內), 如此不需在上層元件撰寫使用者輸入取值的監視邏輯, 讓我們得以在上層元件直接 await GetConfirm 同步取得值進行操作。

這個概念的用途非常廣,例如 Vue router 的 component route guard,在離開表單頁面前跳出使用者確認的 Dialog。

Vue3 實作

<template>
  <v-dialog v-model="dialog" v-bind="$attrs">
    <slot v-bind="{ Resolve }"></slot>
  </v-dialog>
</template>

<script setup>
import { ref } from "vue";

const dialog = ref(false);
let resolve = null;

const Resolve = (v) => {
  resolve(v);
  dialog.value = false;
};

const GetResult = async () => {
  dialog.value = true;
  return new Promise((res) => (resolve = res));
};
defineExpose({ GetResult, Resolve });
</script>

[舊]Vuejs 實作

<button id="xBtn">執行測試</button>
<div id="xApp" class="modal" :style="{display: dialog?'block':'none'}">
  <div class="modal-content">
    <span class="close">Test Modal</span>
    <p>The value selected will resolve by promise.</p>
    <button @click="choose(1)">1</button>
    <button @click="choose(2)">2</button>
  </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
 
<script>
let data = { result: null, dialog: false }
let dialog = new Vue({
  el: '#xApp',
  data:() => data,
  methods: {
   getConfirm() {
     // 先清空 result (避免兩次選中一樣的值無法觸發 watcher)
     this.result = null 
     // open dialog
     this.dialog = true 
     return new Promise((resolve, reject) => {
       try {
         const watcher = this.$watch(
           // 設置監視的對象為 result
           () => this.result ,
           // 一旦 result 的值有改變,就 resolve promise,並啟動下一輪 watcher 
           (newVal) => resolve(newVal) && watcher()
         )
       } catch (error) {
         // 如果出錯就 reject promise
         reject(error)
       }
     })
   },
   choose(value) {
     // 為 result 設置值觸發 watcher 解開 promise
     this.result = value 
     // 關閉 dialog
     this.dialog = false
   }
  }
})
document.getElementById('xBtn')
  .addEventListener( 'click', 
      async e => alert( await dialog.getConfirm() )
    );
</script>
/* The Modal (background) */
.modal {
  display: none; /* Hidden by default */
  position: fixed; /* Stay in place */
  z-index: 1; /* Sit on top */
  padding-top: 100px; /* Location of the box */
  left: 0;
  top: 0;
  width: 100%; /* Full width */
  height: 100%; /* Full height */
  overflow: auto; /* Enable scroll if needed */
  background-color: rgb(0,0,0); /* Fallback color */
  background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}

/* Modal Content */
.modal-content {
  background-color: #fefefe;
  margin: auto;
  padding: 20px;
  border: 1px solid #888;
  width: 80%;
}

[舊]Vue-next 實作

這裡使用 vue-next/setup/quasar/typescript

程式碼

<template>
  <q-dialog v-model="model" :persistent="persistent">
    <q-card>
      <slot>
        <q-card-section> {{ textComputed }} </q-card-section>
      </slot>
      <q-card-actions align="right">
        <slot name="action" :setter="SetResult">
          <q-btn dense color="primary" label="確認" @click="SetResult(true)" />
          <q-btn dense color="info" label="取消" @click="SetResult(false)" />
        </slot>
      </q-card-actions>
    </q-card>
  </q-dialog>
</template>

<script setup lang="ts">
import { computed, ref, watch } from 'vue';
const model = ref(false);
const result = ref<unknown>(null);

interface IProps {
  text?: string;
  persistent?: boolean;
  width?: 'xs' | 'sm' | 'md' | 'lg' | 'max';
}
const props = withDefaults(defineProps<IProps>(), {
  text: '確認或取消?',
  persistent: true,
  width: 'max',
});

defineEmits(['input']);
const SetResult = (v: unknown) => (result.value = v);

const textTmp = ref<string | null>(null);
const textComputed = computed(() => textTmp.value || props.text);
async function GetResult(text: string | null = null) {
  textTmp.value = text || null;
  result.value = null;
  model.value = true;
  return new Promise((resolve, reject) => {
    console.log('new promise...');
    try {
      const watcher = watch(
        () => result.value,
        (cur) => {
          resolve(cur);
          watcher();
          model.value = false;
        }
      );
    } catch (error) {
      reject(error);
    }
  });
}
defineExpose({ SetResult, GetResult, model });
</script>

使用方法

<template>
  <!-- 確認 Dialog -->
  <DialogAsync ref="dlg" width="sm" />
  <q-btn @click="getUserInput()"> </q-btn>
</template>

<script setup lang="ts">
import DialogAsync from '../../components/DialogAsync/IndexPage.vue';
import { ref, Ref } from 'vue';

const dlg = ref(null);

// 顯示文字並取得使用者輸入的 true 或 false
const check = async (str?: string | null) =>
  await (dlg.value as typeof DialogAsync | null)?.GetResult(str);

const getUserInput = async () => {
  let result = await check('請確認')
  console.log('使用者選擇了:', result)
  }

</script>