フロントエンド初心者がVuetifyを使って開発したときの備忘録

## 1. はじめに
先日、社内研修で初めてフロントエンドをVuetifyを用いて開発したので、そこで開発するときに調べたことをまとめた備忘録です。完全にフロントエンド初心者なので、変なところがあるかもしれません。

### 1.1 バージョン
node : v20.17.0
vue : 3.5.11
vuetify : 3.7.2
vite ：5.4.10


### 1.2. 対象者
- 初めてVuetifyを触る人
- Vueを[tutorial](https://ja.vuejs.org/tutorial/)をやった程度に理解している人

## 2．Vuetifyとは
Vueにおいて、material designを実現するUIフレームワークです。コンポーネントがmaterial designで既に作成されており、単純なアプリならほとんどデザインすることなく開発することが可能です。

## 3. 環境構築
今回の研修では途中でVuetifyを使用することにしたので、最初はvueプロジェクトの作成から行います。最初からVuetifyを使うことが決まっている場合は, `npm create vuetify@latest` を使用します。
### 3.1 vueプロジェクトを作成
`npx create-vue` でvueプロジェクトを作成します。設定は単体テストを行いたいので, vitestを含めて以下のように設定しました。
```
Vue.js - The Progressive JavaScript Framework

√ Project name: ... vuetify-sample-project
√ Add TypeScript? ... Yes
√ Add JSX Support? ... No
√ Add Vue Router for Single Page Application development? ... Yes
√ Add Pinia for state management? ... Yes
√ Add Vitest for Unit Testing? ... Yes
√ Add an End-to-End Testing Solution? » No
√ Add ESLint for code quality? » Yes
√ Add Prettier for code formatting? ... Yes

Scaffolding project in ~~~~~~~/vuetify-sample-project...

Done. Now run:

  cd vuetify-sample-project
  npm install
  npm run format
  npm run dev

```
次にVuetifyをインストールします。
```
npm install vuetify@latest --save
```
Vueプロジェクト作成時に生成された`./src/main.ts`にてVuetifyを使うように設定します。Vuetifyの挙動をわかりやすくするために、main.cssは読み込まないようにしておきます。

```main.ts
// import './assets/main.css'
... 略
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'

... 略

const vuetifyTheme = {
  dark: false,
  colors: {
    primary: '#FEF7FF',
    secondary: '#E8DEF8',
    error: '#B00020',
    info: '#2196F3',
    success: '#4CAF50',
    warning: '#FB8C00',
    iconBackground: '#EADDFF',
    button: '#ECE6F0',
  },
  variables: {
    'border-color': '#000000',
    'border-opacity': 0.12,
  },
}

const vuetify = createVuetify({
  components,
  directives,
  theme: {
    defaultTheme: 'vuetifyTheme',
    themes: {
      vuetifyTheme,
    },
  },
})

... 略

const app = createApp(App)

... 略

app.use(vuetify)

```
componentsでVuetifyにて使うコンポーネント、themeで使用する色などのスタイルを設定することができます。試しに `./src/App.vue`で以下のように書いてみます。
```App.vue
<script setup lang="ts">
import TestHeader from './components/TestHeader.vue';
</script>
<template>
  <v-app>
    <TestHeader></TestHeader>
  </v-app>
</template>
```
```TestHeader.vue
<script setup lang="ts"></script>
<template>
  <v-app-bar color="primary">AppBar</v-app-bar>
</template>

```
`npm run dev`を実行して、URLにアクセスすると、vuetifyThemeにてprimaryに設定した色でヘッダーが現れます.
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515297/dceb351a-1858-e21e-25c5-b7f187b6eb91.png)

これでVuetifyが使えるようになりました。

## 4. 開発
ここでは研修の開発において、Vuetifyの使い方でつまづいた部分を紹介します。
Vuetifyにおいてmainタグにあたるv-mainタグを`./src/App.vue`に追加します。

```App.vue
<script setup lang="ts">
import HomeView from '@/views/HomeView.vue'
</script>

<template>
  <v-app>
    <TestHeader></TestHeader>
    <v-main>
      <HomeView></HomeView>
    </v-main>
  </v-app>
</template>
```
HomeView.vueの中にコンポーネントを入れていろいろ試してみます。

### 4.1 スタイルを変えたい
tailwindcssと同様にclassでデザインを指定することができます。ただし、tailwindcssとは微妙に記法が違うので注意が必要です。またVuetifyのコンポーネントに関してはプロパティで背景色などを指定することができたり、styleからも指定することができます。
```DesignComponent.vue
<script setup lang="ts"></script>

<template>
  <v-btn color="secondary">デザイン１</v-btn>
  <v-btn style="background-color: blue">デザイン2</v-btn>
  <v-btn class="bg-black">デザイン3</v-btn>
</template>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515297/822f7004-d30b-6872-729c-a4a0c43baa77.png)


### 4.2 オブジェクトを横並べにしたい

#### 4.2.1 class='d-flex'を使う
classでd-flexを指定することでstyleのdisplay flexを用いるのと同様の扱いになります。
```FlexContainer.vue
<script setup lang="ts"></script>

<template>
  <div class="d-flex">
    <div>あああ</div>
    <div>いいい</div>
    <div>ううう</div>
  </div>
</template>

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515297/dc257a4a-a274-a634-82d9-35b94ae48cfe.png)

#### 4.2.2 v-rowを使う
v-rowというVuetifyのコンポーネントを用いることでも実現できます。
```FlexContainer.vue
<script setup lang="ts"></script>

<template>
  <v-row>
    <div>あああ</div>
    <div>いいい</div>
    <div>ううう</div>
  </v-row>
</template>

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515297/b76cabc2-2c56-2d15-16b3-37c5342c5133.png)


#### 4.2.3 整列について
classから指定することで、並び方を指定できます。v-rowにおいてもclassを設定することで同様のことができます。
##### 左寄せ
```FlexContainer.vue
<script setup lang="ts"></script>

<template>
  <div class="d-flex justify-start">
    <div>あああ</div>
    <div>いいい</div>
    <div>ううう</div>
  </div>
</template>
```
##### 右寄せ
```FlexContainer.vue
<script setup lang="ts"></script>

<template>
  <div class="d-flex justify-end">
    <div>あああ</div>
    <div>いいい</div>
    <div>ううう</div>
  </div>
</template>
```
##### 等間隔
```FlexContainer.vue
<script setup lang="ts"></script>

<template>
  <div class="d-flex justify-space-between">
    <div>あああ</div>
    <div>いいい</div>
    <div>ううう</div>
  </div>
</template>
```

##### v-colを使う
v-colを用いても可能です。デフォルトだと横幅をv-colタグを使った個数で等分されます。colsプロパティを指定することで、列幅をある程度指定可能です。

```RowColContainer.vue
<script setup lang="ts"></script>

<template>
  <div class="d-flex">
    <v-col cols="2">あああ</v-col>
    <v-col cols="3">いいい</v-col>
    <v-col cols="4">ううう</v-col>
  </div>
</template>

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515297/e4150184-605e-d826-fa5e-58a186a00c78.png)

### 4.3 リストを表示したい
v-listとv-list-itemというものを使います。heightを指定すれば自動でスクロール付きの表示になります。またkeyを使わないとwarningが出るので、設定しておきます。
```ListComponent.vue
<script setup lang="ts">
const names = [
  'Taro','Yoko','Hiro','Akira','Sakura','Kenji',
  'Mina','Ryo','Aiko','Naoki','Emi','Kazu',
  'Miki','Sho','Aya'
]
</script>

<template>
  <v-list height="200">
    <v-list-item v-for="name in names" :key="name">
      {{ name }}
    </v-list-item>
  </v-list>
</template>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515297/49e4d492-4eee-07e3-8f4b-4d42c68b5f79.png)

### 4.4 バリデーションがしたい
Vuetifyにある入力コンポーネントに対してrulesを設定します。ここではテキスト入力に対して、入力がされているかどうかをバリデーションします。
適用したいバリデーションルールをrulesで定義して、テキスト入力コンポーネントで設定します。
バリデーションしたい入力コンポーネントをVFormで囲い、refを設定することでバリデーションできるようになります。

```ValidationForm.vue
<script setup lang="ts">
import type { VForm } from 'vuetify/components'
import { ref } from 'vue'

const form = ref<VForm>()
const input = ref('')
const rules = {
  nonNull: (v: string) => v.length > 0 || '文字を入力してください。',
}

const validate = async () => {
  return form.value ? (await form.value.validate()).valid : false
}

const validateClick = async () => {
  const result = await validate()
  if (result) {
    alert('送信完了')
  }
}
</script>

<template>
  <v-container max-width="500">
    <v-form ref="form">
      <v-text-field v-model="input" :rules="[rules.nonNull]"></v-text-field>
      <v-btn @click="validateClick">送信する</v-btn>
    </v-form>
  </v-container>
</template>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515297/51967437-6853-bd28-a726-0cd2a56517ec.png)

### 4.5 localeがjaにの日付入力をしたい
日付を入力するコンポーネントでv-date-inputを使います。このコンポーネントはデフォルトで読み込まれていないので、 main.tsを書き換えます。
```main.ts
...略
import { VDateInput } from 'vuetify/labs/VDateInput'
...略
const vuetify = createVuetify({
-  components,
+  components: { ...components, VDateInput },  
  directives,
  theme: {
    defaultTheme: 'vuetifyTheme',
    themes: {
      vuetifyTheme,
    },
  },
})

```

```DateComponent.vue
<script setup lang="ts"></script>

<template>
  <v-container max-width="500">
    <VDateInput></VDateInput>
  </v-container>
</template>

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515297/80cf44ea-f792-2da3-18ec-85287a9eff10.png)
デフォルトだとlocaleがenであるので、jaに変更します。v-locale-providerを用いることで簡単にlocaleを変更することができます。
```DateComponent.vue
<script setup lang="ts"></script>

<template>
  <v-container max-width="500">
    <v-locale-provider locale="ja">
      <VDateInput></VDateInput>
    </v-locale-provider>
  </v-container>
</template>

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515297/2628377b-29e0-424c-9da3-083391d5b9ec.png)



### 4.6 ダイアログを表示したい
v-dialogタグのmodelディレクティブに開閉を決定するリアクティブな変数を設定します。
```DialogComponent.vue
<script setup lang="ts">
import { ref } from 'vue'

const isOpen = ref(false)
</script>

<template>
  <v-btn @click="isOpen = true">開く </v-btn>
  <v-dialog v-model="isOpen">
    <v-card>
      ダイアログが開いたよ
      <v-btn @click="isOpen = false">閉じる</v-btn>
    </v-card>
  </v-dialog>
</template>
```
![レコーディング 2024-11-29 160721-f69i3sr5sb8ibr8s13uozckrbc.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515297/622fed0b-4c48-4c16-bccb-9f664d18be25.gif)


## 5. テスト
ここではvitestを用いた単体テストの設定方法をまとめます。
先ほど作ったvalidationForm.vueが正しくバリデーションできているかの単体テストを作ることにします。
vitest.config.tsを編集して、Vuetifyを使用するように設定します。ここではtest.setup.tsをディレクトリのトップに作成し、vitest.config.tsにおいてsetUpFilesで指定します。
```diff_typescript
import { fileURLToPath } from 'node:url'
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
import viteConfig from './vite.config'
+import vuetify from 'vite-plugin-vuetify'
import path from 'path'
export default mergeConfig(
  viteConfig,
  defineConfig({
+    plugins: [
+      vuetify({
+        autoImport: true,
+      }),
+    ],
    test: {
      environment: 'jsdom',
      exclude: [...configDefaults.exclude, 'e2e/**'],
      root: fileURLToPath(new URL('./', import.meta.url)),
+      server: {
+        deps: {
+          inline: ['vuetify'],
+        },
+      },
+      setupFiles: path.resolve(__dirname, 'test.setup.ts'),
    },
  }),
)

```
```test.setup.ts
import { config } from '@vue/test-utils'
import { createVuetify } from 'vuetify'

const vuetify = createVuetify()

config.global.plugins = [vuetify]
```

これでVuetifyのコンポーネントに対して、vitestが動くようになります。試しに次のテストを実行してみます
```Validation.spec.ts
import { describe, it, expect } from 'vitest'

import { mount } from '@vue/test-utils'
import ValidationForm from '../ValidationForm.vue'
describe('Validation', () => {
  it('renders properly', async () => {
    const wrapper = mount(ValidationForm)
    const vbtn = wrapper.find('button')
    await vbtn.trigger('click')
    expect(wrapper.text()).toContain('文字を入力してください。')
  })
})

```

```
 ✓ src/components/__tests__/Validation.spec.ts (1)
   ✓ Validaion (1)
     ✓ validate properly

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  13:55:44
   Duration  438ms

 PASS  Waiting for file changes...
       press h to show help, press q to quit
```

v-app-barなどの一部のコンポーネントはv-appタグで囲まないとエラーになっていします。そのような場合はv-appにslotとしてテストしたいコンポーネントを渡すことで、テストすることができます。

```TestHeader.vue
<script setup lang="ts"></script>

<template>
  <v-app-bar color="primary">AppBar</v-app-bar>
</template>
```
```TestHeader.spec.ts
import { describe, it, expect } from 'vitest'

import { mount } from '@vue/test-utils'
import TestHeader from '../TestHeader.vue'
import { VApp } from 'vuetify/components'
global.ResizeObserver = require('resize-observer-polyfill')
describe('TestHeader', () => {
  it('render properly', async () => {
    const wrapper = mount(VApp, {
      slots: {
        default: TestHeader,
      },
    })
    expect(wrapper.text()).toContain('AppBar')
  })
})

```

## 6. さいごに
研修で初めてのフロントエンド開発を行いましたが、フレームワークを使って目的のものを開発することができました。今回開発したものはvuetifyの機能の一部を使うだけで済みましたが、他の機能も使っていきたいと思います。

最後まで読んでいただきありがとうございました。

## 7. 参考
https://vuetifyjs.com/ja/