Vue composition API + setup + VueUseを使ってモーダルを作る
VueのComposition APIとscript setupを知ってから、Vueを書くのが楽しくなりました。
状態を持つロジックをコンポーザブルとして個々に切り出せるので、vueファイルに存在していた複数のロジックを、複数のファイルに切り出し、結果Vueファイルが読みやすくなります。
各ロジックを関心するロジック毎に1ファイルになるので、意図を読みやすくコードを残すことができます。
で、coposableをもっと書きたいと思い、composableを読んでいたところ、参考文献で下記の文言を発見。
VueUse: 発展し続ける Vue コンポーザブル集。このソースコードも素晴らしい学習資料です。
VueUseとは、とおもってサイトを見ると以下の文言
Collection of Essential Vue Composition Utilities
便利なutility集とのことです。
Functionsをみると便利な関数が山ほどあります。
使用例が豊富で、sourceと合わせると、composableを書く際にとても参考になりそうです。
VueUseの使い勝手を知るため、表題の内容
「VueUseを使ってモーダルを作る」
をやってみたのでその記録です。
成果物
環境
"dependencies": {
"vue": "^3.2.45",
"@vueuse/core": "^9.13.0"
},
"volta": {
"node": "18.14.1",
"yarn": "1.22.19"
}
解説
VueUseで使ったのは以下のfunctionです。
onKeyStroke
Listen for keyboard key being stroked.
キー入力にイベントを付与します。
使用例
// ESCキー押下時にモーダルを閉じる
onKeyStroke("Escape", (e) => {
e.preventDefault();
closeModal();
});
ESC
キーを押したときに、closeModal
を実行するように登録します。
ESC
は"Escape"
です。その他の対応はいかを参照してください。
Key values for keyboard events - Web APIs | MDN
内部でuseEventListener | VueUseを呼び出しています。
useEventListenerは以下の通り、unmount時にリスナーを削除するところまで面倒みてくれるのでとってもありがたいです。
Use EventListener with ease. Register using addEventListener on mounted, and removeEventListener automatically on unmounted.
onClickOutside
Listen for clicks outside of an element. Useful for modal or dropdown.
要素外のクリックを検知して関数を実行してくれます。上述の通り、モーダルやドロップダウンを作る時に重宝します。
使用例は下記。
<template>
<div class="modal fade" :class="{ show: isShow }">
<div class="dialog">
<div ref="modalRef" class="content">
</div>
</div>
</div>
</template>
<script setup>
const modalRef = ref(null);
onClickOutside(modalRef, () => {
onClose();
});
</script>
第一引数で、基準とする要素をテンプレート参照 | Vue.jsで指定します。
要素外の要素はこの参照です。
第二引数で、要素外をクリックした時に実行する関数を指定します。
第3引数は今回使いませんでした。リファレンスを読むと要素外の対象から除外する要素をセレクタのクエリで指定できるようです。
例
onClickOutside(modalRef, () => {
onClose();
},
{
ignore: ['button']
});
モーダルの実装
作ったモーダルは以下の機能を有します
- ボタンを押すとモーダルが開く
- ボタンを押すとモーダルが閉じる
- モーダル外を押すとモーダルが閉じる
- ESCキーを押すとモーダルが閉じる
ボタンを押すとモーダルが開く
<template>
<div>
<div>
<button @click="onOpen">開く</button>
</div>
<Modal v-if="isShow" @close="onClose" />
</div>
</template>
<script setup>
import { ref } from "vue";
import Modal from "./Modal.vue";
const isShow = ref(false);
const onOpen = () => {
isShow.value = true;
};
const onClose = () => {
isShow.value = false;
};
</script>
モーダルは Modal.vue
で描画します。モーダルを開くとは isShow === trueのことです。
よって、開くボタンを押すと onOpenが発火→isShowがtrueになる → モーダルが開くということです。
ボタンを押すとモーダルが閉じる
子コンポーネントのキャンセルボタンを押す→onCloseが発火→ closeをemitする → 親で受け取りonCloseを発火→isShowがfalseになる→モーダルが閉じるということです。
Modal.vue
<div>
<button @click="onClose">キャンセル</button>
</div>
####
const onClose = () => {
closeModal();
};
const closeModal = () => {
emits("close");
};
Page.vue
<Modal v-if="isShow" @close="onClose" />
モーダル外を押すとモーダルが閉じる
ようやくVueUseの出番です。といっても下記だけです。
コンポーネント作成時にonClickOutsideでコンファーム表示箇所をテンプレート参照経由で要素として登録し、要素外をクリックした時の処理を第2引数でコールバックとして登録するだけです。
<div ref="modalRef" class="content">
</div>
####
import { onClickOutside } from "@vueuse/core";
const modalRef = ref(null);
onClickOutside(modalRef, () => {
onClose();
});
とっても簡単。
ESCキーを押すとモーダルが閉じる
これも簡単です。既に上の方で説明したとおりです。
コンポーネント作成時にonKeyStrokeでESCキー押下を登録し、押した時の処理を第2引数でコールバックとして登録するだけです。
import { onKeyStroke } from "@vueuse/core";
onKeyStroke("Escape", (e) => {
closeModal();
});
コード
src/components/Page.vue
<template>
<div>
<div>
<button @click="onOpen">開く</button>
</div>
<Modal v-if="isShow" @close="onClose" />
</div>
</template>
<script setup>
import { ref } from "vue";
import Modal from "./Modal.vue";
const isShow = ref(false);
const onOpen = () => {
isShow.value = true;
};
const onClose = () => {
isShow.value = false;
};
</script>
src/components/Modal.vue
は以下の様になりました。
モーダルが表示されたときにふわっとする様にフェードを工夫しました。
が、setTimeout以外の方法が知りたい。
<template>
<div class="modal fade" :class="{ show: isShow }">
<div class="dialog">
<div ref="modalRef" class="content">
<div>
<p>アラートメッセージ</p>
</div>
<div>
<button @click="onClose">キャンセル</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from "vue";
import { onClickOutside } from "@vueuse/core";
import { onKeyStroke } from "@vueuse/core";
const emits = defineEmits(["close"]);
const modalRef = ref(null);
const isShow = ref(false);
onMounted(async () => {
// ふわっとしたトランジションで表示させるように、描画後におこなう。
await nextTick();
setTimeout(() => {
console.log("start open transition");
isShow.value = true;
}, 50);
});
const onClose = () => {
closeModal();
};
const closeModal = () => {
console.log("start close transition");
isShow.value = false;
// ふわっとしたトランジションで非表示するように、トランジションが終わるまで(200ms)emitの発行を待つ。
setTimeout(() => {
console.log("close");
emits("close");
}, 200);
};
// ダイアログ外を押した時にモーダルを閉じる
onClickOutside(modalRef, () => {
console.info("click outside");
onClose();
});
// ESCキー押下時にモーダルを閉じる
onKeyStroke("Escape", (e) => {
console.info("key stroke Escape");
e.preventDefault();
closeModal();
});
</script>
<style scoped lang="css">
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
background-color: rgb(0, 0, 0, 0.75);
}
.fade:not(.show) {
opacity: 0;
}
.fade {
transition: opacity 0.2s linear;
}
.dialog {
max-width: 500px;
margin: 5rem auto;
}
.content {
position: relative;
display: flex;
flex-direction: column;
row-gap: 1rem;
padding: 2rem;
justify-content: center;
background-color: white;
border-radius: 1rem;
}
</style>
Discussion