0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Vueでawaitできるメッセージダイアログを考えてみる

Posted at

考えるきっかけ

Vueでメッセージを表示するダイアログを実現する際、以下のようなコードを書くことがあります。

<script setup>
const logic = () => {
  // メッセージダイアログの表示
  visible.value = true
  message.value = "XXXしますか?"
}

const handleOk = () => {
  visible.value = false
  // OKの時の処理
}

const handleCancel = () => {
  visible.value = false
  // Cancelの時の処理
}
</script>

<template>
  <MessageDialog :visible="visible" :message="message" @ok="handleOk" @cancel="handleCancel" />
</template>

これで確かに実現はできるのですが、以下の点が気になります。

  • もともとのロジックとOK, Cancelのロジックが別れてしまう。
  • ページ毎にMessageDialog を配置する必要がある。

できるものなら以下のように記述したいです。

<script setup>
// ダイアログを利用
const dialog = useMessageDialog()

const logic = async () => {

  // メッセージダイアログの表示
  if (await dialog.confirm("XXXしますか?")) {
    // OKの時の処理
  } else {
    // Cancelの時の処理
    await dialog.alert("キャンセルしました。")
  }
}

</script>

この書き方が実現できるようにするためにメッセージ表示のダイアログを作ってみます。

以下では 要点のみに絞って説明を記述します。
コード全体は最後にリンクを載せていますのでそちらを参照ください。

コンポーネントの階層

各画面毎にダイアログのコンポーネントを配置することは避けたいので
以下のようにルートの直下に配置します。

image01.png

ダイアログを表示するための仕組みを作る

各画面で利用する側の仕組みを作る

各画面のvueでは confirm, cancel を実行するだけで実現できるようにしたいため
この機能を持つ MessageDialogController というクラスを作成し provide/injectの仕組みで利用できるようにします。

image02.png

MessageDialogController.ts

class MessageDialogController {

  /**
   * メッセージの表示
   * @param message メッセージ
   * @returns
   */
  public alert(message: string): Promise<boolean> {
  }

  /**
   * 確認メッセージの表示
   * @param message メッセージ
   * @returns
   */
  public confirm(message: string): Promise<boolean> {
  }

}

export {
  MessageDialogController
}

provide/inject用のキー

MessageDialogKey.ts
/**
 * メッセージダイアログをinjectするためのキー
 */
const messageDialogKey: InjectionKey<MessageDialogController> = Symbol();
export { messageDialogKey };

最上位のコンポーネントからprovideする

App.vue
<script setup lang="ts">
// import省略

// MessageDialogと各ページで利用できるように最上位でprovideする。
provide(messageDialogKey, new MessageDialogController());

</script>

<template>
  <!-- 省略 -->
  <MessageDialog />
</template>

injectして利用する関数を作成する。

useMessageDialog.ts
// import省略

/**
 * メッセージダイアログを使用する。
 *
 * @returns メッセージダイアログのコントローラー
 */
function useMessageDialog() {
  const messageDialog = inject(messageDialogKey);
  if (messageDialog == undefined) {
    throw new Error(`messageDialog is not found`);
  }

  return messageDialog;
}

export { useMessageDialog };

利用する側のコード

Sample.vue
<script setup lang="ts">
// import省略

const dialog = useMessageDialog();

async function showDialog() {
  if (await dialog.confirm("ダイアログを表示")) {
    //OK時の処理
    await dialog.alert("OKが選択されました");
  } else {
    //Cancel時の処理
    await dialog.alert("Cancelが選択されました");
  }
}
</script>

<template>
  <main>
    sample

    <div>
      <button @click="showDialog">ダイアログ表示</button>
    </div>
  </main>
</template>

ダイアログを表示する側の仕組みを作る

先の方法では confirm, alert を実行しても空振りしているだけなので実際にダイアログを表示できるようにしていきます。

これを実現するために MessageDialogControllerMessageDialog.vue でも injectし confirm, alert が呼ばれたときのコールバックをコントローラーに登録します。

image03.png

型の定義


/** ダイアログの実体を操作するためのハンドル(コールバック) */
type MessageDialogHandle = {
  show(param: MessageDialogParameter): void;
};

/**
 * メッセージダイアログのパラメーター
 */
type MessageDialogParameter = {
  title: string,
  message: string,
  buttons: string[],
  buttonHandler: MessageDialogButtonHandler,
}

/**
 * メッセージダイアログのボタンを押下したときのハンドラー
 */
type MessageDialogButtonHandler = (index: number) => void;

コントローラーには コールバックを登録する処理(installHandle)を追加し、confirm, alert でコールバックを実行する。

class MessageDialogController {

  // -------------------------------
  // コールバック登録処理を追加

  /**
   * ダイアログの実体(コールバック)
   */
  private handle?: MessageDialogHandle;

  /**
   * ハンドルをインストールします。
   * @param handle ダイアログのハンドル
   */
  public installHandle(handle: MessageDialogHandle): void {
    this.handle = handle;
  }

  // -------------------------------
  // コールバック(handle.show)を呼ぶようにする

  /**
   * ダイアログ表示(共通)
   * @param param パラメーター
   * @returns
   */
  private show(
    param: Omit<MessageDialogParameter, "buttonHandler">
  ): Promise<number> {
    return new Promise<number>((resolve, reject) => {
      if (this.handle == undefined) {
        throw new Error("handle is not installed.");
      }

      const parameter: MessageDialogParameter = {
        ...param,
        buttonHandler: resolve,
      };
      this.handle.show(parameter);
    });
  }

  /**
   * メッセージの表示
   * @param message メッセージ
   * @returns
   */
  public alert(message: string) {
    return this.show({
      title: "情報",
      message,
      buttons: ["OK"],
    }).then(() => true);// ボタン一つしかないので常にtrue
  }

  /**
   * 確認メッセージの表示
   * @param message メッセージ
   * @returns
   */
  public confirm(message: string) {
    return this.show({
      title: "確認",
      message,
      buttons: ["OK", "Cancel"],
    }).then((x) => x === 0); // 0番目のボタン(OK)が押されればtrue
  }
}

export { MessageDialogController };

ダイアログのコンポーネントで onMounted のタイミングで installHandle を実行し
コールバックの登録を行う。

show関数では ダイアログ表示時のパラメータを受け取るようにする。

ダイアログのOK, Cancelなどのボタンをクリックしたときは handleButtonClick 内で
buttonHandlerを実行することで、各画面から表示する際に作成したPromiseをresolveするようにしている。

MessageDialog.vue
<script setup lang="ts">
//import省略

const messageDialog = useMessageDialog();

// -------------------------------
// 初期値の設定

const visible = ref(false);
const title = ref("");
const message = ref("");
const buttons = ref<string[]>([]);

let buttonHandler: MessageDialogButtonHandler = () => {
  //noop
};

onMounted(() => {
  // 各ページで利用できるように
  // コントローラーのハンドルにこのMessageDialog.vueを設定する。
  messageDialog.installHandle({
    show,
  });
});

//-------------------------------

/**
 * ダイアログの表示
 * @param parameter パラメータ
 */
function show(parameter: MessageDialogParameter) {
  visible.value = true;

  title.value = parameter.title;
  message.value = parameter.message;
  buttons.value = parameter.buttons;
  buttonHandler = parameter.buttonHandler;
}

/**
 * クリーンアップ(閉じるとき)
 */
function cleanup() {
  visible.value = false;

  title.value = "";
  message.value = "";
  buttons.value = [];
  buttonHandler = () => {
    //noop
  };
}

/**
 * ダイアログボタンクリック時
 * @param index 押されたボタンのインデックス
 */
function handleButtonClick(index: number) {
  buttonHandler(index);
  // ボタンクリック時に消す
  cleanup();
}
</script>

<template>
  <div class="messageDialog" v-if="visible">
    <div class="messageDialog-background"></div>
    <div class="messageDialog-content">
      <div class="messageDialog-contentHeader">{{ title }}</div>
      <div class="messageDialog-contentBody">{{ message }}</div>
      <div class="messageDialog-contentButtons">
        <template v-for="(button, index) of buttons">
          <button @click="handleButtonClick(index)">{{ button }}</button>
        </template>
      </div>
    </div>
  </div>
</template>

大規模なプロジェクトでの実用性はわからないですが
上記のように 最上位でコントローラーをprovideし、
利用する画面側でinject,
実際のコンポーネントでコールバックを登録
とすることで awaitできるダイアログが実現できるようになります。

ダイアログを表示するときはフォーカスが後ろに移動しないようにするためにinert属性と組み合わせておくとなお良いでしょう

inertについてはこちらを参照してください。

コード全体

以下に全体のコードと少し改良を加えたものを載せています。

epsilonGtMyon/vue-awaitable-dialog

0
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?