Lesson 6

コンポーネント

Lesson 6 Chapter 1
コンポーネントとは

ここまでで、Vue の基本的な機能について学んできました。
このチャプターでは Vue の開発を効率よく進めていくための手法について学んでいきます。

1. コンポーネントを使ったUI部品

1-1. コンポーネントとは

Vue では、UI(ユーザーインターフェイス)を構造化したコンポーネントという概念を用いることにより、ロジックを使い回し、効率的な開発をすることができます。

コンポーネント(component)とは「構成要素、部品」などの意味を持つ言葉で、特に Vue に限った言葉ではありません。
例えば、通常の Web ページでもヘッダーやフッターなどは複数ページで使い回されますが、それもコンポーネントの一つであると言えます。

Webページでは、下図のように複数のコンポーネントを組み合わせて構成されるのが一般的です。

コンポーネントのイメージ図

Vue.jsの開発においては、ページ内のUIを機能単位で部品のように切り出し、コンポーネントを作成します。 作成したコンポーネントには自分で名前を付けることができます。

コンポーネントは .html ファイルや .js ファイル内で定義することもできますが、 拡張子が .vue の「単一ファイルコンポーネント」として管理されるのが一般的です。
単一ファイルコンポーネントは Lesson 5 で見ましたように、中身はHTML、CSS、JavaScriptで構成されるファイルとなります。

そして、そのファイルが主となるページから呼ばれた際に作成されるインスタンスがコンポーネントです。
インスタンスとは、設計図をもとに作成された実体のことです。インスタンスについての詳細はここでは割愛しますが、コンポーネントファイルが設計図ということになります。

文章だけではまだイメージが湧きにくいと思います。以降のレクチャーで具体的に使い方を見ていきます。

1-2. コンポーネント利用のメリット

コンポーネントとは「何となく部品のようなもの」というイメージは持てたでしょうか。
そのコンポーネントを組み合わせてページを作っていくことで、同じようなコードを何度も書く必要がなくなり、効率よく開発することが可能です。
また、一ファイルあたりのコード量を少なくすることができ、そのファイル内容の理解も容易になります。

さらに、ファイルごとの役割が明確になることで、再利用のし易さや保守性が高まります。
詳しくは以降のレクチャーで説明しますが、開発の規模が大きいほどこれらの恩恵も大きくなります。
しかし、規模があまりに小さい場合(〜数ページ)やコンポーネントの再利用が少ない場合は、細かくコンポーネント化をすると逆に開発が煩雑化してしまう可能性もあります。

2. コンポーネントの再利用

2-1. 再利用の例

前のレクチャーでも説明したように、コンポーネントの特徴として再利用性の高さがあります。 まず具体例を見てみたほうが分かりやすいと思うので、コンポーネントの定義方法についてはここでは一旦考えず、既に作成済みとして再利用の例のみを見てみます。

例えば、クリックするごとに 1 ずつ増えていくボタンをコンポーネント化し、下図のように並べるとします。

コンポーネントの利用例1

コンポーネント名は自由につけられるので、今回はcountup-buttonとします。呼び出しの際は、コンポーネント名のHTMLタグとして記述します。
呼び出し部分のコードは以下のようになります。

index.html
<div id="app">
  <countup-button></countup-button&gt
  <countup-button></countup-button&gt
  <countup-button></countup-button&gt
</div>

コンポーネント側では変数やカウントアップなどのロジックを定義していますが、呼び出し側はそれを気にする必要がなく、あくまでUI構築のための部品として扱っています。
これにより、コードが非常にシンプルに書けることが分かります。

また、それぞれのボタンを上から順に1回、2回、3回と押し、カウントを増やしてみます。すると下図のような状態になります。

コンポーネントの利用例2

これは、それぞれのボタンコンポーネントが別々のインスタンスであるために、カウント用の変数も独立していることを表しています。

2-2. 再利用性と保守性

機能の作成後に、変更が発生する場面は開発現場では珍しくありません。

例えば今回の例において、ボタンをクリックしたときの挙動やボタンのデザインなどを変更したくなったとします。
その場合でも呼び出し元のコードはそのままで良く、コンポーネント定義部のみが修正対象となります。
これにより関係のない箇所への思いがけない影響を防止することができます。

以上のことから、コンポーネントを活用することが再利用性や保守性の高さに繋がることが分かります。
ただし、これらの利点を活かすためにも、どの機能をコンポーネント化するのかを設計段階でしっかり考えておく必要があります。

3. コンポーネントの定義

前回は、コンポーネントの具体的な利用例を示しました。
ここでは、コンポーネントをどのように定義するのかについて説明します。

まず、コンポーネントの定義方法は「グローバルコンポーネント」と「ローカルコンポーネント」の2種類あります。

No 種類 説明
1 グローバルコンポーネント ・全てのコンポーネントで使用できる
・ルートコンポーネントのマウント前に登録する
2 ローカルコンポーネント ・登録をした親コンポーネントのみで利用できる
・components オプションを使用して登録する

グローバルコンポーネントは最初に(マウント前に)登録すれば、どのコンポーネントからも使用することができます。
一方、ローカルコンポーネントは、親コンポーネントの components オプションで指定した場合のみ使用することができます。

以下、一つずつ見ていきましょう。

3-1. グローバルコンポーネント

① 定義方法

まずグローバルコンポーネントの定義方法です。
次のように、アプリケーションインスタンスの component() 関数を使用します。

import { createApp } from 'vue'

const app = createApp({})

app.component('グローバルコンポーネント名', {
  /* コンポーネントの実装内容 */
})

app.mount('#app')

component() 関数の第一引数で「コンポーネント名」を指定し、第二引数には「登録するコンポーネントの内容」を定義します。
登録は、マウント(mount('#app'))より前に行います。

そして、テンプレート側からコンポーネントを呼び出すには、以下のように記述します。

<div id="app">
  <グローバルコンポーネント名></グローバルコンポーネント名&gt
</div>

② 具体例で確認する

それでは、実際にコードを書いてみましょう。
新しく lesson6 フォルダを作成し、chapter1-1.html というファイルを追加した上で、次のようにコードを記載します(ここでは CDN を使用します)。

<div id="app">
  <div>ルートコンポーネントです</div>
  <global-component />
</div>

<script>
  // グローバルコンポーネント
  const GlobalComponent = {
    template: `<div>グローバルコンポーネントです</div>`
  }

  // ルートコンポーネント
  const RootComponent = {}

  Vue.createApp(RootComponent)
    .component('global-component', GlobalComponent)
    .mount('#app')
</script>
全てのコードを表示
chapter1-1.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="https://unpkg.com/vue@3.2.36"></script>

  <div id="app">
    <div>ルートコンポーネントです</div>
    <global-component />
  </div>

  <script>
    // グローバルコンポーネント
    const GlobalComponent = {
      template: `<div>グローバルコンポーネントです</div>`
    }

    // ルートコンポーネント
    const RootComponent = {}

    Vue.createApp(RootComponent)
      .component('global-component', GlobalComponent)
      .mount('#app')
  </script>
</body>
</html>

テンプレート内の <global-component /> の部分に、グローバルコンポーネントのテンプレート「<div>グローバルコンポーネントです</div>」が表示されることになります。

ブラウザで確認すると、次の図の赤枠の部分にグローバルコンポーネントの内容が表示されます。

vue_6-1-1.png

3-2. ローカルコンポーネント

① 定義方法

続いて、ローカルコンポーネントの定義方法を見ていきます。
次のように、ローカルコンポーネントを使用する親コンポーネント内に components オプションを使用して登録します。

import { createApp } from 'vue'

const LocalComponent = {
  /* コンポーネントの実装内容 */
}

const ParentComponent = {
  components: {
    'ローカルコンポーネント名': LocalComponent
  }
}

createApp(ParentComponent).mount('#app')

以下のように、テンプレート側からローカルコンポーネントを呼び出すことができます。

<div id="app">
  <ローカルコンポーネント名></ローカルコンポーネント名&gt
</div>

ローカルコンポーネントを使用する場合は、使用するコンポーネントごとに components オプションで登録する必要があります。
ただ、それは必要のないコンポーネントを読み込まずに済むということにもなるので、必要のない限りはローカル定義を採用するのが好ましいです。

② 具体例で確認する

ローカルコンポーネントについても、実際にコードを書いてみましょう。
chapter1-2.html ファイルを追加した上で、次のようにコードを記載します(CDN を使用)。

<div id="app">
  <div>親コンポーネントです</div>
  <local-component />
</div>

<script>
  // ローカルコンポーネント
  const LocalComponent = {
    template: `<div>ローカルコンポーネントです</div>`
  }

  // 親コンポーネント
  const RootComponent = {
    components: {
      'local-component': LocalComponent
    }
  }

  Vue.createApp(RootComponent).mount('#app')
</script>
全てのコードを表示
chapter1-2.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="https://unpkg.com/vue@3.2.36"></script>

  <div id="app">
    <div>親コンポーネントです</div>
    <local-component />
  </div>

  <script>
    // ローカルコンポーネント
    const LocalComponent = {
      template: `<div>ローカルコンポーネントです</div>`
    }

    // 親コンポーネント
    const RootComponent = {
      components: {
        'local-component': LocalComponent
      }
    }

    Vue.createApp(RootComponent).mount('#app')
  </script>
</body>
</html>

テンプレート内の <local-component /> の部分に、ローカルコンポーネントのテンプレート「<div>ローカルコンポーネントです</div>」が表示されることになります。

ブラウザで確認すると、次の図の赤枠の部分にローカルコンポーネントの内容が表示されます。

vue_6-1-2.png

3-3. 命名規則

コンポーネントの命名については、下記の点に注意が必要です。

① DOM を直接操作する場合

上記の具体例のように、CDN を使用して直接 DOM を操作する場合(ビルドを通さずに直接 HTML 部分を記載する場合)は、次のように指定します。

  • ケバブケースのみ使用可能(例:hello-component
  • HTMLのタグ名と重なることがないよう、2語以上を使用してハイフンを含めること

② 単一ファイルコンポーネント等を使用する場合

ビルドツールなどを用いて、単一ファイルコンポーネントを使用する場合は、次のように指定します。コンポーネント内の template(文字列テンプレート)の場合も同様に指定できます。

  • ケバブケース(例:hello-component)またはパスカルケース(例:HelloComponent)のどちらでも命名できる
  • HTMLのタグ名と重なることがないよう、2語以上を使用する

Lesson 6 Chapter 2
コンポーネント間の通信

前チャプターでは、コンポーネントの概要や静的なコンポーネントの使い方について説明しました。 このチャプターでは、コンポーネント間の通信、つまりデータのやり取りについて見ていきます。

1. コンポーネントの親子関係

1-1. コンポーネントの親子関係の概念

Lesson 4 でも簡単に触れましたが、コンポーネントの親子関係について確認しておきましょう。

Vue プログラミングは「小さく、自己完結的で、再利用可能なコンポーネント」を組み合わせることでアプリケーションを構築していきます(下図は Vue公式 より転載)。

vue_4-7-0.png

上の図のように、1 つの画面は、様々なコンポーネントの組合せで構成されることになります。 大きな画面(親)は、いくつかの部品(子)を持ち、その部品は更に小さな部品(孫)で構成されるというような形式になります。

1-2. コンポーネントで定義された変数・メソッド等のスコープ

基本的にコンポーネントというものは独立しており、スコープ(変数や関数などを参照できる有効範囲)が異なります。
そのため、下図の親コンポーネント A の中で定義したデータは、そのままでは子コンポーネント B で使用することができません。

親子コンポーネントの関係図

親子コンポーネントのイメージ図

1-3. コンポーネント間でデータなどを受け渡す方法

以上のことから、Vue には、コンポーネント間のデータ受渡しを行うためにいくつかの機能が用意されています。
主なものを挙げると下表のとおりです。

No 機能 簡単な説明
1 props 親コンポーネントから子コンポーネントに値を渡す。
2 emit 子コンポーネントから親コンポーネントにイベントを渡す。
3 v-model v-model のイベントを使用して双方向データバインディングを行う。
4 slots 親コンポーネントから子コンポーネントに HTML テンプレート等を渡す。
5 状態管理機能 pinia や vuex などのライブラリでコンポーネント間でデータを共有。
6 provide / inject 親コンポーネントから子コンポーネントにデータやロジックを渡す。

No 5 の「状態管理機能」は Lesson 8 で学習します。
それ以外の機能について、以下、見ていきます。

2. 親子コンポーネント間のデータフロー

2-1. props オプション と $emit メソッド

親子コンポーネント間でデータの受渡しを行う場合の代表的な例として props オプションと $emit メソッドがあります。
この 2 つの機能を例として、親子コンポーネント間のデータフローのイメージを確認していきましょう。

① props オプション

Vue では、props オプションを使用することで、親コンポーネントから子コンポーネントへデータを渡すことができます。
しかし、props はあくまで親から子へ渡す操作であり、子から親への作用はありません。

以下は、親コンポーネントから子コンポーネント(ChildComponent)へ、message という名の props に hello という値を渡す例です。

<ChildComponent message="hello" />

② $emit メソッド

子コンポーネントから親コンポーネントへデータ等を渡す場合は、$emit メソッドというものを使用します。
ちなみに、emitとは「放射する、送る」などの意味を持つ言葉です。

以下は、子コンポーネントから親コンポーネントへ、someEvent という名のイベントを渡す例です。ボタンをクリックするとイベントが発火します。

<button @click="$emit('someEvent')">ボタン</button>

以上のような props オプションと $emit メソッドを使用することで、次の図のように、親子コンポーネント間でのデータの授受が可能となります。

親子コンポーネントのイメージ図

2-2. 親子コンポーネント間の通信を利用した例

親子コンポーネントで通信をする例として、ブログの一覧を表示する画面を考えてみましょう。
親コンポーネントではブログデータの一覧を取得し、子コンポーネントへデータを一つ一つ渡します。 子コンポーネントでは、渡された一件のブログデータの中にあるブログタイトルを表示します。 データ操作まわりは親コンポーネントで担当し、子コンポーネントはUIの表示に専念することができます。

このような構造にすることで、ブログ一覧に限らず様々な一覧表画面に対応することが可能となります。 例えば親から渡すデータをカテゴリの一覧データにすれば、カテゴリ一覧画面として子コンポーネントを再利用することが可能となります。

3. props(親から子へデータを渡す)

3-1. 定型例

props には様々な書き方があります。
ここでは script setup 構文を使用したシンプルな形式を見てみましょう。

子コンポーネント
<script setup>
const props = defineProps(['message'])
</script>

<template>
  <div>{{ props.message }}</div>
</template>
親コンポーネント
<script setup>
import Child from './Child.vue'
</script>

<template>
  <Child message="Hello!" />
</template>

以上の指定をすることで親から子へデータを渡すことができます。
以下、それぞれのコードの意味を見ていきます。

3-2. 子コンポーネント内の定義

3-2-1. 親コンポーネントから受け取るデータの指定

まず、子コンポーネントの props の定義部分です。ここで、親コンポーネントからどのようなデータを受け取るのかを指定しています。

const props = defineProps(['message'])

defineProps を使用して message という名前の props を定義しています。
受け取ったデータは、変数 props に格納されます。
defineProps の引数には、配列形式またはオブジェクト形式にて props を定義します。

① 複数のデータを受け取る場合(配列形式)

defineProps で複数のデータを受け取る場合は、次のように記述します。

const props = defineProps(['message', 'count'])

② オブジェクト形式で指定する場合

defineProps の引数をオブジェクト形式で記述すると、次のように受け取るデータの型などを指定できます。

データ型のみ指定する形式

const props = defineProps({
  message: String,
  count: Number
})

データ型を含めたオプション(バリデーション)を指定する形式

const props = defineProps({
  message: {
    type: String,
    required: true,
  },
  count: {
    type: Number,
    default: 0
  }
})

指定できるデータ型には次のようなものがあります(公式参照)。

No データ型 説明
1 String 文字列型
2 Number 数値型(整数、浮動小数点数型)
3 Boolean 真偽値型
4 Array 配列型
5 Object オブジェクト型
6 Date 日付型

オプション(バリデーション)として指定できるものは次のとおりです(公式参照)。

No バリデーション 説明
1 type データ型を指定する
2 required データを必須で要求するか否かを指定する
3 default データがない場合のデフォルト値を指定する
4 validator カスタマイズしたバリデーション関数を指定する

3-2-2. 受け取ったデータの使用

親コンポーネントから受け取ったデータは、defineProps の戻り値(ここでは変数 props)を介して使用することができます。
子コンポーネントの template で、取得したメッセージを表示するようにしています。

<div>{{ props.message }}</div>

なお、script 内でも、次のように props.message という形式でデータを使用できます。

参考
<script setup>
const props = defineProps(['message'])
console.log(props.message)
</script>

setup() 関数で記述する場合の props のデータ使用

script setup 構文を使用せず、setup() 関数を用いて記述する場合は、次のように記述することで、props のデータを script 内で使用することができます。

<script>
export default {
  props: ['message'],
  setup(props) {
    console.log(props.message)
  }
}
</script>

setup(props) というように、setup() 関数の第 1 引数で props を受け取りその引数を介して値を取得するという形です。

なお、template 内では、props: ['message'] で指定した内容がそのまま利用できますので、次のような形で表示を行います。

<template>
  <div>{{ message }}</div>
</template>

props.message ではなく message で指定するのでご注意ください。

3-3. 親コンポーネント内の定義

定型例の親コンポーネントは、次のように定義していました(再掲)。

<script setup>
import Child from './Child.vue'
</script>

<template>
  <Child message="Hello!" />
</template>

以下、親コンポーネントのコードについて見ていきます。

3-3-1. 子コンポーネントのインポート

props 特有のことではなく一般的なことですが、子コンポーネントを次のところでインポートして使用しています。

<script setup>
import Child from './Child.vue'
</script>

3-3-2. 子コンポーネントへのデータ受渡し

子コンポーネントにデータを渡しているのは次のところです。

<Child message="Hello!" />

これで、message という props に Hello! という文字列を渡すことができます。

文字列以外のデータを props で渡す方法

上記のように message="Hello!" という形式でデータを渡すと、指定した値は全て文字列として子コンポーネントに渡されます。
数値を渡したい場合に、count="12" のように指定しても、データは "12" という文字列で渡されてしまいます。

数値や真偽値等のデータ型を渡す場合は、v-bind を使用します(公式 参照)。

<Child v-bind:count="12" v-bind:flag="true" />

なお、次のように v-bind の省略記法を用いても OK です。

<Child :count="12" :flag="true" />

以上、props の基本的な使用方法を確認してきました。

3-4. 実際にコードを書いて実行する

それでは、親コンポーネントから子コンポーネントへデータを渡す props について実際にコードを書いて確認していきましょう。

ここでは、Lesson 5 で作成した vite-sample プロジェクトにコンポーネントを追加していきます(新しく Vue プロジェクトを作成してコードを記述しても差し支えありません)。

3-4-1. 子コンポーネントの作成

まずは、子コンポーネントから作成していきます。
以下のように src/components ディレクトリに Child.vue ファイルを追加します。

vue_6-2-1.png

記述しているコードは次のとおりです。

src\components\Child.vue
<script setup>
const props = defineProps(['message'])
console.log(props.message)
</script>

<template>
  <div>{{ props.message }}</div>
</template>

先に確認した「定型例」とほぼ同じなので、説明は省略します。

3-4-2. 親コンポーネントの作成

次に、親コンポーネントを作成します。
src/components ディレクトリに Parent.vue ファイルを追加してください。

vue_6-2-2.png

記述しているコードは次のとおりです。

src\components\Parent.vue
<script setup>
import Child from './Child.vue'
</script>

<template>
  <div>親コンポーネントです</div>
  <Child message="Hello!" />
</template>

こちらも「定型例」とほぼ同じですね。

3-4-3. ルートコンポーネントの修正

最後に App.vue コンポーネントを修正して、Parent.vue コンポーネントを呼び出すようにします。
修正するところは、以下の赤枠部分となります。

vue_6-2-3.png

コードをコピペなどする場合は、以下の「全てのコードを表示」から確認してください。

全てのコードを表示
src\App.vue
<script setup>
// import HelloWorld from './components/HelloWorld.vue'
// import SetupTest from './components/SetupTest.vue'
// import RefTest from './components/RefTest.vue'
import Parent from './components/Parent.vue'
</script>

<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <!-- <HelloWorld msg="Vite + Vue" /> -->
  <!-- <SetupTest /> -->
  <!-- <RefTest /> -->
  <Parent />
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

3-4-4. ブラウザで表示する

それでは、ローカルサーバを起動してブラウザで表示してみましょう。
サーバを立ち上げるコマンドは npm run dev でしたね。

vue_6-2-4.png

ブラウザで表示する URL は、http://localhost:5173/ です。

コードが正しく記載できていれば、ブラウザに次のように表示されます。

vue_6-2-5.png

ブラウザ上(左側)に「Hello!」と表示されています。
これは、親コンポーネントから子コンポーネントに受け渡した message の値ですね。
また、Console には、子コンポーネントの script 側で指定した console.log(props.message) の結果が表示されています。

簡単なコードで味気ないですが、以上のように props を使用することで、親コンポーネントから子コンポーネントに値を受け渡すことができることを確認できました。

4. emit(子から親へイベントを渡す)

4-1. 定型例

emit にも様々な書き方があります。
ここでは script setup 構文を使用したシンプルな形式を見ていきます。

子コンポーネント
<script setup>
const emits = defineEmits(['emitTest'])
const clickHandler = () => emits('emitTest')
</script>

<template>
  <button v-on:click="clickHandler">ボタン</button>
</template>
親コンポーネント
<script setup>
import Child from './Child.vue'
const testEvent = () => alert('イベントを受け取りました!')
</script>

<template>
  <Child v-on:emitTest="testEvent"/>
</template>

以上の記述をすることで、子の template にある「ボタン」を押すと、clickHandler メソッドが実行され、emitTest というイベントが親コンポーネントに渡されます。
以下、それぞれのコードの意味を見ていきましょう。

4-2. 子コンポーネント内の定義

4-2-1. 親コンポーネントに渡すイベントの定義

以下のように、子コンポーネントの script 内で emit を定義しています。

const emits = defineEmits(['emitTest'])

defineEmits を使用して、その引数に配列形式でイベント名を定義します。
ここでは、emitTest という名前のイベントが 1 つ定義されていることになります。

defineEmits で複数のイベントを定義する場合は、次のように記述します。

const emits = defineEmits(['emitTest', 'otherEmit'])

4-2-2. 親コンポーネントにイベントを渡す処理

親コンポーネントにイベントを渡す部分は次のとおりです。

const clickHandler = () => emits('emitTest')

clickHandler というメソッド(名前は何でも良い)が実行されると、emits('emitTest') が実行されます。 この emits('emitTest') により、emitTest というイベントが親コンポーネントに渡されることになります。

子コンポーネントの template 内で emit を記述する

上の例では、子コンポーネントの script 内に emit の実行処理を記述しましたが、次のように template 内に直接 emit の処理を記述することもできます。

<script setup>
const emits = defineEmits(['emitTest'])
</script>

<template>
  <button @click="$emit('emitTest')">ボタン</button>
</template>

template 内では $emit() 関数で emit を呼び出すことができますので、その引数にイベント名を指定します。

なお、script 内の emit の定義(const emits = defineEmits(['emitTest']))が無くとも emit を実行することができますが、その場合は「emit が定義されていない」旨の警告が出てしまいますので、定義は省略せず記載しておきます。

4-3. 親コンポーネント内の定義

次に、親コンポーネントのコードについて見ていきましょう。

4-3-1. 子コンポーネントからのイベントの受け取り

子コンポーネントからイベントを受け取っているのは次の部分です。

<Child v-on:emitTest="testEvent"/>

上記を構文に落とすと、次のような構造になっています。

<Child v-on:イベント名="実行するメソッド名"/>

v-on:イベント名 で子コンポーネントのイベントをキャッチして、実行するメソッド名 のメソッドを実行します。
なお、v-on:イベント名 は、省略記法を用いて @イベント名 と記述しても OK です。

4-3-2. メソッドの実行

子コンポーネントからイベントをキャッチした際に実行されるメソッドは次のように指定しています。emit 特有の記載ではなく一般的なメソッドの指定となります。

const testEvent = () => alert('イベントを受け取りました!')

メソッドが実行されると alert() 関数により「イベントを受け取りました!」というダイアログが表示されることになります。

以上、emit の基本的な使用方法を確認しました。

4-4. 実際にコードを書いて実行する

それでは、子コンポーネントから親コンポーネントにイベントを渡す emit について実際にコードを書いて確認していきましょう。

先ほどの Child.vue コンポーネントおよび Parent.vue コンポーネントに emit の処理を追加していきます。

4-4-1. 子コンポーネントへの emit 定義追加

Child.vue コンポーネントから修正していきます。
以下のように emit に関する記述を追加してください。

vue_6-2-6.png

script 内に記述していた console.log(props.message) は不要なので削除しています。

修正後のコードは次のようになります。

src\components\Child.vue
<script setup>
const props = defineProps(['message'])
const emits = defineEmits(['emitTest'])
const clickHandler = () => emits('emitTest')
</script>

<template>
  <div>{{ props.message }}</div>
  <button v-on:click="clickHandler">ボタン</button>
</template>

追加内容は「定型例」で解説したとおりですので、説明は省略します。

4-4-2. 親コンポーネントへの emit 関係処理の追加

次に、親コンポーネントにつき、次のように赤枠部分を追加してください。

vue_6-2-7.png

修正後のコードは次のとおりです。

src\components\Parent.vue
<script setup>
import Child from './Child.vue'
const testEvent = () => alert('イベントを受け取りました!')
</script>

<template>
  <div>親コンポーネントです</div>
  <Child message="Hello!" v-on:emitTest="testEvent" />
</template>

こちらも「定型例」と同じのため解説は省略します。

4-4-3. ブラウザで表示する

それでは、ローカルサーバを起動してブラウザで表示してみましょう(サーバを立ち上げている場合はリロードで OK です)。
次のように、子コンポーネントのボタンが 1 つ追加されています。

vue_6-2-8.png

コードが正しく記載できていれば、ボタンを押すと次のようなダイアログが表示されます。

vue_6-2-9.png

これは、子コンポーネントのボタンを押すことで emitTest というイベント(emit)が親コンポーネントに伝達され、親コンポーネント側の testEvent メソッドが実行されたということになります。

4-5. emit で引数を渡す

emit でイベントを渡す際に「引数」を渡すこともできます。
上の例では、子コンポーネント側で emits() を実行する際には「イベント名のみ」を渡していました。
この emits() の第 2 引数以降にイベント実行用の「引数」を渡すことができます(下記)。

子コンポーネント側
emits(イベント名, 引数1, 引数2, ...)

そして、親コンポーネント側では実行メソッドの引数でその値を受け取ることができます。

親コンポーネント側
const メソッド名 = (引数1, 引数2, ...) => { 処理を記述 }

実際にコードを書いた方が分かりやすいので、早速実践をしてみましょう。
以下、Child.vue コンポーネントおよび Parent.vue コンポーネントを修正していきます。

4-5-1. 子コンポーネントの修正

子コンポーネントにつき、以下のように修正します(赤枠部分)。

vue_6-2-10.png

単に、emit() に引数を追加しただけです。 修正後のコードは次のようになります。

src\components\Child.vue
<script setup>
const props = defineProps(['message'])
const emits = defineEmits(['emitTest'])
const clickHandler = () => emits('emitTest', '1つめの引数です', '2つめの引数です')
</script>

<template>
  <div>{{ props.message }}</div>
  <button v-on:click="clickHandler">ボタン</button>
</template>

4-5-2. 親コンポーネントの修正

次に、親コンポーネントにつき、次のように修正します(赤枠部分)。

vue_6-2-11.png

testEvent メソッドに引数を追加して、alert() でその内容を表示するようにしています。 修正後のコードは次のとおりです。

src\components\Parent.vue
<script setup>
import Child from './Child.vue'
const testEvent = (args1, args2) => alert(args1 + ', ' + args2)
</script>

<template>
  <div>親コンポーネントです</div>
  <Child message="Hello!" v-on:emitTest="testEvent" />
</template>

4-5-3. ブラウザで確認する

それでは、ブラウザで確認してみましょう。
ボタンを押して、次のように表示されるはずです。

vue_6-2-12.png

赤枠のところに、子コンポーネントから引数で渡された値が表示されていることが確認できます。

このように emit を使用することで、子コンポーネントから親コンポーネントにイベントやデータを渡すことができます。

5. v-model(親子間で双方向データバインディングを行う)

5-1. 定型例(v-model のデフォルト設定を使用)

子コンポーネントに渡す属性値に v-model ディレクティブを使用すると、デフォルトで modelValue という props が子コンポーネントに渡されます。
また、子コンポーネントから受け取る emit として、デフォルトで update:modelValue という名称のイベントが使用されます。

5-1-1. コンポーネント間の双方向データバインディングの定型例

言葉で理解するのは難しいですので、定型例を見ながら確認していきましょう。

子コンポーネント
<script setup>
const props = defineProps(['modelValue'])
const emits = defineEmits(['update:modelValue'])
</script>

<template>
  <input
    v-bind:value="props.modelValue"
    v-on:input="$emit('update:modelValue', $event.target.value)"
  />
</template>
親コンポーネント
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const inputMessage = ref('Hello!')
</script>

<template>
  <Child v-model="inputMessage"/>
</template>

以上の記述をすることで、子コンポーネントの props である modelValue と、親コンポーネントのリアクティブ変数である inputMessage が双方向でデータバインディングされます。

以下、コードの内容を確認していきます。

5-1-2. 親コンポーネント側の定義

以下のところで、子コンポーネントに属性(プロパティ)を渡しています。

<Child v-model="inputMessage"/>

一般的な props であれば <Child inputMessage="inputMessage"/> というように、props 名を指定しますが、ここでは代わりに v-model を使用しています。
これで、子コンポーネントに props として inputMessage の値を渡せますが、子コンポーネントに渡されるときの props 名は modelValue という名称になります。

5-1-3. 子コンポーネント側の定義

① props の定義

子コンポーネントの props の定義部分を見てみましょう。

const props = defineProps(['modelValue'])

ここで定義されている props 名 modelValue は、先ほど、親コンポーネントのところで説明したように、v-model を使用した場合のデフォルトの props 名となります。

② emit の定義

次に emit の定義部分です。

const emits = defineEmits(['update:modelValue'])

ここで定義されているイベント名 update:modelValue は、v-model を使用した場合のデフォルトのイベント名となります。
このイベントを実行することで、親コンポーネント側の v-model の値を書き換えることができます(親コンポーネント側では、特にイベント受取りの処理を記述する必要はありません)。

③ template 側の処理

続いて template 側を見てみましょう。

<input
  v-bind:value="props.modelValue"
  v-on:input="$emit('update:modelValue', $event.target.value)"
/>

v-bind:value="props.modelValue"
この部分で、props で受け取った値を input ボックスにバインドしています。

v-on:input="$emit('update:modelValue', $event.target.value)"
この部分は、入力イベントが実行されるたびに update:modelValue というイベントを実行するものです。
2 つ目の引数に指定した値 $event.target.value により、親コンポーネントの v-model の値が更新されます。なお、この値は、イベントオブジェクトから input ボックスの入力値を取得したものです。

以上の指定により、v-model を使用した双方向データバインディングを実装することができます。

5-1-4. 実際にコードを書いて実行する

それでは、v-model を使用した双方向データバインディングについて実際にコードを書いて確認していきましょう。

ここでは新たに ModelChild.vue コンポーネントおよび ModelParent.vue コンポーネントを追加してコードを書いていきます。

① 子コンポーネントの作成

まず、子コンポーネントを作成します。
以下のように src/components ディレクトリに ModelChild.vue ファイルを追加してください。

vue_6-2-13.png

記述するコードは次のようになります。

src\components\ModelChild.vue
<script setup>
const props = defineProps(['modelValue'])
const emits = defineEmits(['update:modelValue'])
</script>

<template>
  <input
    v-bind:value="props.modelValue"
    v-on:input="$emit('update:modelValue', $event.target.value)"
  />
</template>

コードの内容は「定型例」と同じですので、説明は省略します。

② 親コンポーネントの作成

次に、親コンポーネントを作成します。
以下のように src/components ディレクトリに ModelParent.vue ファイルを追加します。

vue_6-2-14.png

記述するコードは次のとおりです。

src\components\ModelParent.vue
<script setup>
import ModelChild from './ModelChild.vue'
import { ref } from 'vue'
const inputMessage = ref('Hello!')
</script>

<template>
  <ModelChild v-model="inputMessage"/>
  <div>{{ inputMessage }}</div>
</template>

こちらも基本的には「定型例」と同じですが、子コンポーネントの入力内容が親コンポーネント側に反映されているかを確認するために <div>{{ inputMessage }}</div> という 1 行を追加しています。

③ ルートコンポーネントの修正

最後に App.vue コンポーネントを修正して、ModelParent.vue コンポーネントを呼び出すようにします。
修正するところは、以下の赤枠部分となります。

vue_6-2-15.png

コードをコピペなどする場合は、以下の「全てのコードを表示」から確認してください。

全てのコードを表示
src\App.vue
<script setup>
// import HelloWorld from './components/HelloWorld.vue'
// import SetupTest from './components/SetupTest.vue'
// import RefTest from './components/RefTest.vue'
import ModelParent from './components/ModelParent.vue'
</script>

<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <!-- <HelloWorld msg="Vite + Vue" /> -->
  <!-- <SetupTest /> -->
  <!-- <RefTest /> -->
  <ModelParent />
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

④ ブラウザで表示する

それでは、ローカルサーバを起動してブラウザで表示してみましょう(サーバを立ち上げている場合はリロードで OK です)。

vue_6-2-16.png

親コンポーネントで定義した初期値「Hello!」が子コンポーネントに反映されていること、かつ、子コンポーネントの input ボックスに入力した値が親コンポーネント側にも反映されて <div>{{ inputMessage }}</div> の部分に表示されていることが確認できます(下図)。

vue_6-2-17.png

これが、コンポーネントの親子間の双方向データバインディングとなります。

5-2. v-model のモデル名に別名を付ける

先の例で見たとおり、コンポーネント間で v-model を使用する場合、デフォルトでは props 名は modelValue となり、イベント名は update:modelValue となります。
このデフォルトのモデル名である modelValue に「別名」を付けることができますので、その方法について解説します。

5-2-1. v-model のモデル名に別名を付ける構文

方法は簡単で、以下のように、親コンポーネント側で定義する v-model: の後に、指定したい別名を渡せば OK です。

親コンポーネント
<Child v-model:message="inputMessage"/>

ここで指定している別名は message となります。

子コンポーネント側では、props 名を message、イベント名を update:message として、以下のように処理を記述することができます。

子コンポーネント
<script setup>
const props = defineProps(['message'])
const emits = defineEmits(['update:message'])
</script>

<template>
  <input
    v-bind:value="props.message"
    v-on:input="$emit('update:message', $event.target.value)"
  />
</template>

デフォルトの定型例のうち modelValue のところを message に変更しているだけです。

5-2-2. 実際にコードを書いて実行する

それでは、実際に v-model に別名を付けてみましょう。

ここでは、新たにファイルは作成せず、先に作成した ModelChild.vueModelParent.vue を修正していきます。

① 親コンポーネント側の修正

親コンポーネント ModelParent.vue には、以下の赤枠のところに別名を追加するだけです。

vue_6-2-18.png

全てのコードをテキストで表示
ModelParent.vue
<script setup>
import ModelChild from './ModelChild.vue'
import { ref } from 'vue'
const inputMessage = ref('Hello!')
</script>

<template>
  <ModelChild v-model:message="inputMessage"/>
  <div>{{ inputMessage }}</div>
</template>

② 子コンポーネント側の修正

次に、子コンポーネント ModelChild.vue を修正します。
こちらも単純で、デフォルトのモデル名 modelValue を、別名の message に修正するだけです。修正箇所は、以下の赤枠の 4 箇所となります。

vue_6-2-19.png

全てのコードをテキストで表示
ModelChild.vue
<script setup>
const props = defineProps(['message'])
const emits = defineEmits(['update:message'])
</script>

<template>
  <input
    v-bind:value="props.message"
    v-on:input="$emit('update:message', $event.target.value)"
  />
</template>

③ ブラウザで表示する

それでは、ローカルサーバを起動してブラウザで表示してみましょう。

vue_6-2-17.png

正しくコードが修正できていれば、デフォルトのモデル名を使用した場合と同様に、双方向データバインディングが設定されていることが確認できるはずです。

④ v-model のモデル名に別名を付ける意味

以上のように、v-model のモデル名に別名を付けることが確認できましたが「このメリットは何なのか?」と思われているかもしれません。

それは、デフォルトのモデル名の場合は、コンポーネント間で v-model は 1 つしか使用できないという制限が生じてしまうことにあります。
モデル名に別名を当てることで、コンポーネント間で複数の v-model を使用することが可能となります。

次の項では、複数の v-model を使用した例について見ていきます。

5-3. 複数の v-model を使用した双方向データバインディング

コンポーネント間で複数の v-model を使用する場合は、先の項で説明したように、それぞれの v-model に異なるモデル名を指定することになります。
実際にコードを書いて確認していきましょう。

ここでも、作成済みの ModelChild.vueModelParent.vue ファイルを修正していきます。

① 親コンポーネント側の修正

親コンポーネント ModelParent.vue には、以下の赤枠のところを追加します。

vue_6-2-20.png

全てのコードをテキストで表示
ModelParent.vue
<script setup>
import ModelChild from './ModelChild.vue'
import { ref } from 'vue'
const inputTitle = ref('none')
const inputMessage = ref('Hello!')
</script>

<template>
  <ModelChild
    v-model:title="inputTitle"
    v-model:message="inputMessage"
  />
  <div>title: {{ inputTitle }}</div>
  <div>message: {{ inputMessage }}</div>
</template>

見てのとおり title というモデル名を持つ v-model を 1 つ追加しています。

② 子コンポーネント側の修正

子コンポーネント ModelChild.vue には、以下の赤枠のところを追加します。

vue_6-2-21.png

全てのコードをテキストで表示
ModelChild.vue
<script setup>
const props = defineProps(['message', 'title'])
const emits = defineEmits(['update:message', 'update:title'])
</script>

<template>
  題名: 
  <input
    v-bind:value="props.title"
    v-on:input="$emit('update:title', $event.target.value)"
  /><br />
  本文: 
  <input
    v-bind:value="props.message"
    v-on:input="$emit('update:message', $event.target.value)"
  />
</template>

親モデルから受け取る title という名称の v-model に対して definePropsdefineEmits にそれぞれ定義を追加しています。
また、template には、title の双方向データバインディングを確認するため input タグを使用した入力ボックスを追加しています。

③ ブラウザで表示する

それでは、ローカルサーバを起動してブラウザで表示してみましょう。

vue_6-2-22.png

入力ボックスの「題名」および「本文」部分に、適当に文字を入力してみてください。

vue_6-2-23.png

上記のように、2 つの入力ボックス(v-model)について、双方向データバインディングが設定されていることが確認できたと思います。

6. slots(親から子に HTML テンプレート等を渡す)

Lesson 4 の v-slot ディレクティブの解説で少し触れましたが、slots を使用することで「親コンポーネントから子コンポーネントに HTML テンプレート等を渡す」ことができます。

6-1. 基本形式

まず、手始めに基本的な形式から確認していきましょう。

6-1-1. 定型例

① 子コンポーネント側

子コンポーネント側では、親コンポーネントから受け取る slots(HTML テンプレート)を挿入する箇所に <slot></slot> と指定します(<slot /> でも OK です)。

TodoButon.vue(子コンポーネント)
<template>
  <button>
    <slot></slot>
  </button>
</template>

上記の例では、親コンポーネントから受け取った slots は、ボタンのテキストとして表示されます。

なお、次のように slot タグ内に値を指定した場合は、親コンポーネントから slots が渡されなかったときのデフォルト値として表示されます。

<slot>デフォルト表示</slot>

② 親コンポーネント側

親コンポーネント側では、子コンポーネントに渡すテンプレートを、子コンポーネントのタグ内に指定します。

TodoParent.vue(親コンポーネント)
<template>
  <TodoButton>
    <span style="color: red;">ボタン名</span>
  </TodoButton>
</template>

上記の例で渡されるテンプレートは <span style="color: red;">ボタン名</span> の部分となります。

③ 実際の表示(イメージ)

以上のコードで指定した内容は、イメージとして次のように表示されることとなります。

vue_6-2-24.png

6-1-2. 実際にコードを書いて実行する

それでは、slots を使用したコードを実際に書いていきましょう。

ここでは新たに TodoButon.vue コンポーネントおよび TodoParent.vue コンポーネントを追加してコードを書いていきます。

① 子コンポーネントの作成

子コンポーネントとして、src/components ディレクトリに TodoButon.vue ファイルを追加します。

vue_6-2-25.png

記述したコードは次のとおりです。

src\components\TodoButon.vue
<template>
  <button>
    <slot>指定なし</slot>
  </button>
</template>

基本的に「定型例」と同じですが、親コンポーネントから slots が渡されなかった場合のデフォルト値として、slot タグ内に「指定なし」を記述しています。

② 親コンポーネントの作成

親コンポーネントとして、src/components ディレクトリに TodoParent.vue ファイルを追加します。

vue_6-2-26.png

記述したコードは次のとおりです。

src\components\TodoParent.vue
<script setup>
import TodoButton from './TodoButton.vue'
</script>

<template>
  <TodoButton>
    <span style="color: red;">追加ボタン</span>
  </TodoButton><br />
  <TodoButton></TodoButton>
</template>

こちらも基本的には「定型例」と同じですが、子コンポーネントを 2 つ挿入しています。
2 つ目の <TodoButton></TodoButton> は slots の指定をしない場合となります。

③ ルートコンポーネントの修正

いつものとおりですが App.vue コンポーネントを修正して、TodoParent.vue コンポーネントを呼び出すようにします。
修正するところは、以下の赤枠部分となります。

vue_6-2-27.png

コードをコピペなどする場合は、以下の「全てのコードを表示」から確認してください。

全てのコードを表示
src\App.vue
<script setup>
// import HelloWorld from './components/HelloWorld.vue'
// import SetupTest from './components/SetupTest.vue'
// import RefTest from './components/RefTest.vue'
import TodoParent from './components/TodoParent.vue'
</script>

<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <!-- <HelloWorld msg="Vite + Vue" /> -->
  <!-- <SetupTest /> -->
  <!-- <RefTest /> -->
  <TodoParent />
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

④ ブラウザで表示する

それでは、ローカルサーバを起動してブラウザで表示してみましょう。

vue_6-2-28.png

1 つ目のボタンには、slots の内容 <span style="color: red;">追加ボタン</span> が反映されて赤文字で表示されています。
2 つ目のボタンには、渡される slots がないため、デフォルト値の 指定なし が表示されています。

6-2. スコープ付きスロット

前の項で確認した基本例は、親コンポーネントから子コンポーネントに対してのみテンプレートが渡せるものでした。
ここでは、子コンポーネントのデータを親コンポーネントに渡すことができる「スコープ付きスロット」について見ていきます。

6-2-1. 定型例

① 子コンポーネント側

子コンポーネント側の変数 userName を親コンポーネント側に渡す例です。
<slot :userName="userName"></slot> というように、slot タグ内の属性として userName の値を指定しています。

SlotsChild.vue(子コンポーネント)
<script setup>
const userName = '山田'
</script>

<template>
  <slot :userName="userName"></slot>
</template>

なお、:userName は、v-bind:userName を省略したものです。

複数の値を渡す場合は、次のように複数の属性値を記載します。

<slot :userName="userName" :userAge="userAge"></slot>

② 親コンポーネント側

親コンポーネント側では、子コンポーネントで定義した slot の値を v-slot ディレクティブを使用して取得することができます。

SlotsParent.vue(親コンポーネント)
<template>
  <SlotsChild>
    <template v-slot:default="slotProps">
      私の名前は {{ slotProps.userName }} です。
    </template>
  </SlotsChild>
</template>

v-slot:default="slotProps" の部分で、子コンポーネントの slot で定義した属性値をオブジェクト形式で取得しています。 右辺の slotProps から、slotProps.userName の振り合いで子コンポーネントの属性値を取得できます。

なお、v-slot に続く引数 default は、スロット名を指定しない場合のデフォルト値です。 スロットが default スロット 1 つのみの場合は v-slot="slotProps" と省略して記載することもできます。
また、slotProps はただの変数でありどんな名称でも構いません。

③ 実際の表示(イメージ)

以上のコードで指定した内容は、イメージとして次のように表示されることとなります。

vue_6-2-29.png

つまり、親コンポーネントのテンプレート 私の名前は {{ slotProps.userName }} です。 のうち {{ slotProps.userName }} の部分に、子コンポーネントの変数 userName が表示されているということになります。

6-2-2. 実際にコードを書いて実行する

それでは、スコープ付きスロットを使用したコードを実際に書いていきましょう。

ここでは新たに SlotsChild.vue コンポーネントおよび SlotsParent.vue コンポーネントを追加してコードを書いていきます。

① 子コンポーネントの作成

子コンポーネントとして、src/components ディレクトリに SlotsChild.vue ファイルを追加します。

vue_6-2-30.png

記述したコードは次のとおりです。

src\components\SlotsChild.vue
<script setup>
import { ref } from 'vue'
const userName = ref('山田')
const userAge = ref(30)
</script>

<template>
  <slot
    :userName="userName"
    :userAge="userAge"
  ></slot>
</template>

userNameuserAge という 2 つの値を slot の属性値として指定しています。

② 親コンポーネントの作成

親コンポーネントとして、src/components ディレクトリに SlotsParent.vue ファイルを追加します。

vue_6-2-31.png

記述したコードは次のとおりです。

src\components\SlotsParent.vue
<script setup>
import SlotsChild from './SlotsChild.vue'
</script>

<template>
  <SlotsChild>
    <template v-slot:default="slotProps">
      私の名前は {{ slotProps.userName }} です。<br />
      年齢は {{ slotProps.userAge }} 歳です。
    </template>
  </SlotsChild>
</template>

子コンポーネントで指定された 2 つの属性値(userNameuserAge)をテンプレート内で使用しています。

③ ルートコンポーネントの修正

App.vue コンポーネントを修正して、SlotsParent.vue コンポーネントを呼び出すようにします。 修正するところは、以下の赤枠部分となります。

vue_6-2-32.png

コードをコピペなどする場合は、以下の「全てのコードを表示」から確認してください。

全てのコードを表示
src\App.vue
<script setup>
// import HelloWorld from './components/HelloWorld.vue'
// import SetupTest from './components/SetupTest.vue'
// import RefTest from './components/RefTest.vue'
import SlotsParent from './components/SlotsParent.vue'
</script>

<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <!-- <HelloWorld msg="Vite + Vue" /> -->
  <!-- <SetupTest /> -->
  <!-- <RefTest /> -->
  <SlotsParent />
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

④ ブラウザで表示する

ローカルサーバを起動してブラウザで表示してみましょう。

vue_6-2-33.png

子コンポーネントから 2 つの値(userNameuserAge)を取得して、テンプレートで表示できていることが確認できました。

6-2-3. 複数の slot を使用する場合の例

複数の slot を使用する場合は、v-slot ディレクティブによる名前付きスロットを使用します。
これは具体例で確認してみましょう。

作成済みの SlotsChild.vueSlotsParent.vue ファイルを修正していきます。

① 子コンポーネント側の修正

子コンポーネント SlotsChild.vue には、以下の赤枠のところを追加します。

vue_6-2-34.png

全てのコードをテキストで表示
SlotsChild.vue
<script setup>
import { ref } from 'vue'
const userName = ref('山田')
const userAge = ref(30)
const displayText = ref('フッターです')
</script>

<template>
  <slot
    :userName="userName"
    :userAge="userAge"
  ></slot>
  <slot name="footer" :displayText="displayText"></slot>
</template>

template において footer という名前の slot を追加しています。
親コンポーネントに渡す属性値として displayText を定義しています。

② 親コンポーネント側の修正

親コンポーネント SlotsParent.vue には、以下の赤枠のところを追加します。

vue_6-2-35.png

全てのコードをテキストで表示
SlotsParent.vue
<script setup>
import SlotsChild from './SlotsChild.vue'
</script>

<template>
  <SlotsChild>
    <template v-slot:default="slotProps">
      私の名前は {{ slotProps.userName }} です。<br />
      年齢は {{ slotProps.userAge }} 歳です。
    </template>
    <template v-slot:footer="footerProps">
      <p>{{ footerProps.displayText }}</p>
    </template>
  </SlotsChild>
</template>

v-slot を使用して、スロット名を footer と指定しています。

③ ブラウザで表示する

ローカルサーバを起動してブラウザで表示してみましょう。

vue_6-2-36.png

2 つの slot に指定した内容が表示されることが確認できました。

以上、スロット(slots)を使用したデータの受け渡しについて学習しました。
コンポーネントを設計する際は今回学んだスロットもうまく取り入れ、再利用性の高いコードを目指すようにしましょう。

7. KeepAlive(コンポーネントの状態の維持)

ここまでは主にコンポーネント間の通信について説明してきましたが、今回はコンポーネントの状態についても着目してみましょう。

7-1. 動的コンポーネント(KeepAlive を使用しない場合)

ユーザーの操作によって表示が切り替わる動的コンポーネントについて見ていきます。
デフォルトでは、コンポーネントのインスタンスは、別のコンポーネントに切り替えられたときにアンマウントされ、再表示の際に新しいインスタンスが作成されます。
このとき、それまでの入力内容などユーザーが操作した状態は破棄されてしまいます。

実際に、動的コンポーネントを作成して確認してみましょう。
vite-sample プロジェクトに、TabParent.vueTabHome.vueTabInput.vue という 3 つのコンポーネントを追加してコードを書いていきます。

① 子コンポーネントの作成

src/components ディレクトリに TabHome.vueおよび TabInput.vue という名前の 2 つのファイルを追加します。

vue_6-2-37.png

記述したコードは、それぞれ次のとおりです。

src\components\TabHome.vue
<template>
  <p>Home</p>
</template>
src\components\TabInput.vue
<template>
  <p>入力: <input /></p>
</template>

どちらも template のみの簡素なコンポーネントです。
TabHome.vue は文字を表示するだけで、TabInput.vue は入力ボックスを表示するだけです。

② 親コンポーネントの作成

次に、親コンポーネントとして、src/components ディレクトリに TabParent.vue ファイルを追加します。

vue_6-2-38.png

記述したコードは次のとおりです。

src\components\TabParent.vue
<script setup>
import TabHome from './TabHome.vue'
import TabInput from './TabInput.vue'
import { ref, computed } from 'vue'

const currentTab = ref('home')
const activeComponent = computed(() => 
  currentTab.value === 'input' ? TabInput : TabHome
)
</script>

<template>
  <button @click="currentTab = 'home'">Home</button>
  <button @click="currentTab = 'input'">Input</button>
  <component :is="activeComponent" />
</template>

これは、2 つのボタンにより表示する子コンポーネントを切り替えるものとなります。
コンポーネントの表示を行っているのは次のところです。

  <component :is="activeComponent" />

ここで新しい記述 :is が出てきています(v-bind:is の省略記法)。
これは「is 属性」といい、動的にコンポーネントを表示するために使用するものです。
activeComponent の部分には表示するコンポーネントのオブジェクト(object 型)または名称(string 型)を指定します。 本例では、算出プロパティ(computed)により、TabHome または TabInput のどちらかのオブジェクトが表示されるようにしています。

③ ルートコンポーネントの修正

最後に App.vue コンポーネントを修正して、TabParent.vue コンポーネントを呼び出すようにします。
修正するところは、以下の赤枠部分となります。

vue_6-2-39.png

上記の例では、コメントアウトしていたコードは全て削除しています。
ただし、後で確認をする際に残しておいた方が便利な場合もありますので、必要と考える方はコメントアウトは残しておいていただければと思います。

コードをコピペなどする場合は、以下の「全てのコードを表示」から確認してください。

全てのコードを表示
src\App.vue
<script setup>
import TabParent from './components/TabParent.vue'
</script>

<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <TabParent />
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

④ ブラウザで表示する

それでは、ローカルサーバを起動してブラウザで表示してみましょう。

vue_6-2-40.png

これは、Home ボタンを押せば TabHome.vue コンポーネントが表示され、Input ボタンを押せば TabInput.vue コンポーネントが表示されるという簡単なアプリケーションです

それでは、Input ボタンを押して、表示された入力ボックスに適当に文字を入力してみてください。

vue_6-2-41.png

今度は、Home ボタンを押して、TabHome.vue コンポーネントに表示を切り替えます。

vue_6-2-42.png

そして、Input ボタンを押して再度 TabInput.vue コンポーネントを表示してみましょう。

vue_6-2-43.png

入力していた文字は破棄されて、空欄の入力ボックスに戻っていることが確認できました。
先にも説明したとおり、新しいインスタンスが再作成されたため、以前のインスタンスで入力された内容は残っていないということになります。

7-2. KeepAlive を使用して状態を維持する

続いて、コンポーネントの切り替えがされても、ユーザーが操作した状態を維持する方法を見ていきましょう。
本チャプターの標題のとおり「KeepAlive」を使用することで、コンポーネントの状態を保持することができます。
状態を保持することは「キャッシュを有効にする」と言ったりもします。

使い方はとてもシンプルで、キャッシュを有効にしたいコンポーネントを <KeepAlive> タグで囲むだけです。これで動的にコンポーネントを切り替えた場合でもデータが消えることがありません。

7-2-1. KeepAlive を使用した具体例

それでは、先ほど作成した動的コンポーネントに <KeepAlive> を適用してみましょう。

① 親コンポーネント TabParent.vue の修正

修正するのは、TabParent.vue ファイルのみです。
赤枠部分のように、動的コンポーネントの表示部分を <KeepAlive> タグで囲みます。

vue_6-2-44.png

全てのコードを表示
src\App.vue
<script setup>
import TabHome from './TabHome.vue'
import TabInput from './TabInput.vue'
import { ref, computed } from 'vue'

const currentTab = ref('home')
const activeComponent = computed(() => 
  currentTab.value === 'input' ? TabInput : TabHome
)
</script>

<template>
  <button @click="currentTab = 'home'">Home</button>
  <button @click="currentTab = 'input'">Input</button>
  <KeepAlive>
    <component :is="activeComponent" />
  </KeepAlive>
</template>

修正箇所は次の部分です。

src\components\TabParent.vue
  <KeepAlive>
    <component :is="activeComponent" />
  </KeepAlive>

単純に、コンポーネントの表示部分を <KeepAlive> タグで囲むだけで OK ということです。

こうすることで、コンポーネントが切り替わった際に「アンマウントされる代わりに非アクティブ化状態に移行」します。そして、再表示をする際には、再度「アクティブ化」されるということになります。

DOM テンプレート内で KeepAlive を使用する場合

CDN などのように、直接 DOM テンプレート内で KeepAlive を使用する場合は <keep-alive> のようにケバブケースで記述する必要があります。

② ブラウザで表示する

ブラウザで表示し、Input ボタンを押して入力ボックスに文字を入力してみましょう。

vue_6-2-45.png

Home ボタンを押して、TabHome.vue コンポーネントに表示を切り替えます。

vue_6-2-42.png

そして、Input ボタンを押して再度 TabInput.vue コンポーネントを表示してみましょう。

vue_6-2-46.png

上のとおり、TabInput.vue コンポーネントの入力内容が保持されていることが確認できたと思います。
タブを何度切り替えても消えてしまうことはありません。

7-2-2. KeepAlive 設定時のライフサイクルフック

KeepAlive でキャッシュを有効にした場合、コンポーネントが切り替わった際には、アンマウントされる代わりに非アクティブ化状態に移行し、再表示をする際には、再度アクティブ化されます。
このときに、一般的なライフサイクルフックである、onMounted()onUnmounted() は呼び出されません(Lesson 3 参照)。

そのため、KeepAlive コンポーネントには、アクティブ化の状態に対応する onActivated() と 非アクティブ化の状態に対応する onDeactivated() という 2 つのライフサイクルフックが用意されています。

No ライフサイクルフック 説明
1 onActivated() キャッシュからの再挿入のたびに呼ばれる
(最初のマウント時にも呼ばれる)
2 onDeactivated() DOM から削除されてキャッシュに挿入されるときに呼ばれる
(アンマウントされるときにも呼ばれる)

使用方法は、他のライフサイクルフックと同様です(下記コードは公式より転載)。

src\components\TabParent.vue
<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // 最初のマウントと
  // キャッシュからの再挿入のたびに呼ばれる
})

onDeactivated(() => {
  // DOM から削除されてキャッシュに挿入されるときと
  // アンマウントされるときにも呼ばれる
})
</script>

以上、今回は KeepAlive コンポーネントを使った状態の維持について学習しました。
うまくキャッシュを利用し、ユーザーの負担が少なくなるような UI 構築を目指しましょう。

Lesson 6 Chapter 3
コンポーネントの設計

1. 機能の切り分け

これまでのセクションではコンポーネントの定義や利用方法について学んできました。
しかし、開発の際はそれらのコードを書く前に、まず作成するページをどのような機能(コンポーネント)に分割するのかを考える必要があります。 それを考えるにあたり、何か指針となる考えを持っておくと効率的に進めることができます。

Web アプリケーションは主に以下の要素から構成されることが多いため、これらをベースに考えてみると良いでしょう。

  • ヘッダー(ナビゲーションバー)
  • サイドバー
  • メインコンテンツ
  • フッター

画面に配置してみた際のレイアウト例が以下です。上記の各構成要素の中に、カテゴリや詳細コンテンツなどのアイテムがリンクやボタンなどで配置されます。

機能の切り分け

ちなみに、Vue.jsの公式サイトも、下図のようにおおよそ上記の構成要素に分かれています。
(フッターはページ下部にあります。)

Vue.js公式ページ

設計の際は、まず上記のように機能構成の大枠を決めてみましょう。

2. 設計

ページ内を機能ごとに分割することができたら、続いては切り分けた機能ごとに更に細かい粒度でコンポーネント化することを考えます。

コンポーネントとは機能をまとめた部品のようなものであることは「コンポーネントを使った UI 部品」のセクションでも説明しましたが、設計の際は部品としての汎用性、つまりは再利用性を高めることを意識しましょう。
そのためには、特定の親コンポーネントとの関連(依存性)が強くなってしまわぬよう、なるべく疎結合となるような設計にする必要があります。
疎結合とは、あるプログラムが他のプログラムとの結びつきが弱い、つまり独立性が高い状態のことを言います。そうすることで、他のプログラムの変更の影響を受けにくくなります。

ここで言うプログラムとは、つまりはコンポーネントのことになります。
これまでに学んだコンポーネント間でのデータ授受(props,emit)、スロットなどの活用について整理し、なるべく疎結合で汎用性の高いコンポーネントを設計することを意識してみましょう。

その上でコンポーネントをどこまで細分化するかなどについては経験も必要かもしれませんが、実際の機能やデザインなどの再利用性も踏まえて考えてみましょう。

Atomicデザイン

機能の切り分けについて、ヘッダーやサイドバーなどの大枠の構成要素があることを説明しましたが、ここでは詳細な構成要素を考える際の有名な設計手法について紹介します。

それは「Atomic Design(アトミックデザイン)」と呼ばれるもので、2013年にアメリカのWebデザイナー Brad Frost 氏によって考案・提唱されました。
この手法の中ではページ内の構成を Atoms(アトムス=原子)、Molecules(モルキュールス=分子)、Organisms(オーガニズム=有機物)、Templates(テンプレート)、Pages(ページ)という 5 つの要素に分離しています。 要素の詳細を以下に示します。

  • Atoms:構成の最小単位。ラベル、フォーム、ボタンなどの基本的な HTML 要素。
  • Molecules:Atoms を組み合わせた最小のグループ。検索用の入力フォーム+検索ボタンなど。
  • Organisms:Atoms、Molecules、または他の Organisms を組み合わせたもの。検索フォーム付きのナビゲーションバーなど。
  • Templates:上記の要素をページデザインに沿って配置したレイアウト。あくまでレイアウトのため、テキストや画像は未設置。
  • Pages:Templates に実際のテキストや画像を適用したもの。実際の Web ページ。

(公式:https://atomicdesign.bradfrost.com/

Atomic という名前にもあるとおり、これらを小さい単位から組み合わせ 5 段階で徐々に大きくしていくことにより、最終的に 1 ページのデザイン(UI)を作り上げようという考え方です。
部品を組み合わせることで全体を形成するという点は、先ほどのコンポーネントの設計とも共通しています。

このような手法を指針にすることで設計に統一性をもたせることもできるので、コンポーネント設計の際には是非参考にしてみてください。