【Nuxt3】Vuetify3 + Piniaで通知トーストの表示・単体テスト実装について
こちらは、Mavs Advent Calendar2023の2日目の記事です!🎅
ハコザキです。
今回は、Nuxt3 + Vuetify + Vitest + Piniaの環境を構築した後、
Vuetifyのコンポーネントを用いて以下のようなトースト表示をできるようにしたいと思います。
また、Piniaのテストについても少し紹介したいと思います。
Nuxt3 + Vuetify + Vitestの構築
Nuxt3 + Vuetify + Vitestにつきましては、こちらの記事で紹介しております。
【Nuxt3】Vuetify3 + Vitestで単体テスト実行環境を構築
今回はこの環境を使用した上で進みたいと思います!
Piniaの導入
続いて、上記環境にPiniaの導入を行っていきます。
基本的に公式ドキュメントのNuxt対応方法を参考に進めてます。
Nuxt.js | Pinia
ライブラリのインストールを行います。
pnpm i pinia @pinia/nuxt
nuxt.config.tsのmodulesに追加します。
import path from 'path'
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
srcDir: './src/',
components: [
{
path: path.resolve(__dirname, './src/components/'),
pathPrefix: false,
},
],
build: {
transpile: ['vuetify'],
},
modules: [
(_options, nuxt) => {
nuxt.hooks.hook('vite:extendConfig', (config) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
config.plugins.push(vuetify({ autoImport: true }))
})
},
'@pinia/nuxt',
],
vite: {
vue: {
template: {
transformAssetUrls,
},
},
},
})
これでPiniaの導入は完了しました。
PiniaはNuxt3のAuto importも対応していますが、今回は割愛します。
Nuxt.js Auto Imports | Pinia
通知トーストの実装
VuetifyにはSnackbarというコンポーネントが用意されているため、
このコンポーネントを用いてトーストを表示したいと思います。
トーストは一時的なメッセージの表示に使われるもので、
ログインや何かしらの登録/削除のイベントなど、様々な場面で使用されます。
そのためトースト自体をコンポーネント化し、
表示やテキストを状態管理で保持しておき、どこからでも呼び出せるように実装したいと思います。
一つのコンポーネントで通知トーストの表示
まずはボタンを押してトーストが表示されるコンポーネントを作成します。 ~/src/components/Toast/TheToast.vue
を作成し以下を記述します。
<script setup lang="ts">
const isActive = ref(false)
</script>
<template>
<VBtn color="success" width="100px" @click="isActive = true">通知を表示</VBtn>
<VSnackbar
v-model="isActive"
multi-line
timeout="1500"
color="success"
location="top center"
variant="tonal"
class="py-2"
@on-vnode-unmounted="isActive = false"
>
通知
</VSnackbar>
</template>
~/src/layouts/default.vue
で通知トースト用のコンポーネントを呼び出し、
トーストを表示します。
<template>
<VApp>
<TheToast />
</VApp>
</template>
トーストをどこでも呼び出せるように改修
続いて、このトーストをどこでも呼び出せるようにします。
現在のTheToastコンポーネントはボタンと表示するトーストがセットになっています。
どこでも呼び出せるようにするコンポーネントには不要のため、ボタンは削除しておきます。
~/src/components/Toast/TheToast.vue
を以下のようにします。
<script setup lang="ts">
const isActive = ref(false)
</script>
<template>
<VSnackbar
v-model="isActive"
multi-line
timeout="1500"
color="success"
location="top center"
variant="tonal"
class="py-2"
@on-vnode-unmounted="isActive = false"
>
通知
</VSnackbar>
</template>
続いて、トーストの表示・非表示の状態を全体で管理できるように、
Piniaでの状態管理を行います。
Piniaでのトースト表示の状態を管理
~/src/store/toast.ts
を作成します。
import { defineStore } from 'pinia'
interface State {
isActive: boolean
}
export const useToastStore = defineStore('toast', {
state: (): State => ({
isActive: false,
}),
actions: {
unsetSnackbar() {
this.$reset()
},
setActive() {
this.isActive = true
},
},
})
このストアでは、後ほど色々追加しますが、
ひとまず表示/非表示のBooleanを定義しておきます。
unsetSnackbar()では$reset()を活用してState状態のリセット、
setActive()を呼び出すことで表示にするというアクションを定義しました。
続いて、TheToastコンポーネントの表示用のv-modelをStateの値と連携します。
<script setup lang="ts">
import { useToastStore } from '~/store/toast'
const toastStore = useToastStore()
</script>
<template>
<VSnackbar
v-model="toastStore.isActive"
multi-line
timeout="1500"
color="success"
location="top center"
variant="tonal"
class="py-2"
@on-vnode-unmounted="toastStore.unsetSnackbar()"
>
通知
</VSnackbar>
</template>
v-model="toastStore.isActive"
で表示状態を同期します。@on-vnode-unmounted="toastStore.unsetSnackbar()"
でトーストがタイムアウトになったとき、 unsetSnackbar()
を呼び出し、State状態のリセットをします。
トースト呼び出し側の実装
呼び出し側はトーストのState状態を表示
にするだけでokです。
~/src/layouts/default.vue
を以下のようにします。
<template>
<VApp>
<slot />
<TheToast />
</VApp>
</template>
続いて、~/src/pages/index.vue
でボタンを設置し、トーストを表示させてみます。
<script setup lang="ts">
import { useToastStore } from '~/store/toast'
const toastStore = useToastStore()
</script>
<template>
<VContainer>
<VBtn color="success" @click="toastStore.setActive()"
>通知を表示(トップページ)</VBtn
>
</VContainer>
</template>
動作確認
ボタンクリック → トーストの表示の流れは同じですが、
pagesで定義したボタンをクリックすることでトースト表示できていればokです!
通知タイトルやColorのカスタマイズ
一通りのトースト表示の実装はできました。
このままでは毎回 通知
というトーストが表示されるだけで汎用性がありません..
タイトルやトーストの色をカスタマイズしたいと思います!
まずはストアを編集します。~/src/store/toast.ts
を以下のようにします。
import { defineStore } from 'pinia'
interface State {
isActive: boolean
text: string | null
color: 'success' | 'error'
}
export const useToastStore = defineStore('toast', {
state: (): State => ({
isActive: false,
text: null,
color: 'success',
}),
actions: {
/**
* Stateの初期化
*/
unsetSnackbar() {
this.$reset()
},
/**
* トーストの表示
*/
setToast(text: State['text']) {
this.isActive = true
this.text = text
},
setSuccessToast(text: State['text']) {
this.setToast(text)
this.color = 'success'
},
setErrorToast(text: State['text']) {
this.setToast(text)
this.color = 'error'
},
},
})
TheToastコンポーネントでは表示テキスト・Colorを直接指定していました。
Stateの値を利用して動的にします。~/src/components/Toast/TheToast.vue
を以下のようにします。
<script setup lang="ts">
import { useToastStore } from '~/store/toast'
const toastStore = useToastStore()
</script>
<template>
<VSnackbar
v-model="toastStore.isActive"
multi-line
timeout="1500"
:color="toastStore.color"
location="top center"
variant="tonal"
class="py-2"
@on-vnode-unmounted="toastStore.unsetSnackbar()"
>
{{ toastStore.text }}
</VSnackbar>
</template>
続いて、呼び出し側に2つのボタンを設置します。~/src/pages/index.vue
を以下のようにします。
<script setup lang="ts">
import { useToastStore } from '~/store/toast'
const toastStore = useToastStore()
</script>
<template>
<VContainer>
<VBtn color="success" @click="toastStore.setSuccessToast('成功しました!')">
成功トーストの表示</VBtn
>
<VBtn color="error" @click="toastStore.setErrorToast('失敗しました!')">
失敗トーストの表示</VBtn
>
</VContainer>
</template>
この状態で動作確認します。
以下のようにそれぞれボタンクリックし、
トーストの表示が動的になっていればokです!!
ここまでで実装は完了しました!!
最後にPiniaのテストコードを書いてみたいと思います!!
おまけ:Piniaのテストについて
Piniaのテストについては、公式ドキュメントにも記載があります。
Testing Stores | Pinia
上記の参考コードをもとに、今回作成したトースト用のストア、
コンポーネントに対しテストコードを実装したいと思います。
トースト用ストアのテスト
~/src/tests/store/toast.spec.ts
を作成し以下のようにします。
import { useToastStore } from '@/store/toast'
import { createPinia, setActivePinia } from 'pinia'
const toastText = 'トーストのテキスト'
describe('Toast Store', () => {
beforeEach(() => {
// 毎テストごとにStore初期化
setActivePinia(createPinia())
})
it('初期値', () => {
const toastStore = useToastStore()
expect(toastStore.$state).toEqual({
isActive: false,
text: null,
color: 'success',
})
})
it('SuccessToastの表示', () => {
const toastStore = useToastStore()
toastStore.setSuccessToast(toastText)
expect(toastStore.$state).toEqual({
isActive: true,
text: toastText,
color: 'success',
})
})
it('ErrorToastの表示', () => {
const toastStore = useToastStore()
toastStore.setErrorToast(toastText)
expect(toastStore.$state).toEqual({
isActive: true,
text: toastText,
color: 'error',
})
})
})
beforeEachでテストごとに初期化しています。
おわり
今回はVuetifyのSnackbarコンポーネントを活用し、
Nuxt3でどこからでも呼び出すことができるトーストの実装・Piniaのテストコードについて紹介しました。
トースト表示はログイン/ログアウトや、
何らかのイベントの結果に応じて表示されることが多いです。
今回Nuxt3 + Vuetifyで実装する機会があったため、実装例を紹介しました!
今回実装したコードは、以下のリポジトリにpushしております!
https://github.com/p-t-a-p-1/nuxt3-vuetify-toast