VueとVuetifyではじめるフロントエンド開発入門

## はじめに

武藤と申します。
日本総合システム株式会社でシステム開発に従事しています。

本記事では、フロントエンド開発に挑戦したい方へ向けて

- Vue を用いたフロントアプリケーションの構築
- Vuetify を用いたコンポーネントの開発

について解説します。
ソースコードは GitHub で公開しています。

https://github.com/yskmt2018/prototyping-vuetify/

### Vue とは

https://ja.vuejs.org/

いわゆる【フロントエンド 3 大フレームワーク】の一つで、特にフロントエンド開発に入門するにはうってつけのフレームワークだと思います。

Vue の最大の特徴が **単一ファイルコンポーネント（SFC）** です。
一つのファイル（`*.vue`）の中に、ロジック・ビュー・スタイルをまとめて記述します。

```vue:src/components/SFCsample.vue
<script setup lang="ts">
// <script> ブロックにロジックを記述する。言語は JavaScript（TypeScript）
const companyName = "Nippon Sogo Systems, Inc.";
</script>

<template>
  <!-- <template> ブロックにビューを記述する。言語は HTML -->
  <div class="underline">{{ companyName }}</div>
</template>

<style lang="css" scoped>
/* <style> ブロックにスタイルを記述する。言語は CSS */
.underline:hover {
  text-decoration: underline;
}
</style>
```

この特徴により、コンポーネントをシンプルかつ、カプセル化して開発を進めることができます。

### Vuetify とは

https://vuetifyjs.com/ja/

Vue アプリケーションのための UI ライブラリで、Google の Material Design の仕様に基づいて実装されたコンポーネントが多数提供されています。

Vuetify を利用することで、アプリケーション全体の外観を統一し、UX の向上に寄与することができます。
様々な機能や柔軟なカスタマイズ性を備えたライブラリですが、本記事では基礎的な事柄を中心に解説します。

## 環境構築

- OS：Windows 11
- IDE：Visual Studio Code

### Node.js のインストール

https://nodejs.org/ja/

Node.js は **JavaScript の実行環境** です。ウェブサイトからインストーラをダウンロードしてインストールしてください。
以下のコマンドでインストールを確認できます。

```bash
node --version
npm --version
```

### プロジェクトの構築

以下のコマンドで新規にプロジェクトを構築できます。

```bash
npm create vuetify@latest
```

コマンドラインでいくつか質問されるので、以下のように回答してください。

| 質問                                            |              回答              |
| ----------------------------------------------- | :----------------------------: |
| Project name:                                   |           任意の名前           |
| Which preset would you like to install?         | Barebones (Only Vue & Vuetify) |
| Use TypeScript?                                 |              Yes               |
| Would you like to install dependencies with ... |              npm               |
| Install Dependencies?                           |              Yes               |

:::note
本記事は学習目的のため、必要最小限の【Barebones】プリセットを選択しています。
本格的に開発を行う場合は【Default】または【Recommended】プリセットの選択をおすすめします。
:::

:::note warn
プロキシ環境下でインストールに失敗する場合は、以下のコマンドでプロキシを設定できます。

```bash
npm config --global set proxy http://proxy.xxxxx.co.jp:8080
npm config --global set https-proxy http://proxy.xxxxx.co.jp:8080
```

以下のコマンドで現在の設定を確認できます。

```bash
npm config --global list
```

:::

作成されたプロジェクトフォルダに移動した後、以下のコマンドでアプリケーションを起動できます。

```bash
npm run dev
```

ブラウザで`http://localhost:3000/`にアクセスし、トップページが表示されれば完了です！

- 画面表示例
  ![welcome-to-vuetify.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3799996/66d33807-dfe8-4072-9d73-ed5f4132a6e0.png "トップページ")


## 実装概要

### App.vue について

`App.vue`は Vue アプリケーションの **トップレベルコンポーネント** です。
作成された`App.vue`を確認してみます。

```vue:src/App.vue
<template>
  <v-app>
    <v-main>
      <HelloWorld />
    </v-main>
  </v-app>
</template>

<script setup lang="ts">
  //
</script>
```

`<v-app>`ブロックが Vuetify の **ルートレイアウトコンポーネント** となります。
`App.vue`の一番外側に配置することで、アプリケーション全体で Vuetify ライブラリが使用可能になります。
そして`<v-main>`ブロックが Vuetify の **メインコンテンツエリア** となります。

:::note
Vuetify ではアプリケーションの画面構成（ **ワイヤーフレーム** ）のサンプルが多数提供されています。
ベースラインの構成から Discord・Steam インスパイアの構成まで様々なサンプルがソースコード込みで閲覧できます。

興味のある方は [公式ドキュメント](https://vuetifyjs.com/ja/getting-started/wireframes/ "ワイヤーフレーム") を参照してください。
:::

### コンポーネントについて

コンポーネントは Vue アプリケーションの画面を構成する、機能を持つ UI 要素です。
思い浮かべやすいのはボタン・テキストフィールド・チェックボックス等でしょうか。

後ほど実装の解説を行いますが、その前にいくつかの重要な構文を説明します。

#### リアクティブな変数の宣言（ref）

`ref()`でリアクティブ変数を宣言します。リアクティブ変数はコンポーネントの内部や外部と値の紐づけ（ **バインディング** ）をする際に使用します。
リアクティブ変数の値が更新されると、バインディング先の値も同期して更新されます。

#### プロパティの宣言（defineProps）

`defineProps()`で親コンポーネントに設定させるプロパティを宣言します。
リアクティブ変数を指定することも可能です。その場合は **単方向（親 ⇒ 子）** のバインディングとなります。

#### 双方向バインディングプロパティの宣言（defineModel）

`defineModel()`で **双方向（親 ⇔ 子）** にバインディングするプロパティを宣言します。
子コンポーネントから親コンポーネントに対して値の更新を同期する際に使用します。

#### 発行するイベントの宣言（defineEmits）

`defineEmits()`で親コンポーネントに通知するイベントを宣言します。
子コンポーネントから親コンポーネントに対してデータや通知を送信する際に使用します。

#### 明示的に公開するプロパティの宣言（defineExpose）

`defineExpose()`で親コンポーネントに公開するプロパティを宣言します。
子コンポーネントから親コンポーネントに対して内部状態やメソッドを公開する際に使用します。

#### スロットアウトレットの宣言（slot）

Vue の **スロット（差し込み口）** 機能により、親コンポーネントから子コンポーネントに対して任意のコンポーネントを差し込むことができます。
`<slot>`で **スロットアウトレット** を宣言します。スロットアウトレットは親コンポーネントが提供した **スロットコンテンツ** を表示する場所を示します。
スロット機能を利用することで、後ほど解説するテンプレートの作成などコンポーネントをより柔軟に使用できるようになります。

## 実装解説

ここからはソースコードを基に実装の要点を解説していきます。
親コンポーネントによる使用例も合わせて掲載しています。ぜひ【実装の詳細と解説を見る】でご確認ください。

:::note
画面表示例の視認性を考慮し、Vuetify のデフォルトテーマを変更しています。
`src/plugins/vuetify.ts`の以下の記述を修正することで変更できます。

```diff_typescript
theme: {
- defaultTheme: 'dark',
+ defaultTheme: 'light',
},
```

**テーマの設定** は Vuetify において非常に重要なトピックですが、本記事では説明を割愛します。
興味のある方は [公式ドキュメント](https://vuetifyjs.com/ja/features/theme/ "テーマの設定") を参照してください。
:::

### コンポーネント

#### テキストフィールド（v-text-field）

`v-text-field`でテキストフィールドを作成できます。1 行以内のテキストを入力する場合に使用します。

https://vuetifyjs.com/ja/components/text-fields/

- 画面表示例
  ![example-vtextfield.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3799996/f12294d2-99ef-4b8b-9eaf-d20ed248c432.png "例：テキストフィールド")


<details>
  <summary>実装の詳細と解説を見る</summary>

- 実装例

```vue:src/components/InputText.vue
<script setup lang="ts">
import { ref } from "vue";

defineProps<{
  /** タイトル */
  title: string;
  /** ヒント（任意） */
  hint?: string;
  /** 入力欄のプレースホルダ（任意） */
  placeholder?: string;
  /** コンポーネントの横幅 */
  width: string | number;
}>();

/** 入力された文字列 */
const inputted = defineModel<string>("inputted", { required: true });
// 入力の更新を通知
defineEmits(["update"]);

/** 非活性フラグ */
const disabled = ref<boolean>(false);
// 非活性フラグ操作関数を公開
defineExpose({
  setDisabled: () => (disabled.value = true),
  setEnabled: () => (disabled.value = false),
});
</script>

<template>
  <div>
    <div class="text-h6">{{ $props.title }}</div>
    <div class="text-caption" v-if="$props.hint">{{ $props.hint }}</div>
    <v-text-field
      v-model:model-value="inputted"
      @update:model-value="$emit('update')"
      :disabled="disabled"
      :placeholder="$props.placeholder"
      :width="$props.width"
      density="compact"
      hide-details="auto"
      color="primary"
      base-color="tertiary"
    ></v-text-field>
  </div>
</template>
```

- 親コンポーネントによる使用例

```vue
<script setup lang="ts">
import { ref } from "vue";
import InputText from "@/components/InputText.vue";

const text = ref<string>("");
</script>

<template>
  <v-container class="my-10">
    <v-row>
      <InputText
        v-model:inputted="text"
        title="テキストフィールド"
        hint="1 行以内のテキストを入力する場合に使用します。"
        width="400"
      ></InputText>
      <div>入力された値⇒{{ text }}</div>
    </v-row>
  </v-container>
</template>
```

- 解説
  - InputText.vue
    - `v-model`を使用して`<v-text-field>`とリアクティブ変数を双方向バインディングしています
      - 入力された値は`<v-text-field>`を通してリアクティブ変数に格納されます
    - `defineModel()`を使用してリアクティブ変数を親コンポーネントから双方向バインディング可能にしています
      - `{ required: true }`としてバインディングを強制（`undefined`を許容しない）できます
    - `defineProps()`を使用してコンポーネントのプロパティを規定しています
      - プロパティは`<template>`ブロック内部からは`$props`でアクセスできます
  - 親コンポーネント
    - `v-model`を使用して`<InputText>`とリアクティブ変数を双方向バインディングしています
      - `<InputText>`を通して入力された値を取得し、画面に表示しています

</details>

#### テキストエリア（v-textarea）

`v-textarea`でテキストエリアを作成できます。1 行以上のテキストを入力する場合に使用します。

https://vuetifyjs.com/ja/components/textareas/

- 画面表示例
  ![example-vtextarea.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3799996/a9ff4aeb-de58-493e-a2e6-1371c0d5e842.png "例：テキストエリア")


<details>
  <summary>実装の詳細と解説を見る</summary>

- 実装例

```vue:src/components/Textarea.vue
<script setup lang="ts">
import { ref } from "vue";

defineProps<{
  /** タイトル */
  title: string;
  /** ヒント（任意） */
  hint?: string;
  /** 入力欄のプレースホルダ（任意） */
  placeholder?: string;
  /** コンポーネントの横幅 */
  width: string | number;
}>();

/** 入力された文字列 */
const inputted = defineModel<string>("inputted", { required: true });
// 入力の更新を通知
defineEmits(["update"]);

/** 非活性フラグ */
const disabled = ref<boolean>(false);
// 非活性フラグ操作関数を公開
defineExpose({
  setDisabled: () => (disabled.value = true),
  setEnabled: () => (disabled.value = false),
});
</script>

<template>
  <div>
    <div class="text-h6">{{ $props.title }}</div>
    <div class="text-caption" v-if="$props.hint">{{ $props.hint }}</div>
    <v-textarea
      v-model:model-value="inputted"
      @update:model-value="$emit('update')"
      :disabled="disabled"
      :placeholder="$props.placeholder"
      :width="$props.width"
      density="compact"
      counter
      persistent-counter
      color="primary"
      base-color="tertiary"
    ></v-textarea>
  </div>
</template>
```

- 親コンポーネントによる使用例

```vue
<script setup lang="ts">
import { ref } from "vue";
import Textarea from "@/components/Textarea.vue";

const text = ref<string>("");
</script>

<template>
  <v-container class="my-10">
    <v-row>
      <Textarea
        v-model:inputted="text"
        title="テキストエリア"
        hint="1 行以上のテキストを入力する場合に使用します。"
        width="400"
      ></Textarea>
      <div>入力された値⇒{{ text }}</div>
    </v-row>
  </v-container>
</template>
```

- 解説
  - Textarea.vue
    - `v-model`を使用して`<v-textarea>`とリアクティブ変数を双方向バインディングしています
      - 入力された値は`<v-textarea>`を通してリアクティブ変数に格納されます
    - `defineModel()`を使用してリアクティブ変数を親コンポーネントから双方向バインディング可能にしています
      - `{ required: true }`としてバインディングを強制（`undefined`を許容しない）できます
    - `defineProps()`を使用してコンポーネントのプロパティを規定しています
      - プロパティは`<template>`ブロック内部からは`$props`でアクセスできます
  - 親コンポーネント
    - `v-model`を使用して`<Textarea>`とリアクティブ変数を双方向バインディングしています
      - `<Textarea>`を通して入力された値を取得し、画面に表示しています

</details>

#### ラジオボタン（v-radio）

`v-radio`と`v-radio-group`でラジオボタンを作成できます。選択肢の中から 1 つだけを選択する場合に使用します。

https://vuetifyjs.com/ja/components/radio-buttons/

- 画面表示例
  ![example-vradio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3799996/f3b8beb6-dc5d-42ba-8bea-3ef8f06f3665.png "例：ラジオボタン")


<details>
  <summary>実装の詳細と解説を見る</summary>

- 実装例

```vue:src/components/RadioButton.vue
<script setup lang="ts">
import { ref } from "vue";

defineProps<{
  /** タイトル */
  title: string;
  /** ヒント（任意） */
  hint?: string;
  /** 選択肢のリスト（ラベル、識別子） */
  choices: { label: string; value: string }[];
  /** コンポーネントの横幅 */
  width: string | number;
}>();

/** 選択された識別子 */
const selected = defineModel<string>("selected", { required: true });
// 選択の更新を通知
defineEmits(["update"]);

/** 非活性フラグ */
const disabled = ref<boolean>(false);
// 非活性フラグ操作関数を公開
defineExpose({
  setDisabled: () => (disabled.value = true),
  setEnabled: () => (disabled.value = false),
});
</script>

<template>
  <div>
    <div class="text-h6">{{ $props.title }}</div>
    <div class="text-caption" v-if="$props.hint">{{ $props.hint }}</div>
    <v-radio-group
      v-model:model-value="selected"
      @update:model-value="$emit('update')"
      :disabled="disabled"
      :width="$props.width"
      hide-details="auto"
    >
      <v-radio
        v-for="choice in $props.choices"
        :key="choice.value"
        :label="choice.label"
        :value="choice.value"
        color="primary"
        base-color="tertiary"
      ></v-radio>
    </v-radio-group>
  </div>
</template>
```

- 親コンポーネントによる使用例

```vue
<script setup lang="ts">
import { ref } from "vue";
import RadioButton from "@/components/RadioButton.vue";

const selected = ref<string>("");
const choices = [
  { label: "プロパティの宣言", value: "defineProps" },
  { label: "双方向バインディングプロパティの宣言", value: "defineModel" },
  { label: "発行するイベントの宣言", value: "defineEmits" },
  { label: "明示的に公開するプロパティの宣言", value: "defineExpose" },
];
</script>

<template>
  <v-container class="my-10">
    <v-row>
      <RadioButton
        v-model:selected="selected"
        title="ラジオボタン"
        hint="選択肢の中から 1 つだけを選択する場合に使用します。"
        :choices="choices"
        width="400"
      ></RadioButton>
      <div>選択された値⇒{{ selected }}</div>
    </v-row>
  </v-container>
</template>
```

- 解説
  - RadioButton.vue
    - 親コンポーネントから選択肢のリストを受け取り、その数だけ`<v-radio>`を使用してラジオボタンを表示しています
      - `label`が選択肢のラベル、`value`が選択肢の識別子となります
    - `v-model`を使用して`<v-radio-group>`とリアクティブ変数を双方向バインディングしています
      - 選択された値（識別子）は`<v-radio-group>`を通してリアクティブ変数に格納されます
  - 親コンポーネント
    - 選択肢のリストを作成し`<RadioButton>`のプロパティに指定しています
      - 選択肢を【`string`型の`label`属性】と【`string`型の`value`属性】を持つオブジェクトで表現しています
    - `v-model`を使用して`<RadioButton>`とリアクティブ変数を双方向バインディングしています
      - `<RadioButton>`を通して選択された値（識別子）を取得し、画面に表示しています

</details>

#### チェックボックス（v-checkbox）

`v-checkbox`でチェックボックスを作成できます。選択肢の中から 1 つ以上選択する場合に使用します。

https://vuetifyjs.com/ja/components/checkboxes/

- 画面表示例
  ![example-vcheckbox.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3799996/1a5bc1c3-86c1-4336-9f74-ffbdc1936d98.png "例：チェックボックス")


<details>
  <summary>実装の詳細と解説を見る</summary>

- 実装例

```vue:src/components/Checkbox.vue
<script setup lang="ts">
import { ref } from "vue";

defineProps<{
  /** タイトル */
  title: string;
  /** ヒント（任意） */
  hint?: string;
  /** 選択肢のリスト（ラベル、識別子） */
  choices: { label: string; value: string }[];
  /** コンポーネントの横幅 */
  width: string | number;
}>();

/** 選択された識別子 */
const selected = defineModel<string[]>("selected", { required: true });
// 選択の更新を通知
defineEmits(["update"]);

/** 非活性フラグ */
const disabled = ref<boolean>(false);
// 非活性フラグ操作関数を公開
defineExpose({
  setDisabled: () => (disabled.value = true),
  setEnabled: () => (disabled.value = false),
});
</script>

<template>
  <div>
    <div class="text-h6">{{ $props.title }}</div>
    <div class="text-caption" v-if="$props.hint">{{ $props.hint }}</div>
    <v-checkbox
      v-model:model-value="selected"
      @update:model-value="$emit('update')"
      :disabled="disabled"
      v-for="choice in $props.choices"
      :key="choice.value"
      :label="choice.label"
      :value="choice.value"
      :width="$props.width"
      density="compact"
      hide-details="auto"
      color="primary"
      base-color="tertiary"
    ></v-checkbox>
  </div>
</template>
```

- 親コンポーネントによる使用例

```vue
<script setup lang="ts">
import { ref } from "vue";
import Checkbox from "@/components/Checkbox.vue";

const selected = ref<string[]>([]);
const choices = [
  { label: "プロパティの宣言", value: "defineProps" },
  { label: "双方向バインディングプロパティの宣言", value: "defineModel" },
  { label: "発行するイベントの宣言", value: "defineEmits" },
  { label: "明示的に公開するプロパティの宣言", value: "defineExpose" },
];
</script>

<template>
  <v-container class="my-10">
    <v-row>
      <Checkbox
        v-model:selected="selected"
        title="チェックボックス"
        hint="選択肢の中から 1 つ以上選択する場合に使用します。"
        :choices="choices"
        width="400"
      ></Checkbox>
      <div>選択された値⇒{{ selected }}</div>
    </v-row>
  </v-container>
</template>
```

- 解説
  - Checkbox.vue
    - 親コンポーネントから選択肢のリストを受け取り、その数だけ`<v-checkbox>`を使用してチェックボックスを表示しています
      - `label`が選択肢のラベル、`value`が選択肢の識別子となります
    - `v-model`を使用して`<v-checkbox>`とリアクティブ変数を双方向バインディングしています
      - 選択された値（識別子）**のリスト** は`<v-checkbox>`を通してリアクティブ変数に格納されます
  - 親コンポーネント
    - 選択肢のリストを作成し`<Checkbox>`のプロパティに指定しています
      - 選択肢を【`string`型の`label`属性】と【`string`型の`value`属性】を持つオブジェクトで表現しています
    - `v-model`を使用して`<Checkbox>`とリアクティブ変数を双方向バインディングしています
      - `<Checkbox>`を通して選択された値（識別子）**のリスト** を取得し、画面に表示しています

</details>

#### セレクトボックス（v-select）

`v-select`でセレクトボックスを作成できます。多くの選択肢の中から 1 つだけを選択する場合に使用します。

https://vuetifyjs.com/ja/components/selects/

- 画面表示例
  ![example-vselect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3799996/a0c70dca-800e-4469-b36e-dcb9aeba4789.png "例：セレクトボックス")


<details>
  <summary>実装の詳細と解説を見る</summary>

- 実装例

```vue:src/components/Selectbox.vue
<script setup lang="ts">
import { ref } from "vue";

defineProps<{
  /** タイトル */
  title: string;
  /** ヒント（任意） */
  hint?: string;
  /** 選択肢のリスト（ラベル、識別子） */
  choices: { label: string; value: string }[];
  /** コンポーネントの横幅 */
  width: string | number;
}>();

/** 選択された識別子 */
const selected = defineModel<string>("selected", { required: true });
// 選択の更新を通知
defineEmits(["update"]);

/** 非活性フラグ */
const disabled = ref<boolean>(false);
// 非活性フラグ操作関数を公開
defineExpose({
  setDisabled: () => (disabled.value = true),
  setEnabled: () => (disabled.value = false),
});
</script>

<template>
  <div>
    <div class="text-h6">{{ $props.title }}</div>
    <div class="text-caption" v-if="$props.hint">{{ $props.hint }}</div>
    <v-select
      v-model:model-value="selected"
      @update:model-value="$emit('update')"
      :disabled="disabled"
      :items="$props.choices"
      item-title="label"
      item-value="value"
      :width="$props.width"
      density="compact"
      hide-details="auto"
      color="primary"
      base-color="tertiary"
    ></v-select>
  </div>
</template>
```

- 親コンポーネントによる使用例

```vue
<script setup lang="ts">
import { ref } from "vue";
import Selectbox from "@/components/Selectbox.vue";

const selected = ref<string>("");
const choices = [
  { label: "プロパティの宣言", value: "defineProps" },
  { label: "双方向バインディングプロパティの宣言", value: "defineModel" },
  { label: "発行するイベントの宣言", value: "defineEmits" },
  { label: "明示的に公開するプロパティの宣言", value: "defineExpose" },
];
</script>

<template>
  <v-container class="my-10">
    <v-row>
      <Selectbox
        v-model:selected="selected"
        title="セレクトボックス"
        hint="多くの選択肢の中から 1 つだけを選択する場合に使用します。"
        :choices="choices"
        width="400"
      ></Selectbox>
      <div>選択された値⇒{{ selected }}</div>
    </v-row>
  </v-container>
</template>
```

- 解説
  - Selectbox.vue
    - 親コンポーネントから選択肢のリストを受け取り、`<v-select>`の`items`プロパティに指定しています
      - `item-title`プロパティで選択肢のラベル属性名、`item-value`プロパティで選択肢の識別子属性名を指定できます
    - `v-model`を使用して`<v-select>`とリアクティブ変数を双方向バインディングしています
      - 選択された値（識別子）は`<v-select>`を通してリアクティブ変数に格納されます
  - 親コンポーネント
    - 選択肢のリストを作成し`<Selectbox>`のプロパティに指定しています
      - 選択肢を【`string`型の`label`属性】と【`string`型の`value`属性】を持つオブジェクトで表現しています
    - `v-model`を使用して`<Selectbox>`とリアクティブ変数を双方向バインディングしています
      - `<Selectbox>`を通して選択された値（識別子）を取得し、画面に表示しています

</details>

#### ボタン（v-btn）

`v-btn`でボタンを作成できます。何らかのアクションを実行する場合に使用します。

https://vuetifyjs.com/ja/components/buttons/

- 画面表示例
  ![example-vbtn.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3799996/5832bc68-be27-4e1b-9364-49c31458931c.png "例：ボタン")


<details>
  <summary>実装の詳細と解説を見る</summary>

- 実装例

```vue:src/components/Button.vue
<script setup lang="ts">
import { ref } from "vue";

const props = defineProps<{
  /** テキスト */
  text: string;
  /** ボタンの重要度 */
  importance: "primary" | "secondary" | "tertiary";
}>();

// クリックアクションを通知
defineEmits(["click"]);

/** 非活性フラグ */
const disabled = ref<boolean>(false);
// 非活性フラグ操作関数を公開
defineExpose({
  setDisabled: () => (disabled.value = true),
  setEnabled: () => (disabled.value = false),
});

// ボタンの重要度によりスタイルを変更
const variant =
  props.importance === "primary"
    ? "elevated"
    : props.importance === "secondary"
    ? "outlined"
    : "tonal";
</script>

<template>
  <div>
    <v-btn
      @click="$emit('click')"
      :disabled="disabled"
      :text="$props.text"
      min-height="48"
      min-width="96"
      :variant="variant"
      :color="$props.importance"
      :base-color="$props.importance"
    ></v-btn>
  </div>
</template>
```

- 親コンポーネントによる使用例

```vue
<script setup lang="ts">
import { ref } from "vue";
import Button from "@/components/Button.vue";

const count = ref<number>(0);
const clickAction = () => {
  count.value++;
};
</script>

<template>
  <v-container class="my-10">
    <v-row>
      <Button
        @click="clickAction"
        text="プライマリ"
        importance="primary"
        class="mx-2"
      ></Button>
      <Button
        @click="clickAction"
        text="セカンダリ"
        importance="secondary"
        class="mx-2"
      ></Button>
      <Button
        @click="clickAction"
        text="ターシャリ"
        importance="tertiary"
        class="mx-2"
      ></Button>
      <div>クリックされた回数⇒{{ count }}</div>
    </v-row>
  </v-container>
</template>
```

- 解説
  - Button.vue
    - `defineEmits()`を使用してコンポーネントから発行するイベントを規定しています
      - `<template>`ブロック内部からは`$emit()`でアクセスできます
    - `@click`を使用して`<v-btn>`からクリックイベントが発行された時に実行する処理を指定しています
      - `$emit('click')`を指定し、親コンポーネントにクリックイベントを通知しています
    - プロパティでボタンの重要度を 3 種類（プライマリ・セカンダリ・ターシャリ）規定しています
      - 指定された重要度に応じてスタイルを設定しています
  - 親コンポーネント
    - `@click`を使用して`<Button>`からクリックイベントが発行された時に実行する処理を指定しています
      - 数値をカウントアップする関数式を指定し、クリックされた回数を画面に表示しています

</details>

### テンプレート

テンプレートはコンポーネントの一種で、複数のコンポーネントを組み合わせるための雛形です。
既定のコンポーネントを配置するだけでなく、先ほど説明したスロット機能を利用して任意のコンポーネントを配置することもできます。

- 画面表示例
  ![example-formstemplate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3799996/fdcfdb6e-9cc4-4424-a3ae-32c5b0d8b169.png "入力フォームテンプレート")


<details>
  <summary>実装の詳細と解説を見る</summary>

- 実装例

```vue:src/templates/FormsTemplate.vue
<script setup lang="ts">
import Button from "@/components/Button.vue";

defineProps<{
  /** タイトル */
  title: string;
  /** 使用するスロット名のリスト */
  slotNames: string[];
  /** 左側のボタン */
  leftButton: {
    /** テキスト */
    text: string;
    /** クリックアクション */
    clickAction: () => void;
  };
  /** 右側のボタン */
  rightButton: {
    /** テキスト */
    text: string;
    /** クリックアクション */
    clickAction: () => void;
  };
}>();
</script>

<template>
  <div>
    <v-card class="pa-12" width="800">
      <div class="text-h4">{{ $props.title }}</div>
      <v-divider class="my-4" />
      <slot v-for="name in $props.slotNames" :key="name" :name="name" />
      <v-row class="mt-4">
        <Button
          @click="$props.leftButton.clickAction"
          :text="$props.leftButton.text"
          importance="secondary"
        ></Button>
        <v-spacer />
        <Button
          @click="$props.rightButton.clickAction"
          :text="$props.rightButton.text"
          importance="primary"
        ></Button>
      </v-row>
    </v-card>
  </div>
</template>
```

- 親コンポーネントによる使用例

```vue
<script setup lang="ts">
import { ref } from "vue";
import FormsTemplate from "@/templates/FormsTemplate.vue";
import InputText from "@/components/InputText.vue";
import Checkbox from "@/components/Checkbox.vue";

const name = ref<string>("");
const selected = ref<string[]>([]);
const choices = [{ label: "確認しました", value: "checked" }];
</script>

<template>
  <FormsTemplate
    title="入力フォームテンプレート"
    :slot-names="['name', 'confirm']"
    :left-button="{
      text: '戻る',
      clickAction: () => {
        /* 何もしない */
      },
    }"
    :right-button="{
      text: '進む',
      clickAction: () => {
        /* 何もしない */
      },
    }"
  >
    <template #name>
      <InputText
        v-model:inputted="name"
        title="名前"
        width="400"
        class="my-4"
      ></InputText>
    </template>
    <template #confirm>
      <Checkbox
        v-model:selected="selected"
        title="確認事項"
        :choices="choices"
        width="400"
        class="my-4"
      ></Checkbox>
    </template>
  </FormsTemplate>
</template>
```

- 解説
  - FormsTemplate.vue
    - `<slot>`を使用して親コンポーネントにスロットアウトレットを提供しています
      - 親コンポーネントからスロット名のリストを受け取り、名前付きスロットを作成します
    - `<Button>`コンポーネントをテンプレートの左下と右下に配置しています
      - プロパティでボタンのテキストとクリックアクションを規定しています
  - 親コンポーネント
    - `<template #~~~>`を使用してテンプレートにスロットコンテンツを提供しています
      - 使用例では`<InputText>`と`<Checkbox>`を指定しています

</details>

### App.vue

紹介したコンポーネントとテンプレートを組み合わせて、簡単なプロトタイプを作成してみました。

- 画面表示例
  ![overview-application.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3799996/df84a232-1fca-49b0-a94d-fa0e82115ebc.png "お問い合わせフォーム例")


<details>
  <summary>実装の詳細を見る</summary>

- 実装例

```vue:src/App.vue
<script setup lang="ts">
import { ref } from "vue";
import InputText from "@/components/InputText.vue";
import Textarea from "@/components/Textarea.vue";
import RadioButton from "@/components/RadioButton.vue";
import Checkbox from "@/components/Checkbox.vue";
import FormsTemplate from "@/templates/FormsTemplate.vue";

const inputtedName = ref<string>("");
const inputtedMessage = ref<string>("");
const business = [
  { label: "金融システム", value: "financialsystem" },
  { label: "流通システム", value: "distributionsystem" },
  { label: "製造システム", value: "manufacturingsystem" },
  { label: "海事サービス", value: "maritimeservices" },
  { label: "システム技術", value: "systemtechnology" },
  { label: "仙台支社", value: "sendai" },
];
const selectedBusiness = ref<string>("");
const privacy = [{ label: "同意する", value: "agreed" }];
const selectedPrivacy = ref<string[]>([]);
</script>

<template>
  <v-app id="prototype">
    <!-- 簡易的な横幅調整のため両サイドに配置 -->
    <v-navigation-drawer location="left" color="grey-lighten-3" />
    <v-navigation-drawer location="right" color="grey-lighten-3" />

    <v-app-bar>
      <v-img src="/src/assets/logo.svg" max-width="64" class="mx-4" />
      <v-app-bar-title>
        VueとVuetifyではじめるフロントエンド開発入門
      </v-app-bar-title>
    </v-app-bar>

    <v-main>
      <v-container>
        <v-row>
          <v-spacer />
          <FormsTemplate
            title="お問い合わせフォーム例"
            :slot-names="['name', 'business', 'message', 'privacy']"
            :left-button="{
              text: '戻る',
              clickAction: () => {
                /* 何もしない */
              },
            }"
            :right-button="{
              text: '確認',
              clickAction: () => {
                /* 何もしない */
              },
            }"
          >
            <template #name>
              <InputText
                v-model:inputted="inputtedName"
                title="氏名"
                width="300"
                class="my-5"
              ></InputText>
            </template>
            <template #business>
              <RadioButton
                v-model:selected="selectedBusiness"
                title="お問い合わせ先"
                :choices="business"
                width="300"
                class="my-5"
              ></RadioButton>
            </template>
            <template #message>
              <Textarea
                v-model:inputted="inputtedMessage"
                title="お問い合わせ内容"
                width="600"
                class="my-5"
              ></Textarea>
            </template>
            <template #privacy>
              <Checkbox
                v-model:selected="selectedPrivacy"
                title="情報保護方針"
                :choices="privacy"
                width="300"
                class="my-5"
              ></Checkbox>
            </template>
          </FormsTemplate>
          <v-spacer />
        </v-row>
      </v-container>
    </v-main>

    <v-footer app border>
      <v-icon icon="mdi-domain" start />
      Nippon Sogo Systems, Inc.
      <v-spacer />
      <v-icon icon="mdi-github" start />
      yskmt2018 (Yusuke Muto)
    </v-footer>
  </v-app>
</template>
```

</details>

:::note
`<v-navigation-drawer>`・`<v-app-bar>`・`<v-footer>`ブロックは Vuetify の **レイアウトコンポーネント** です。

**アプリケーションレイアウト** は Vuetify において非常に重要なトピックですが、本記事では説明を割愛します。
興味のある方は [公式ドキュメント](https://vuetifyjs.com/ja/features/application-layout/ "アプリケーションレイアウト") を参照してください。
:::

## おわりに

Vue と Vuetify によるフロントアプリケーションの構築とコンポーネントの開発について解説しました。

本記事で紹介したコンポーネントはいずれも最小限の機能しか備えておらず、そのままの状態で実際の開発に耐えるものではありません。
大事なことは **【改良を重ねてコンポーネントを磨いていくこと】** です。
時にチャレンジングな要求にも精一杯答えながら、開発効率の向上とプロダクトの価値に貢献できるコンポーネントを創り上げていきましょう。

ご覧いただきありがとうございました。