考えるきっかけ
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>
この書き方が実現できるようにするためにメッセージ表示のダイアログを作ってみます。
以下では 要点のみに絞って説明を記述します。
コード全体は最後にリンクを載せていますのでそちらを参照ください。
コンポーネントの階層
各画面毎にダイアログのコンポーネントを配置することは避けたいので
以下のようにルートの直下に配置します。
ダイアログを表示するための仕組みを作る
各画面で利用する側の仕組みを作る
各画面のvueでは confirm
, cancel
を実行するだけで実現できるようにしたいため
この機能を持つ MessageDialogController
というクラスを作成し provide/injectの仕組みで利用できるようにします。
class MessageDialogController {
/**
* メッセージの表示
* @param message メッセージ
* @returns
*/
public alert(message: string): Promise<boolean> {
}
/**
* 確認メッセージの表示
* @param message メッセージ
* @returns
*/
public confirm(message: string): Promise<boolean> {
}
}
export {
MessageDialogController
}
provide/inject用のキー
/**
* メッセージダイアログをinjectするためのキー
*/
const messageDialogKey: InjectionKey<MessageDialogController> = Symbol();
export { messageDialogKey };
最上位のコンポーネントからprovideする
<script setup lang="ts">
// import省略
// MessageDialogと各ページで利用できるように最上位でprovideする。
provide(messageDialogKey, new MessageDialogController());
</script>
<template>
<!-- 省略 -->
<MessageDialog />
</template>
injectして利用する関数を作成する。
// import省略
/**
* メッセージダイアログを使用する。
*
* @returns メッセージダイアログのコントローラー
*/
function useMessageDialog() {
const messageDialog = inject(messageDialogKey);
if (messageDialog == undefined) {
throw new Error(`messageDialog is not found`);
}
return messageDialog;
}
export { useMessageDialog };
利用する側のコード
<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
を実行しても空振りしているだけなので実際にダイアログを表示できるようにしていきます。
これを実現するために MessageDialogController
を MessageDialog.vue
でも injectし confirm
, alert
が呼ばれたときのコールバックをコントローラーに登録します。
型の定義
/** ダイアログの実体を操作するためのハンドル(コールバック) */
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するようにしている。
<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についてはこちらを参照してください。
コード全体
以下に全体のコードと少し改良を加えたものを載せています。