Vue の基本的な使い方 (1) Options API

以下は Vue のごく基本的な使い方についての解説のような覚書です。以下で使用している Vue のバージョンは 3.2.36 です。

Vue では Options API と Composition API と呼ばれる 2 種類の異なる API スタイルが利用できますが、以下は Options API を利用する使い方についてです。また、以下では簡単に導入できる CDN を読み込む方法を使用しています。

関連ページ

作成日:2022年10月16日

Vue ドキュメント

Vue 3 のドキュメントは以下で確認できます。

現時点では https://ja.vuejs.org/ のドキュメントは日本語にはなっていない部分もありますが、こちらがメインになるのかと思います(左上に Options API と Composition API の切り替えボタンがあります)。

以下では Vue ドキュメントへのリンクは上記が混在しています。

インストール

Vue.js をプロジェクトに追加するには以下のような方法があります。

  • ページ上で CDN パッケージ として取り込む(以下ではこの方法を利用)
  • JavaScript ファイルをダウンロードして読み込む
  • npm を使ってインストールする
  • Vue CLI や Vite などを使ってプロジェクトを構築する

Vue ドキュメント:クイックスタート

CDN

以下はバージョン 3.2.36 のグローバルビルドを CDN 経由で読み込む例です。

グローバルビルドは、すべてのトップレベル API がグローバルな Vue オブジェクトのプロパティとして公開されています。

<script src="https://unpkg.com/vue@3.2.36/dist/vue.global.js"></script>
<!-- または -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.36/dist/vue.global.js"></script>

本番環境の場合は vue.global.prod.js を読み込みます。

<script src="https://unpkg.com/vue@3.2.36/dist/vue.global.prod.js"></script>

最新版を読み込むにはバージョン番号(3.2.36)の代わりに next を指定します。

<script src="https://unpkg.com/vue@next/dist/vue.global.js"></script>

Vue v3.x の最新版を読み込むには vue@3 を指定します。

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

以下を記述するとページには Hello World! と表示されます。

<!DOCTYPE html>
<html lang="ja">
<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>Vue Sample</title>
</head>
<body>
  <div id="app">
    <!--  Mustache 構文で data プロパティの message を出力-->
    <p>{{ message }}</p>
  </div>
  <!-- CDN で Vue.js を読み込む -->
  <script src="https://unpkg.com/vue@3.2.36/dist/vue.global.js"></script>
  <script>
  // オプションオブジェクト
  const option = {
    //data プロパティ(または data オプションとも呼びます)
    data() {
      return {
        message: 'Hello World!'
      }
    }
  }
  // createApp() にオプションオブジェクトを指定してアプリケーションインスタンスを生成
  const app = Vue.createApp(option);  //グローバル API のメソッド createApp()
  //アプリケーションインスタンスを mount() で DOM 要素(id="app" の要素)にマウント
  const vm = app.mount('#app');
  </script>
</body>
</html>
  • 10〜13行目:テンプレート部分
  • 15行目: Vue.js の読み込み
  • 16〜30行目: Vue の定義部分

テンプレート部分は以下のように出力され、Hello World! と表示されます。

<div id="app" data-v-app="">
  <p>Hello World!</p>
</div>

CDN での使用では、グローバルビルド以外に ES モジュール ビルドインポートマップを利用することもできます。

ES モジュールビルドの使用

ES モジュールビルドの CDN のリンクを読み込んで、ES モジュール構文の import や export を使うこともできます。※ script タグの type 属性に module を指定する必要があります。

<!DOCTYPE html>
<html lang="ja">
<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>Vue Sample</title>
</head>
<body>
  <div id="app">
    <p>{{ message }}</p>
  </div>
  <!-- script タグの type 属性に module を指定 -->
  <script type="module">
  //ES モジュールビルドの CDN から createApp メソッドをインポート
  import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
  const option = {
    data() {
      return {
        message: 'Hello World!'
      }
    }
  }
  // createApp はインポートして分割代入しているので、Vue. は不要
  const app = createApp(option);
  const vm = app.mount('#app');
  </script>
</body>
</html>

ダウンロード

必要であれば、CDN のリンクにアクセスして表示されるコードをコピーしてファイルに保存して使用することもできます。

例えば、以下のリンクにアクセスすると、その時点での最新の本番向けビルド (.prod.js)のコードが表示されます(アクセスした時点での最新版の URL に切り替わります)。

control + a を押すなどして表示されるコードを全て選択してコピーし、vue.global.prod.js などの名前でファイルに保存します。

そして、CDN の URL を読み込む代わりに、保存したファイルを読み込めばOKです。

<!DOCTYPE html>
<html lang="ja">
<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>Vue Sample</title>
</head>
<body>
  <div id="app">
    <p>{{ message }}</p>
  </div>
  <!-- ダウンロードして保存したファイルを読み込む -->
  <script src="./vue.global.prod.js"></script>
  <script>
  const option = {
    data() {
      return {
        message: 'Hello World!'
      }
    }
  }
  const app = Vue.createApp(option);
  const vm = app.mount('#app');
  </script>
</body>
</html>
ES モジュールビルドの使用

ES モジュールビルドを CDN で読み込んで利用することができますが、ダウンロードして使用することもできます。

ES モジュールビルドを使用するには、以下のいずれかの URL にアクセスして表示されるコードをコピーしてファイルを作成して保存します。

コードをコピーして保存した(ダウンロードした)ファイルの名前は任意の名前を付けられます。

そして script タグの type 属性に module を指定すれば、上記で保存したファイルから必要なメソッドなどをインポートすることができます。

例えば、ダウンロードしたファイルを vue.js という名前で保存した場合、以下のように記述できます。

<!DOCTYPE html>
<html lang="ja">
<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>Vue Sample</title>
</head>
<body>
  <div id="app">
    <p>{{ message }}</p>
  </div>
  <!-- script タグの type 属性に module を指定 -->
  <script type="module">
  //保存した ES モジュールビルドのファイル(vue.js)から createApp をインポート(分割代入)
  import { createApp } from './vue.js'
  const option = {
    data() {
      return {
        message: 'Hello World!'
      }
    }
  }
  // createApp はインポートして分割代入しているので、Vue. は不要
  const app = createApp(option);
  const vm = app.mount('#app');
  </script>
</body>
</html>

Vue Devtools

ブラウザに Vue Devtools をインストールしておくとデバッグ時などに便利です。

使用するエディタ(開発環境)

Vue.js の開発では Visual Studio Code(VS Code)を公式の拡張機能 Volarマーケットプレース)と共に使用するのが推奨されています。

また、プラグインの es6-string-html を入れれば、テンプレートリテラル内の HTML などをシンタックスハイライトしてくれます。

es6-string-html でテンプレートリテラル内の HTML をシンタックスハイライトする場合は、テンプレートリテラルの開始文字のバッククォート(`)の前にコメント /*html*/ を入れます。

関連ページ:VS Code で Web 制作

createApp

全ての Vue アプリケーションは Vue クラスのメソッド createApp でアプリケーションインスタンス(Vue アプリケーションを管理するためのオブジェクト)を作成して使用します。

Vue のグローバルビルドを CDN 経由で読み込んでいる場合、グローバル API の関数などは Veu. でアクセスできます(例 Vue.createApp())。

createApp は data や methods、computed、mounted など(オプション: 状態 )からなるオプションオブジェクトを引数に受け取り、アプリケーションのインスタンスを生成して返します。

// オプションオブジェクト
const option = {
  /* オプション(data や methods、computed、mounted など) */
}

// オプションオブジェクトを引数に指定してアプリケーションのインスタンスを生成
const app = Vue.createApp(option);

以下のように、オプションオブジェクトを createApp の引数に直接指定して記述することもできます。

const app = Vue.createApp({
  /* オプション */
})

グローバルビルドを CDN 経由で使っている場合、グローバル API の関数はグローバルな Vue オブジェクトを介してアクセスできるので、上記は分割代入を使って以下のように記述することもできます。

const { createApp } = Vue; // Vue から createApp を変数に代入(分割代入)
const app = createApp({
  /* オプション */
})

ルートコンポーネント

createApp に渡されるオプションは、ルートコンポーネントの設定(動作などの定義)に使われます。

ルートコンポーネントは、アプリケーションをマウントする際に、レンダリングの起点として使われる最上位のコンポーネントです。

作成したアプリケーションは mount() メソッドを使って DOM 要素にマウントする必要があります。マウントすることでテンプレートは、アプリケーションのデータなどにアクセスすることができます。

例えば、 Vue アプリケーションを <div id="app"></div> にマウントするには、 mount() メソッドに #app(id 属性のセレクタ)を渡します。

const options = {
  /* オプション */
}
const app = Vue.createApp(options)
//アプリケーションのメソッド mount() を使って DOM 要素にマウント
const vm = app.mount('#app')  

殆どのアプリケーションインスタンスのメソッドはアプリケーションを返しますが、mount() メソッドはルートコンポーネントのインスタンスを返します。

また、慣習としてコンポーネントのインスタンスを参照するのに vm (ViewModel の略) という変数を使うことがよくあります。

以下の例では Vue アプリを mount() で「id="app"」の要素にマウントしています(紐付けています)。

<body>
<div id="app">
  <p>{{ message }}</p> <!--  Mustache 構文で data の message を参照 -->
</div>
<!-- Vue のインポート(読み込み) -->
<script src="https://unpkg.com/vue@3.2.36/dist/vue.global.js"></script>
<script>
//createApp にオプションを渡してアプリケーション(インスタンス)を生成
const app = Vue.createApp({
  data: function () {
    return {
      message: 'Hello World!'
    };
  }
});
//アプリケーションを mount() メソッドで DOM 要素(id="app" の要素)にマウント
const vm = app.mount('#app');
</script>
</body>

マウントされた DOM 要素の innerHTML は、アプリケーションのルートコンポーネントのレンダリングされたテンプレートに置き換えられます。上記は以下のようにレンダリングされます。

<div id="app" data-v-app="">
  <p>Hello World!</p>
</div>

Options API

Options API では、data、methods、mounted などのオプションからなる1つのオブジェクト(オプションオブジェクト)を用いてコンポーネントのロジックを定義します。

オプションオブジェクトに定義した data プロパティを data オプションなどと呼びます。また data プロパティ(オプション)に定義された(返される)オブジェクトを data オブジェクトなどと呼びます。

同様にオプションオブジェクトに定義した methods プロパティを methods オプション、computed プロパティを computed オプションなどと呼びます。

これらのオプションによって定義されたプロパティには、コンポーネントのインスタンスを指す this を使ってアクセスできます。

data プロパティ

オプションオブジェクトに data プロパティを定義することで、アプリケーションの様々なデータを管理・操作することができます(リアクティブな変数を定義することができます)。

data プロパティ(オプション)はコンポーネントインスタンスのデータオブジェクト(テンプレートから参照できる値を格納したオブジェクト)を返す関数です。

3.x では、 data オプションは object を返す function 宣言のみ受け入れるよう標準化されています。

const app = Vue.createApp({
  //data プロパティは関数
  data: function() {
    return { message: 'Hello World!'};  //オブジェクトを返す
  }
});

ES6(ECMAScript 2015)で導入されたメソッド定義の短縮構文を使って以下のように function 句を省略して記述することができます。

また、createApp メソッドは自身のアプリケーションインスタンスを返すため、他のメソッドをチェーンさせることができます。以下では mount() をチェーンしています。

Vue.createApp({
  data() {
    return { message: 'Hello World!'};  //オブジェクトを返す
  }
}).mount('#app');

Vue.js ではアプリで利用する値をデータオブジェクトとして用意して、テンプレートから参照する仕組みになっています(データバインディング)。

コンポーネントインスタンスのプロパティ

data で定義されたプロパティは、コンポーネントインスタンスを介して公開されます(参照できます)。

また、Vue は data で定義されたプロパティ(オブジェクト)をそのリアクティブシステム(変更を自動的に検知して更新する機能)でラップして、コンポーネントのインスタンスに $data として格納します。

const app = Vue.createApp({
  data() {
    return { message: 'Hello World!'};  //data で定義されたプロパティ
  }
});
//変数 vm はコンポーネントインスタンス
const vm = app.mount('#app');

console.log(vm.message); //Hello World!
console.log(vm.$data.message); //Hello World!

これらのインスタンスプロパティは、インスタンスの初回作成時にのみ追加されます。

新しいプロパティを data に含めずに、コンポーネントのインスタンスに直接追加することはできますが、その場合、そのプロパティはリアクティブな $data オブジェクトによってサポートされていないので、 Vue のリアクティブシステム によって自動的に追跡(更新)されることはありません。

data プロパティの名前

Vue は、コンポーネントのインスタンスを介して自身のビルトイン API を公開する際に、 $ をプレフィックスとして使います。 また、内部プロパティのために _ を予約しています。

※ トップレベルの data プロパティの個々の名前に、$ や _ から始まる名前を使うことは避けるべきです。

その他のオプション

data オプション以外にもコンポーネントインスタンスにユーザ定義のプロパティを追加する様々なコンポーネントオプション(computedmethodsprops など)があります。

Vue データプロパティ

データオブジェクトにアクセス

Vue.js では HTML ベースのテンプレート構文を使っています。

Mustache 構文

テンプレートからデータオブジェクトにアクセスしてテキスト展開するには {{ }} という Mustache 構文(mustache タグ)を利用します。

{{ }} には JavaScript の任意のを指定することができます(を指定することはできません)。

以下の場合、{{ message }} は data の message プロパティを参照して「Hello World!」と出力されます。

<div id="app">
  <!-- テンプレート -->
  <p>{{ message }}</p> <!-- data の message プロパティを参照してテキスト展開 -->
</div>

<script>
Vue.createApp({
  data() {
    return { message: 'Hello World!'};
  }
}).mount('#app');
</script>

Reactivity System(リアクティブシステム)

上記の message の値を「Hello Vue!」に変更すると、Vue は自動的に新しい値を検知して出力を「Hello Vue!」に更新します。

これは、Vue がリアクティブであるためです。Vue には、更新を処理するリアクティブシステムが備わっていて、データの値が変更されると、そのデータに依存しているすべての箇所が、自動的に更新されます。

HTML として出力

{{ }} はデータを HTML ではなく、プレーンなテキストとして扱います。テキストではなく HTML として出力するには、v-html ディレクティブを使用します。

属性値をバインド

属性値に JavaScript 式を埋め込むには Mustache 構文(mustache タグ)は使えないので、v-bind ディレクティブを利用します。

v-bind ディレクティブは、リアクティブに HTML 属性を更新します。

ディレクティブの中には引数を取るものがあり、引数はディレクティブ名の後にコロンで指定します。

v-bind ディレクティブでは、引数に属性名を指定します。以下の場合、href 属性に url プロパティの値(url は文字列ではなく式)をバインドしています。

<a v-bind:href="url"> ... </a>
<div id="app">
  <a v-bind:href="url">Link</a>
  <!-- v-bind ディレクティブで href 属性の値に data の url を参照 -->
</div>
<script>
Vue.createApp({
  data() {
    return {
      url: 'https://example.com'
    }
  }
}).mount('#app');
</script>

上記の場合、以下のように出力されます。

<a href="https://example.com">Link</a>

Built-in Directives

computed 算出プロパティ

テンプレートでは単純なプロパティの参照にとどめ、演算やメソッドの呼び出しなど(リアクティブなデータを含む複雑なロジック)は算出プロパティ computed を利用します。

算出プロパティは、既存のプロパティを演算(算出)した結果を取得するゲッター(getter)です。

算出プロパティでは this.プロパティ名 でデータオブジェクトのプロパティにアクセスできます。

算出プロパティをテンプレートから参照するには、データオブジェクト同様、{{プロパティ名}} でアクセスできます。定義側はメソッドですが、参照側は単にプロパティで参照できます(メソッド呼び出しのカッコは不要)。

HTML
<div id="app">
  <!-- テンプレートから算出プロパティを参照 -->
  <p>{{ hostName }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      url:'https://webdesignleaves.com'
    }
  },
  computed: {
    hostName() {
      return this.url.split('//')[1];  //this.url で url にアクセス
    }
  }
}).mount('#app');
  

以下はコードとしては正しい(上記と同じこと)ですが、コードが読みにくく悪い例です。

例えば、同じ式を複数の場所で記述していれば、修正する際は全てを修正しなければなりません。

演算やメソッドの呼び出しなど、複雑なロジックはコード側で行い、テンプレートではプロパティの参照にとどめるようにします。

HTML
<div id="app">
  <!-- 良くない例(テンプレートがよみにくい)-->
  <p>{{ this.url.split('/\/')[1] }}</p>
</div> 

アロー関数は使わない

アロー関数は内部で this を持たないため、this でコンポーネントのインスタンスを参照できません(アロー関数を使った定義では this.プロパティ名 でデータオブジェクトのプロパティにアクセスできません)。そのため、computed ではアロー関数を利用するべきではありません。

Vue 算出プロパティ

メソッド

コンポーネントのインスタンスにメソッドを追加するには methods オプションを使います。

Vue は、 methods の this を自動的にバインドして、常にコンポーネントのインスタンスを参照します。これにより、メソッドがイベントリスナやコールバックとして使われる際に、正しい this の値を保持することができます。

methods はコンポーネントのテンプレート内からアクセスすることができ、イベントリスナとしてよく使われます。

以下のメソッド increment() を実行すると、コンポーネントインスタンス(this)の count プロパティ(data オプション)の値を1増加します。

JavaScript
const app = Vue.createApp({
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      // this はコンポーネントインスタンスを参照
      this.count++
    }
  }
})
// vm はルートコンポーネントのインスタンス
const vm = app.mount('#app')

console.log(vm.count) // 0

vm.increment()  //メソッドを実行

console.log(vm.count) // 1

以下はテンプレートで v-on ディレクティブを使ってイベントリスナに increment() をアタッチする例です(引数がない場合はカッコを省略できます)。

<div id="app">
  <!-- クリックイベントのリスナに increment を設定 -->
  <button v-on:click="increment">Count Up</button>
  <p>{{ count }}</p>
</div>

メソッドの呼び出しでも算出プロパティと同様のことができます。

以下は前述の算出プロパティをメソッドを使って書き換えたものです。テンプレートのメソッドの呼び出しでは、カッコが必要になります。

HTML
<div id="app">
<p>{{ hostName() }}</p><!-- メソッドの呼び出しではカッコが必要 -->
</div>
JavaScript
Vue.createApp({
data() {
  return {
    url:'https://webdesignleaves.com'
  }
},
methods: {  //computed を methods に変更
  hostName() {
    return this.url.split('//')[1];
  }
}
}).mount('#app');

アロー関数は使わない

computed 同様、methods ではアロー関数を利用するべきではありません。

算出プロパティとメソッドの違い

算出プロパティ(compuated)とメソッド(methods)には以下のような違いがあります。

  • 算出プロパティは引数を持てない

    算出プロパティはプロパティなので、カッコ () を伴う呼び出しができないため、引数を持てません。

  • 算出プロパティの値はキャッシュされる

    算出プロパティはリアクティブな依存関係に基づいてキャッシュされるという違いがあります。算出プロパティはリアクティブな依存関係の一部が変更された場合にのみ再評価されます。

メソッドは、再描画の際に常に評価されますが、算出プロパティはそれが依存するプロパティが変更された場合にのみ評価されます。

算出プロパティ vs メソッド

ライフサイクル

各コンポーネントインスタンスは、生成されるときに一連の初期化ステップを通り、要素にマウントされ、データの変化に応じてビューを更新させていき、最終的には破棄されます。

Vue.js では、このライフサイクルの変化に応じて呼び出されるメソッドが用意されていて、これらのメソッドをライフサイクルフックと呼び、以下のようなメソッドがあります。

ライフサイクルフック
名前 説明
beforeCreate

インスタンスが初期化された直後、データの監視とイベント/ウォッチャの設定前(データの初期化の前)に、同期的に呼び出されます。

created

インスタンスが作成された後(データの初期化の後)に、同期的に呼び出されます。この段階では、インスタンスはオプションの処理を終えています。つまり、次のものが設定されています: データの監視、算出プロパティ、メソッド、ウォッチ/イベントのコールバック。しかし、マウントのフェーズは始まっていないので、$el プロパティ(コンポーネントインスタンスが管理しているルート DOM 要素)はまだ利用できません。

beforeMount

マウントがはじまる(コンポーネントがページに紐づく)直前に呼び出されます: render 関数が初めて呼び出されるところです。

mounted

インスタンスがマウントされた(コンポーネントがページに紐づいた)後に呼び出され、app.mount に渡された要素は、その新しく作成された vm.$el で置き換えられます。ルートインスタンスがドキュメントの中の要素にマウントされた場合、mounted が呼び出されたときに、vm.$el もドキュメントに配置されます。

mounted は、すべての子コンポーネントもマウントされていることを保証しないことに注意してください。ビュー全体がレンダリングされるまで待ちたい場合は、mounted の代わりに vm.$nextTick を使うことができます

beforeUpdate

データが変更されるとき、DOM に patch (Virtual DOM の処理プロセス)される前(再描画の前)に呼び出されます。これは例えば、手動で追加されたイベントリスナを削除するといった、更新前に既存の DOM にアクセスするのに適しています。

updated

データの変更後に仮想 DOM が再レンダリングされ、patch が適用された後(再描画の後)に呼び出されます。

このフックが呼び出されたときには、コンポーネントの DOM は更新されているので、ここで DOM に依存した操作を行うことができます。しかしほとんどの場合、フックの中で状態を変更することは避けるべきです。状態を変更するためには、通常代わりに 算出プロパティ や ウォッチャ を使うほうがよいでしょう。

updated は、すべての子コンポーネントが再レンダリングされたことを保証するものでは ありません。ビュー全体が再レンダリングされるまで待ちたいなら、vm.$nextTick を updated の中で使うことができます:

beforeUnmount

コンポーネントインスタンスがアンマウントされる(破棄される)直前に呼び出されます。この段階ではインスタンスはまだ完全に機能しています。

unmounted

コンポーネントインスタンスがアンマウントされた(破棄された)後に呼び出されます。このフックが呼び出されたときには、コンポーネントインスタンスのすべてのディレクティブはバインド解除され、すべてのイベントリスナは削除され、すべての子コンポーネントインスタンスもアンマウントされています。

errorCaptured 任意の子孫コンポーネントからエラーが捕捉されたときに呼び出されます。このフックは 3 つの引数を受け取ります: エラーと、エラーを引き起こしたコンポーネントインスタンスと、エラーが捕捉された箇所の情報を含む文字列です。このフックは更にエラーが伝播するのを防ぐために、false を返すことができます。
renderTracked 仮想 DOM の再レンダリングが追跡されたときに呼び出されます。このフックは引数として debugger event を受け取ります。このイベントは、どの操作がコンポーネントを追跡したのか、その操作のターゲットオブジェクトとキーを教えてくれます。
renderTriggered 仮想 DOM の再レンダリングが実行されたときに呼び出されます。renderTracked と同様に、引数として debugger event を受け取ります。このイベントは、どの操作が再レンダリングのきっかけとなったか、その操作のターゲットオブジェクトとキーを教えてくれます。
activated

kept-alive コンポーネント(keep-alive が内部で保持する子コンポーネント)がアクティブになったときに呼び出されます(コンポーネントが待機状態でなくなったときに呼び出されます)。

deactivated

kept-alive コンポーネント(keep-alive が内部で保持する子コンポーネント)が非アクティブになったときに呼び出されます(コンポーネントが待機状態になったときに呼び出されます)。

HTML
<div id="app">
  <p>Life Cycle</p>
  <p>Current: {{ current }}</p>
  <input type="button" value="Click" v-on:click="onclick">
</div>
JavaScript
const app = Vue.createApp({
  beforeCreate() {
    console.log('beforeCreate hook');
  },
  created() {
    console.log('created hook');
  },
  beforeMount() {
    console.log('beforeMount hook');
  },
  mounted() {
    console.log('mounted hook');
  },
  beforeUpdate() {
    console.log('beforeUpdate hook');
  },
  updated() {
    console.log('updated hook');
  },
  beforeUnmount() {
    console.log('beforeUnmount hook');
  },
  unmounted() {
    console.log('unmounted hook');
  },
  data() {
    return {
      current: new Date().toLocaleString()
    }
  },
  methods: {
    onclick() {
      this.current = new Date().toLocaleString();
    }
  }
})
app.mount('#app');

setTimeout(() => {
  app.unmount();
},5000);
  

上記の場合、最初に以下がほぼ同時にコンソールに出力されます。

beforeCreate hook
created hook
beforeMount hook
mounted hook

そして5秒後に app.unmount() により以下が出力されます。

beforeUnmount hook
unmounted hook

5秒以内(アンマウントする前)にボタンをクリックすると上記の前に以下が出力されます。

beforeUpdate hook
updated hook

アロー関数

アロー関数自身は this を持っていない(内部で this が定義されていない)ため、もし this がアクセスされた場合は、外側のスコープ(関数)を探索します。

そのため、data や computed、 methods、ライフサイクルフック(オプションのプロパティやコールバック)でアロー関数を利用するべきではありません。

以下の場合は this でプロパティにアクセスできます。

HTML
<div id="app">
  <p>Date and Time: {{ date }}</p>
  <input type="button" value="Click" v-on:click="onclick">
</div>
JavaScript
const app = Vue.createApp({
  data() {
    return {
      date: new Date().toLocaleString()
    }
  },
  methods: {
    onclick() {
      //this.date でデータプロパティにアクセス
      console.log('onclick:' + this.date); //例 onclick:2022/7/5 10:52:03
    }
  },
  created() {
    //this.date でデータプロパティにアクセス
    console.log('created hook:' + this.date); //例 created hook:2022/7/5 10:52:03
  }
})
app.mount('#app');

以下は上記と同じことなので、同様に this でプロパティにアクセスできます。

methods: {
  onclick: function() {
    console.log('onclick: ' + this.date);
  }
},
created: function() {
  console.log('created hook: ' + this.date);
},

但し、アロー関数を使うと this を使ってアクセスすることはできません。この例の場合、this.date は undefined になります。

methods: {
  onclick: () => {
    console.log('onclick: ' + this.date);  //created hook: undefined
  }
},
created: () => {
  console.log('created hook: ' + this.date);  //onclick: undefined
}

リアクティブデータ

Vue.js では createApp メソッドの data オプションに登録されたデータをリアクティブデータと呼びます。

リアクティブは「反応的(反応する)」というような意味で、データオブジェクトの変化を検知してページに自動的に反映させます。

以下を実行すると、current プロパティの変化に応じてページ側の時刻の表示も変わります。

created フックを利用して、setInterval で1秒毎に current プロパティの値を現在の時刻に更新しています。また、beforeUnmount フックで不要になったタイマーを破棄しています。

HTML
<div id="app">
  <p>Current Time {{ current.toLocaleString() }}</p>
</div>
JavaScript
const app = Vue.createApp({
  data() {
    return {
      current: new Date()
    }
  },
  created() {
    this.timer = setInterval( ()=> {
      this.current = new Date();
    }, 1000)
  },
  beforeUnmount() {
    clearInterval(this.timer);
  }
});
app.mount('#app');

setTimeout(() => {
app.unmount();
},5000);

Vue.js では、data オブジェクトにデータを登録すると、その全てのプロパティを監視対象として登録し、変更を検知すると、自動的にビューに反映します。

リアクティビティーの基礎

非同期更新

リアクティブシステムによるページ(ビュー)の更新は非同期です。

Vue.js では、データの変更を検知しても、即座にビューに反映させるわけではなく、連動して発生する全ての変更をプールした上で、最終的な結果をビューに反映させます。

以下は mounted ライフサイクルフックでアプリケーションがマウントされている要素(this.$el)のテキストが、設定したメッセージの値(this.message)を含んでいるかを確認しています。結果は false となります(データオブジェクトへの更新が即座には反映されないことを意味します)。

$el はアプリインスタンスで管理された要素を Element オブジェクトとして返すプロパティです(Vue.js のメンバーには接頭辞 $ が付きます)。

HTML
<div id="app">
  <p>{{ message }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      message: 'Hello World'
    }
  },
  mounted() {
    this.message = 'Hi Vue.js';
    console.log(this.$el.textContent.includes(this.message));   //false
  }
}).mount('#app');
$nextTick

$nextTick は Vue.js によるビューの更新を待って指定された処理を実行(DOM 更新サイクルの後に実行)するので、以下の場合は true になります。

HTML
<div id="app">
  <p>{{ message }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      message: 'Hello World'
    }
  },
  mounted() {
    this.message = 'Hi Vue.js';
    // ビューの更新を待って指定された処理を実行
    this.$nextTick( () => {
      console.log(this.$el.textContent.includes(this.message));  //true
    })
  },
}).mount('#app');

ウォッチャ

リアクティブシステムではデータの更新を自動的に検知してビューに反映させます。算出プロパティ及びメソッドも依存するデータの変化を検知します。

更新タイミングを手動で制御したい場合は、ウォッチャ(watch オプション)を利用できます。

watch オプションを利用することで、特定のプロパティが変化したときに任意の処理を実行できます。

以下が watch オプションの構文です。

  • prop:監視するプロパティ名
  • newValue:プロパティの変更後の値
  • oldValue:プロパティの変更前の値
watch: {
  prop(newValue, oldValue) {
    //プロパティが変化したときに実行する処理
  },
  ...
}

以下は v-model ディレクティブを使ってテキストボックスの値(input 要素の value)と data オプションの question プロパティを関連付けています。

そして watch オプションで question プロパティを監視し、値が変わるたびにその値にクエスチョンマークが含まれていれば getAnswer() を呼び出します。

getAnswer()では、fetch() メソッド(非同期処理)で「Yes」または「No」を返す API へアクセスして、取得した値で this.answer を更新しています。

HTML
<div id="app">
  <p>
    質問(yes/no 形式で回答します):
    <input v-model="question" />
  </p>
  <p>{{ answer }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      question: '',
      answer: '質問の後にクエスチョンマーク(半角)を入力すると回答します'
    }
  },
  watch: {
    // question が変わるたびに、この関数が実行される
    question(newQuestion, oldQuestion) {
      if (newQuestion.indexOf('?') > -1) {
        this.getAnswer();
      }
    }
  },
  methods: {
    getAnswer() {
      this.answer = '考え中...';
      fetch('https://yesno.wtf/api')
      .then((response) => {
          return response.json();
        })
        .then((data) => {
          this.answer = data.answer
        })
        .catch(error => {
          this.answer = 'Error! Could not reach the API. ' + error
      });
    }
  }
}).mount('#app')

watch: は以下のように記述することもできます。

watch: {
  question : {
    handler(newQuestion, oldQuestion) {
      if (newQuestion.indexOf('?') > -1) {
        this.getAnswer();
      }
    }
  }
}

動作オプション

以下の形式でウォッチャのオプションを指定することができます。

watch: {
  prop : {
    handler(newValue, oldValue) {
      //プロパティが変化したときに実行する処理
    },
    deep: true,  //オプション
    immediate: true,  //オプション
    flush: 'post'  //オプション
  }
}
  
オプション 説明
handler コールバック関数
deep ネストされたオブジェクトを監視するかどうか(デフォルトは false)
immediate 起動時に即座に実行するかどうか(デフォルトは false)
flush 処理(コールバック)の実行タイミングを制御(以下を指定)
  • pre:レンダリング(描画)前に実行(デフォルト)。テンプレートの実行前にコールバックが他の値を更新することができます。
  • post:レンダリング後に実行(更新後の文書ツリーにアクセスする場合や $refs 経由で子コンポーネントにアクセスする場合に利用)
  • sync:値が変更されたら即座に実行

$watch メソッド

ウォッチャは $watch インスタンスメソッドを使って定義することもできます。

前述の例の場合、以下のように watch オプションを削除して、created フックに $watch メソッドを定義しても同じことです。

Vue.createApp({
  data() {
    return {
      question: '',
      answer: '質問の後にクエスチョンマーク(半角)を入力すると回答します'
    }
  },
  created() {  //created フックに $watch メソッドを定義
    this.$watch(
      'question', (newQuestion, oldQuestion) => {
        if (newQuestion.indexOf('?') > -1) {
          this.getAnswer();
        }
      }
    )
  },
  methods: {
    getAnswer() {
      this.answer = '考え中...';
      fetch('https://yesno.wtf/api')
        .then((response) => {
          return response.json();
        })
        .then((data) => {
          this.answer = data.answer
        })
        .catch(error => {
          this.answer = 'Error! Could not reach the API. ' + error
        });
    }
  }
}).mount('#app')

ディレクティブ

属性やスタイルの操作、条件分岐、繰り返し処理などの機能を組み込むにはディレクティブを利用します。

ディレクティブは v- から始まる特別な構文で、値は単一の JavaScript 式を指定します(但し、v-for と v-on は例外です)。

v-text

v-text は要素の textContent を更新します。textContent の一部を更新する必要がある場合、代わりに Mustache 構文 {{ }} を使います。

Mustache 構文 はページを起動したときに一瞬だけ表示されてしまう問題がありますが、 v-text を使えばチラツキません(v-cloak)。

HTML
<div id="app">
  <p v-text="message"></p>
  <p>{{ message }}</p><!-- 上記(二行目)と同じこと -->
  <p>Greeting: {{ message }}</p><!-- 一部を更新 -->
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      message: 'Hello Vue!'
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <p>Hello Vue!</p>
  <p>Hello Vue!</p>
  <p>Greeting: Hello Vue!</p>
</div>

v-html

v-html は要素の innerHTML を更新します。

任意の HTML を動的にレンダリングすることは、XSS 攻撃に簡単につながるため、v-html は信用できるコンテンツのみに使い、ユーザが提供するコンテンツ(外部からの入力など)には使わないようにします。

HTML
<div id="app">
  <div v-html="title"></div>
  <div>{{ title }}</div>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      title: '<h1>Hello World</h1>'
    }
  }
}).mount('#app');

以下のように出力されます。{{ }} はデータを HTML ではなく、プレーンなテキストとして扱います。

<div id="app">
  <div><h1>Hello World</h1></div>
  <div>&lt;h1&gt;Hello World&lt;/h1&gt;</div><!-- テキストとして出力される -->
</div>

v-pre

v-pre はこの要素とそのすべての子要素のコンパイルを省略します。これは Mustache タグそのものを表示(Mustache 構文としてではなく文字列として表示)するときに使えます。

HTML
<div id="app">
  <p v-pre>{{ message }}</p>
  <p>{{ message }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      message: 'Hello World'
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <p>{{ message }}</p>;<!-- Mustache 構文を文字列として表示 -->
  <p>Hello World</p>
</div>

v-bind

v-bind は属性やコンポーネントのプロパティを式へ動的にバインドします。

v-bind(ディレクティブ名)の後にコロンで属性名を引数として指定します。

書式
v-bind:属性名="式" 

以下は、src 属性に image プロパティの値をバインドしています。

HTML
<div id="app">
  <img v-bind:src="image" alt="">
</div>
JavaScript
ue.createApp({
  data() {
    return {
      image: './assets/images/sample.jpg'
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <img src="./assets/images/sample.jpg" alt="">
</div>

省略形

v-bind はよく使われるので、v-bind を省略して :(コロン)のみを記述する省略形があります。

HTML
<div id="app">
  <img :src="image" alt="">
</div>

ブール属性

checked, selected, disabled, multiple などの属性名だけを指定する属性をバインドするには true を指定します。

HTML
<div id="app">
  <input type="button" value="click" v-bind:disabled="flag">
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      flag: true
    }
  }
}).mount('#app');

以下のように出力されます。

HTML
<div id="app">
  <input type="button" disabled value="click">
</div>

false にすると属性は出力されません。上記の場合、flag: false とすると disabled 属性は出力されません。

複数の属性を指定

以下のように複数の属性を個々に v-bind を使ってバインドすることができます。

HTML
<div id="app">
  <input type="text" v-bind:size="size" v-bind:required="required">
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      size: 30,
      required: true
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
<input type="text" size="30" required>
</div>

オブジェクトで複数の属性をまとめてバインド

v-bind には 属性名:値 の形式のオブジェクトを渡すことで、複数の属性をまとめてバインドすることができます。この場合、v-bind の引数(属性名)は不要です。

HTML
<div id="app">
  <input type="text" v-bind="name">
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      name: {
        size: 30,
        placeholder: 'Your Name',
        required: true
      }
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <input type="text" size="30" placeholder="Your Name" required>
</div>

以下のように記述することもできます(上記と同じ結果になります)。

HTML
<div id="app">
  <input type="text" v-bind="{ size: size, placeholder: placeholder, required: required }">
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      size: 30,
      placeholder: 'Your Name',
      required: true
    }
  }
}).mount('#app');

重複した属性(記述順)

直接記述された属性と v-bind でオブジェクトとして渡された属性が重複した場合、後から記述されたものが優先されます。例えば、上記の例の場合、以下のように記述すると、size 属性は 40 になります。

<input type="text" v-bind="name" size="40">

以下のように記述すると、size 属性は 30 になります。

<input type="text" size="40" v-bind="name">

動的引数

v-bind や v-on では角括弧 [ ] で囲むことで JavaScript 式をディレクティブの引数に使うこともできます。

HTML
<div id="app">
  <select v-model="selected">
    <option value="height">Height</option>
    <option value="width">Width</option>
  </select>
  <input type="text" size="5" v-model="inputValue">
  <img src="./images/sample.jpg" alt="" v-bind:[selected]="inputValue">
</div>

この例の場合、selected の初期値は width ですが、v-model により select 要素(セレクトボックス)の選択が変更されると selected の値 [selected] が動的に変化します。

同様に inputValue の初期値は 100 ですが、input 要素に入力された値により変化します。

JavaScript
Vue.createApp({
  data() {
    return {
      selected:'width',  //初期値(select 要素で変更)
      inputValue: 100  //初期値(input 要素で変更)
    }
  }
}).mount('#app');

初期状態では以下が出力されます。

<div id="app">
  <select>
    <option value="height">Height</option>
    <option value="width">Width</option>
  </select>
  <input type="text" size="5" >
  <img src="./images/sample.jpg" alt="" width="100">
</div>

セレクトボックス(select 要素)で height を選択し、テキストフィールド(input 要素)に 300 と入力すると、img 要素は以下のようになります。

<img src="./images/sample.jpg" alt="" height="300">

クラスとスタイルのバインディング

v-bind を使って要素のクラスリストとインラインスタイルを操作することができます。

Vue は v-bind が class と style と一緒に使われるとき、特別な拡張機能を提供します。文字列だけではなく、式はオブジェクトまたは配列を返すことができます(詳細は次項)。

クラスとスタイルのバインディング

スタイルのバインディング

v-bind:styleインラインスタイルを設定することができます。

スタイルは プロパティ名:値 の形式のオブジェクトで指定します。

CSS のプロパティ名がハイフンを含む場合は、キャメルケース (camelCase) で記述するか、ハイフンを含むケバブケース (kebab-case) の場合はプロパティ名をクォートで括る必要があります。

HTML
<div id="app">
  <p v-bind:style="{ color: '#fff', backgroundColor: 'green', fontSize: '2rem'}">Hello!</p>
</div>

以下は data プロパティに値を指定する場合の例です。

HTML
<div id="app">
  <p v-bind:style="{ color: textColor, backgroundColor: bgColor, fontSize: fontSize + 'rem'}">Hello!</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      textColor: '#fff',
      bgColor: 'green',
      fontSize: 2
    }
  }
}).mount('#app');

上記のいずれも以下のように出力されます。

<div id="app">
  <p style="color: rgb(255, 255, 255); background-color: green; font-size: 2rem;">Hello!</p>
</div>

または、以下のように data プロパティにスタイルのオブジェクトを用意して参照することもできます。

HTML
<div id="app">
  <p v-bind:style="helloStyle">Hello!</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      helloStyle: {
        color: '#fff',
        backgroundColor: 'green',
        fontSize: '2rem'
      }
    }
  }
}).mount('#app');

配列構文(複数のスタイル)

配列形式で複数のオブジェクトを渡して、同じ要素に複数のスタイルオブジェクトを適用することができます。

オブジェクト間で重複したスタイルは後者が優先されます。

HTML
<div id="app">
  <p v-bind:style="[ color, size, active ]">Hello!</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      color: {
        color: '#fff',
        backgroundColor: 'green',
      },
      size: {
        fontSize: '2rem',
      },
      active: {
        color: 'yellow'  //color の #fff を上書き
      }
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <p style="color: yellow; background-color: green; font-size: 2rem;">Hello!</p>
</div>

ベンダープレフィックスの自動補完

:style で ベンダープレフィックスが必要な CSS プロパティを使用するとき、Vue は自動的に適切なプレフィックスを追加します。

クラスのバインディング

v-bind:class は クラス名:値 の形式のオブジェクトを受け取り、値が true の場合にそのクラスが有効になります。

HTML
<div id="app">
  <!-- v-bind:class={ クラス名: 値 } この例の場合、isActive はデータプロパティで定義-->
  <p v-bind:class="{ active: isActive }">Foo</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      isActive: true
    }
  }
}).mount('#app');

上記の場合、isActive は true なので、active クラスが追加されます。isActive が false の場合は active クラスは追加されません。

<div id="app">
  <p class="active">Foo</p>
</div>

:class ディレクティブは静的な class 属性と共存できます。

HTML
<div id="app">
  <p class="foo" v-bind:class="{ active: isActive, 'text-danger': hasError }">Foo</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      isActive: true,
      hasError: false
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <p class="foo active">Foo</p>
</div>

isActive もしくは hasError が変化するとき、クラスリストはそれに応じて更新されます。例えば、hasError が true になった場合、クラスリストは "foo active text-danger" になります。

以下のようにオブジェクトを指定することもできます。

HTML
<div id="app">
  <p class="foo" v-bind:class="fooClass">Foo</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      fooClass: {
        active: true,
        'text-danger': false
      }
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <p class="foo active">Foo</p>
</div>

オブジェクトを返す算出プロパティにバインドすることもできます。

HTML
<div id="app">
  <p class="foo" v-bind:class="fooClass">Foo</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      isActive: true,
      error: true
    }
  },
  computed: {
    fooClass() {
      return {
        active: this.isActive && !this.error,
        'text-danger': this.error
      }
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <p class="foo text-danger">Foo</p>
</div>

配列構文

:class に配列を渡してクラスのリストを適用することができます。

HTML
<div id="app">
  <p v-bind:class="[activeClass, errorClass]">Foo</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      activeClass: 'active',
      errorClass: 'text-danger'
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <p class="active text-danger">Foo</p>
</div>

リスト内のクラスを条件に応じて切り替えたい場合は、三項演算子式を使うことができます。

HTML
<div id="app">
  <p v-bind:class="[isActive ? activeClass : '', errorClass]">Foo</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      isActive: true,
      activeClass: 'active',
      errorClass: 'text-danger'
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <p class="active text-danger">Foo</p>
</div>

配列構文の内部でオブジェクト構文を使うこともできます。以下は上記と同じことです。

HTML
<div id="app">
  <p v-bind:class="[{ active: isActive }, errorClass]">Foo</p>
</div>

クラス名(文字列)の配列を渡すこともできます。この場合、単純にクラスのリストが出力されます。

HTML
<div id="app">
  <p v-bind:class="['active','text-danger']">Foo</p>
</div>

以下のように出力されます。

<div id="app">
  <p class="active text-danger">Foo</p>
</div>

この場合も三項演算子式を使うことができます。

HTML
<div id="app">
  <p v-bind:class="[isActive ? 'active' : '', hasError ? 'text-danger' : 'text-normal']">Foo</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      isActive: true,
      hasError: false
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <p class="active text-normal">Foo</p>
</div>

コンポーネントでのクラスの指定

単一のルート要素を持つコンポーネントで class 属性を使用すると、それらのクラスがテンプレートのルート要素に追加されます(既存のクラスは上書きされません)。

関連項目:props でない属性

例えば、以下のような単一のルート要素(この場合は1つの p 要素)のコンポーネントを定義した場合、

JavaScript
const app = Vue.createApp({});

app.component('my-component', {
  template: `<p class="foo bar">Hello!</p>`
});
app.mount('#app');

呼び出し側でいくつかのクラスを指定すると、

HTML
<div id="app">
  <my-component class="baz qux"></my-component>
</div>

呼び出し側で指定したクラスは、テンプレートのルート要素に追加され、以下のように出力されます。

<div id="app" data-v-app="">
  <p class="foo bar baz qux">Hello!</p> <!-- ルート要素 -->
</div>

クラスバインディングの場合も同様です。

JavaScript
const app = Vue.createApp({
  //ルートコンポーネントの data
  data() {
    return {
      isActive: true
    }
  }
});

app.component('my-component', {
  template: `<p class="foo bar">Hello!</p>`
});
app.mount('#app');

呼び出し側で v-bind:class を指定すると、

HTML
<div id="app">
  <my-component v-bind:class="{ active: isActive }"></my-component>
</div>

呼び出し側で指定した有効なクラスはテンプレートのルート要素に追加され、以下のように出力されます。

<div id="app" data-v-app="">
<p class="foo bar active">Hello!</p>
</div>

コンポーネントに複数のルート要素がある場合は、どのコンポーネントがこのクラスを受け取るかを $attrs プロパティを使って定義する必要があります。

以下は2つのルート要素がある場合の例です。 v-bind:class="$attrs.class" を指定した要素に呼び出し側で指定したクラスが追加されます(両方に指定することもできます)。

JavaScript
const app = Vue.createApp({});

app.component('my-component', {
  template: `
  <p class="foo">Hello!</p>
  <p class="bar" v-bind:class="$attrs.class">Hello again!</p>
  `
});
app.mount('#app');
HTML
<div id="app">
  <my-component class="baz"></my-component>
</div>

以下のように出力されます。

<div id="app" data-v-app="">
<p class="foo">Hello!</p>
<p class="bar baz">Hello again!</p>
</div>

コンポーネントに複数のルート要素があり、呼び出し側でクラス(props でない属性)を指定した場合、どのコンポーネントがそれらを受け取るかを指定しないと「Extraneous non-props attributes (class) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. 」のような警告がコンソールに出力されます。

v-on

v-on は要素にイベントリスナをアタッチします。イベントタイプは v-on(ディレクティブ名)の後にコロンで引数として指定します。そして v-on 属性の値にメソッド名または式を指定します。

v-on の書式
v-on:イベントタイプ="メソッド名"  //または v-on:イベントタイプ="JavaScript 式"

イベントハンドリング

以下はボタンをクリックすると現在の日時を表示する例です。

HTML
<div id="app">
  <button v-on:click="dateTime">Click</button>
  {{ now }}
</div>

v-on:@ と省略して記述することができます。以下は上記と同じことです。

HTML
<div id="app">
  <button @click="dateTime">Click</button>
  {{ now }}
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      now: ''
    }
  },
  methods: {
    dateTime() {
      this.now = new Date().toLocaleString();
    }
  }
}).mount('#app');

メソッド名を指定する代わりに、JavaScript 式でメソッドを指定する(メソッド名にカッコを付けて式として呼び出す)こともできます。

HTML
<div id="app">
  <button @click="dateTime()">Click</button>
  {{ now }}
</div>

動作テストなどでシンプルなコードの場合は v-on 属性の値に JavaScript 式を直接記述することもできますが、テンプレートにコードが混在するため見通しがよくありません。以下は上記と同じです。

HTML
<div id="app">
  <button @click="now = new Date().toLocaleString()">Click</button>
  {{ now }}
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      now: ''
    }
  }
}).mount('#app');

以下は mouseenter と mouseleave イベントを使って、画像にマウスオーバーした際に画像を差し替える例です。

HTML
<div id="app">
  <img v-bind:src="path" alt="" v-on:mouseenter="onmouseenter" v-on:mouseleave="onmouseleave">
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      path: './images/01.jpg'
    }
  },
  methods: {
    onmouseenter() {
      this.path = './images/02.jpg';
    },
    onmouseleave() {
      this.path = './images/01.jpg';
    }
  }
}).mount('#app');

引数を渡す

メソッド名にカッコを付けて式として呼び出す構文を使えば、イベントリスナに任意の引数を渡すことができます。

HTML
<div id="app">
  <button @click="greet('Hello Vue!')">Greet</button>
  {{ greeting }}
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      greeting: ''
    }
  },
  methods: {
    greet(msg) {
      this.greeting = msg;
    }
  }
}).mount('#app');

複数イベントハンドラ

イベントハンドラ内ではカンマで区切ることで、複数のメソッドを設定することができます。

以下の場合、ボタンをクリックすると、greet() と dateTime() の両方が実行されます 。

HTML
<div id="app">
  <button @click="greet('Hello Vue!'), dateTime()">Click</button>
  <p>{{ greeting }}</p>
  <p>{{ now }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      greeting: '',
      now: ''
    }
  },
  methods: {
    greet(msg) {
      this.greeting = msg;
    },
    dateTime() {
      this.now = new Date().toLocaleString();
    }
  }
}).mount('#app');
イベントオブジェクト

イベントリスナからイベントオブジェクト(DOM イベント)を参照するには、イベントリスナに e や event などの任意の1つの引数を指定して参照することができます。

以下はボタンをクリックすると、そのイベントオブジェクトをコンソールに出力する例です。

HTML
<div id="app">
  <button @click="onclick">Click</button>
</div>
JavaScript
Vue.createApp({
  methods: {
    onclick(e) {
      console.log(e);  //イベントオブジェクトをコンソールに出力
    }
  }
}).mount('#app');

以下は要素をクリックした際のイベントのプロパティを出力する例です。

HTML
<div id="app">
  <div id="foo" @click="showInfo" :style="{width: '200px', height: '200px', backgroundColor: 'yellow'}"></div>
  <p>type:{{ type }}</p>
  <p>target: {{ target }}</p>
  <p>clientX :{{ clientX }}</p>
  <p>clientY :{{ clientY }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      type: '',
      target: '',
      clientX: '',
      clientY: ''
    }
  },
  methods: {
    //イベントオブジェクト(DOM イベント)を引数 e で参照
    showInfo(e) {
      this.type = e.type;  //この場合は click
      this.target = e.target.id;  //この場合は foo
      this.clientX = e.clientX;  //クリックされたブラウザ上のX座標
      this.clientY = e.clientY;  //クリックされたブラウザ上のY座標
    }
  }
}).mount('#app');

DOM イベントを参照 $event

複数の引数を使う場合などでは、呼び出し側(インラインステートメント)で特別な変数 $event を使うことでメソッドに DOM イベントを渡すことができます。

HTML
<div id="app">
<button @click="onclick('Hello', $event)">Click</button>
{{ message }}
</div>
JavaScript
Vue.createApp({
data() {
  return {
    message: ''
  }
},
methods: {
  onclick(msg, e) {
    this.message = msg + '! Event Type: ' + e.type;
    //ボタンをクリックすると Hello! Event Type: click と出力される
  }
}
}).mount('#app');

v-on

イベント修飾子

Vue ではイベントハンドラ内での event.preventDefault() や event.stopPropagation() などをイベント修飾子(event modifiers)を使って呼び出すことができます。

v-on で使用できるイベント修飾子には以下のようなものがあります。

修飾子 概要
.stop 上位の要素への伝播を中止(stopPropagation() に相当)
.prevent デフォルトの動作をキャンセル(preventDefault() に相当)
.capture イベントハンドラをキャプチャモードで作動
.self イベントの発生元がその要素自身の場合にのみ実行
.once イベントハンドラを一度だけ実行(addEventListener の once:true に相当)
.passive パッシブを有効にする(addEventListener の passive:true に相当)

以下は .once を使って一度しか実行されないイベントハンドラを登録する例です。

初回クリック時にのみ実行され、2回目以降のクリックでは何も実行されません。

HTML
<div id="app">
  <button v-on:click.once="onclick">Click</button>
  <p>ラッキーナンバー:{{ luckyNumber }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      luckyNumber: ''
    }
  },
  methods: {
    onclick() {
      this.luckyNumber = Math.floor(Math.random() * 10);
    }
  }
}).mount('#app');

値(イベントハンドラ)を指定せず、修飾子だけ利用することもできます。

以下は contextmenu イベントのデフォルトの動作を中止して、右クリックをしてもコンテキストメニューを表示しないようにする例です。

HTML
<div id="app">
  <div v-on:contextmenu.prevent v-bind:style="{ backgroundColor: 'green', width: '200px', height: '200px'}"></div>
</div>
JavaScript
Vue.createApp({
}).mount('#app');

条件付きのキャンセル

.prevent 修飾子は、イベントのデフォルトの動作を無条件にキャンセルします。以下のように条件付きでキャンセルする場合は、.preventDefault() を使います。テキストボックスに入力された値は v-model で取得できます。

HTML
<div id="app">
  <form v-on:submit="onsubmit">
    <label for="email">Email: </label>
    <input id="email" type="email" v-model="email">
    <input type="submit" value="Send">
  </form>
  <p v-text="message"></p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      email: '',
      message: ''
    }
  },
  methods: {
    onsubmit(e) {
      if (this.email === '') {
        //email が空の場合はデフォルトの動作(フォームの送信)を停止
        e.preventDefault();
        //メッセージを表示
        this.message = 'メールアドレスを入力してください';
      }
    }
  }
}).mount('#app');

イベントの伝播の制御

デフォルトではページ上で発生したイベントは上位の要素にも伝播します(通知されます)。

例えば、以下のような入れ子になった要素がある場合、id が child の要素をクリックすると、クリックイベントはその上位の要素に伝播し、上位の要素に click イベントのハンドラが設定されていれば、それらも実行されます。

HTML
<div id="app">
  <div id="parent" v-on:click="parentClick" :style="[parentStyle, center]">
    parent
    <div id="foo" v-on:click="fooClick" :style="[fooStyle,center]">
      foo
      <div id="child" v-on:click="childClick" :style="[childStyle, center]">
        child
      </div>
    </div>
  </div>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      //スタイル
      parentStyle: {
        backgroundColor: 'aquamarine',
        width: '200px',
        height: '200px'
      },
      fooStyle: {
        backgroundColor: 'lightseagreen',
        width: '140px',
        height: '140px'
      },
      childStyle: {
        backgroundColor: 'lightgreen',
        width: '80px',
        height: '80px'
      },
      center: {
        position: 'absolute',
        top: '50%',
        left: '50%',
        marginRight: '-50%',
        transform: 'translate(-50%, -50%)'
      }
    }
  },
  methods: {
    parentClick(e) {
      console.log('parent listener : 発生元 = ' + e.target.id);
    },
    fooClick(e) {
      console.log('foo listener : 発生元 = ' + e.target.id);
    },
    childClick(e) {
      console.log('child listener : 発生元 = ' + e.target.id);
    }
  }
}).mount('#app');

上記の場合、id が child の要素をクリックするとコンソールには以下のように出力されます。

child listener : 発生元 = child
foo listener : 発生元 = child
parent listener : 発生元 = child

id が foo の要素をクリックするとコンソールには以下のように出力されます。

foo listener : 発生元 = foo
parent listener : 発生元 = foo

上記の場合、id が parent の要素をクリックするとコンソールには以下のように出力されます。

parent listener : 発生元 = parent

伝播のキャンセル .stop

id が child の要素のイベントハンドラに以下のように .stop 修飾子を指定すると、上位の要素(foo と parent)にイベントは伝播されないようになります。

HTML
<div id="child" v-on:click.stop="childClick">

.self

.self を指定すると、その要素でイベントが発生した場合にだけ処理を実行します(バブリングは抑制しない)。

HTML
<div id="app">
  <div id="parent" v-on:click="parentClick">
    parent
    <div id="foo" v-on:click.self="fooClick">
      foo
      <div id="child" v-on:click="childClick">
        child
      </div>
    </div>
  </div>
</div>

例えば、上記のように id が foo の要素のイベントハンドラに .self 修飾子を指定すると id が foo の要素をクリックするとコンソールには以下のように出力されます。バブリングは抑制しないので、上位の parent にイベントは伝播します。

foo listener : 発生元 = foo
parent listener : 発生元 = foo

id が child の要素をクリックするとコンソールには以下のように出力され、foo の要素にはイベントは伝播しません。

child listener : 発生元 = child
parent listener : 発生元 = child

イベント修飾子の連結

イベント修飾子は連結することができますが、連結する順番により意味が異なってきます。

以下は id が foo の要素のイベントハンドラに .stop.self の順番で修飾子を指定する例です。

HTML
<div id="app">
  <div id="parent" v-on:click="parentClick">
    parent
    <div id="foo" v-on:click.stop.self="fooClick">
      foo
      <div id="child" v-on:click="childClick">
        child
      </div>
    </div>
  </div>
</div>

上記の場合、id が foo の要素のバブリングをキャンセルし、自分自身で発生したイベントのみを処理します。

以下のようにすると、id が foo の要素で発生した要素でのみバブリングをキャンセルします。

<div id="foo" v-on:click.self.stop="fooClick">
キー修飾子

Vue では、キーイベントで押されたキーを判別するためのキー修飾子が用意されています。

KeyboardEvent.key で公開されている任意のキー名は、ケバブケースに変換することで修飾子として直接使用できます。また、一般的に使用される以下のようなキーコードのエイリアスが用意されています。

  • .enter
  • .tab
  • .delete ("Delete" と "Backspace" キー両方をキャプチャします)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

以下はテキストボックス内で esc キーが押された場合に値をクリアする例です。

HTML
<div id="app">
  <label for="name">Name</label>
  <input type="text" id="name" v-on:keyup.esc="clear" v-model="name">
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      name: ''
    }
  },
  methods: {
    clear() {
      this.name = ''
    }
  }
}).mount('#app');

システムキーとの組み合わせ(システム修飾子キー)

control や shift などの他のキーやクリックとセットで利用するキーは以下のシステム修飾子キーが利用できます。

  • .ctrl
  • .alt
  • .shift
  • .meta

以下はテキストボックス内で control + h を押すとメッセージを表示する例です。

HTML
<div id="app">
  <label for="name">Name</label>
  <input type="text" id="name" v-on:keyup.ctrl.h="help" v-model="name" v-on:change="clearMsg">
  <p v-text="message"></p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      name: '',
      message: ''
    }
  },
  methods: {
    help() {
      this.message = 'お名前を入力ください'
    },
    clearMsg() {
      if(this.name !=='') {
        this.message = ''
      }
    }
  }
}).mount('#app');

.exact 修飾子

前述の keyup.ctrl.h は control + h が押されているものを検知しますが、control + shift + h のように他のキーが押されてもイベントをトリガーします。

システム修飾子の正確な組み合わせを制御するには .exact 修飾子を使用することができます。

前述の例を、厳密に control + h が押された場合にイベントをトリガーするには以下のように記述します。

<input type="text" id="name" v-on:keyup.ctrl.h.exact="help" ...>

マウスボタンの修飾子

マウスの特定のボタンを検知する以下のような修飾子も用意されています。

  • .left
  • .right
  • .middle

以下は id が foo の要素内で右クリックすると独自のコンテキストメニューを表示し、左クリックでコンテキストメニューを非表示にする例です。

左クリック(@click.lef)でメソッドの hide を呼び出し、右クリック(@click.right.prevent)でメソッド showMenu を呼び出すようにしています。

右クリックでは .prevent を指定して、デフォルトの動作(システムのコンテキストメニューの表示)をキャンセルしています。

コンテキストメニューの表示・非表示は v-show で切り替えています。

HTML
<div id="app">
  <div id="foo" @click.left="hide" @click.right.prevent="showMenu">
    右クリックでコンテキストメニューを表示し、左クリックで非表示にします。
  </div>
  <div :style="contextStyle" v-show="show">
    <ul>
      <li><a href="../help.html">Help</a></li>
      <li><a href="../faq.html">FAQ</a></li>
      <li><a href="../contact.html">Contact</a></li>
    </ul>
  </div>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      contextStyle: {
        left:0,
        top:0,
      },
      show: false  //初期状態ではコンテキストメニューを非表示
    }
  },
  methods: {
    hide() {
      this.show = false; //左クリックでンテキストメニューを非表示に
    },
    showMenu(e) {
      this.contextStyle = {
        //クリックされた位置を取得してスタイルに設定
        left: e.pageX + 'px',
        top: e.pageY + 'px',
        position: 'absolute',
        backgroundColor: '#ccc'
      };
      this.show = true; //右クリックでンテキストメニューを表示
    }
  }
}).mount('#app');

右クリックの代わりに、shift を押しながらクリックすると独自のコンテキストメニューを表示するには、以下のように記述します。この場合は .prevent は不要です。

<div id="foo" @click.left="hide" @click.shift="showMenu" >

v-model

form の input 要素や textarea 要素、 select 要素に双方向データバインディングを付与するためには、v-model を使用することができます。

双方向データバインディング

双方向データバインディングはデータオブジェクトとテンプレートの状態を同期する仕組みで、データオブジェクトの変更を検知してテンプレートに反映するだけでなく、テンプレート(テキストボックスへの入力など)の変更を検知してデータオブジェクトのデータを更新します。

value 属性や checked 属性、selected 属性は無視される

v-model は現在アクティブなインスタンスの data を信頼できる情報源として扱うため、フォームのコントロール要素の value 属性(テキストボックスの場合)や checked 属性、selected 属性の初期値を無視します。

これらの属性を指定すると [Vue warn]: Template compilation error: Unnecessary value binding used alongside v-model. のような警告が発生し、設定した値は無視されます。

初期値の宣言は JavaScript 側(コンポーネントの data オプション内)で行います。

フォーム入力バインディング

以下はテキストボックスに入力された名前を使って「Hello, xxxx」と表示する例です。

初期状態ではデータオブジェクト側の値(Foo)が、テキストボックスの値に反映されていて、テキストボックスに入力すると、データオブジェクトに反映され、「Hello, xxxx」が更新されます。

HTML
<div id="app">
  <input v-model="name"/>
  <p>Hello, {{ name }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      name: 'Foo'  //name の初期値
    }
  }
}).mount('#app');

内部的には、v-model は以下のようなプロパティとイベントを使用しています。

  • input 要素の text 及び textarea 要素には、value プロパティ(属性)と input イベントを用います
  • チェックボックス及びラジオボタンには、checked プロパティ(属性)と change イベントを用います
  • select フィールドには、value プロパティ(属性)と change イベントを用います

以下は前述の v-model を v-on と v-bind で書き換えた例です。

HTML
<div id="app">
  <input v-on:input="updateName" v-bind:value="name"/>
  <p>Hello, {{ name }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      name: 'Foo'
    }
  },
  methods: {
    updateName(e) {
      this.name = e.target.value;
    }
  }
}).mount('#app');

または、methods を使わずに以下のように記述しても同じことです。$event はイベントオブジェクトを表す変数です。

HTML
<div id="app">
  <input v-on:input="name = $event.target.value;" v-bind:value="name"/>
  <p>Hello, {{ name }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      name: 'Foo'
    }
  }
}).mount('#app');

ラジオボタン

ラジオボタンでは全ての選択項目で同一の v-model (同じ値の v-model 属性)を指定します。

HTML
<div id="app">
  <label for="wine">Wine</label>
  <input type="radio" id="wine" value="wine" v-model="drink">
  <label for="beer">Beer</label>
  <input type="radio" id="beer" value="beer" v-model="drink">
  <label for="sake">Sake</label>
  <input type="radio" id="sake" value="sake" v-model="drink">
  <p>{{ drink }}</p>
</div>

この例では初期値を空文字にしていますが、value 属性に指定した値のいずれかを設定すれば、その項目が初期状態で選択されます。任意の文字列を指定することもできます。

JavaScript
Vue.createApp({
  data() {
    return {
      drink: ''  //初期値
    }
  }
}).mount('#app'); 

チェックボックス(単一項目)

チェックボックスの値はデフォルトでは真偽値(true または false)として管理されます。

HTML
<div id="app">
  <label for="agree">同意します</label>
  <input type="checkbox" id="agree" v-model="agree">
  <p>{{ agree }}</p>
</div>

この例では agree の初期値を false にしているので、チェックボックスは選択されていない状態になります。初期値を true にすると初期状態でチェックボックスは選択された状態になります。

任意の文字列を初期値に指定することもできます(チェックボックスは選択されていない状態になります)。

JavaScript
Vue.createApp({
  data() {
    return {
      agree: false  //初期値
    }
  }
}).mount('#app');

チェックボックスの値を真偽値以外にするには、true-value または false-value 属性を利用します。

以下の場合、agree の値は選択されている場合は「yes」、選択されていない場合は「no」になります。

<input type="checkbox" v-model="agree" true-value="yes" false-value="no">

チェックボックス(複数項目)

複数項目のチェックボックスの場合、全ての項目に対して同一の v-model(同じ値の v-model 属性)を指定します。

HTML
<div id="app">
  <label for="apple">Apple</label>
  <input type="checkbox" id="apple" value="apple" v-model="fruits">
  <label for="banana">Banana</label>
  <input type="checkbox" id="banana" value="banana" v-model="fruits">
  <label for="orange">Orange</label>
  <input type="checkbox" id="orange" value="orange" v-model="fruits">
  <p>{{ fruits }}</p>
</div>

プロパティの値は配列で指定します。以下の場合は、空の配列を指定しているので、初期状態では何も選択されていない状態になります。

value 属性に指定した値のいずれかを設定すれば、その項目が初期状態で選択されます。

JavaScript
Vue.createApp({
  data() {
    return {
      fruits: []
    }
  }
}).mount('#app');

セレクトボックス

セレクトボックスの場合は select 要素に v-model を指定します。

HTML
<div id="app">
  <label for="color">Color</label>
  <select id="color" v-model="color">
    <option disabled value="">選択してください</option>
    <option>Red</option>
    <option>Blue</option>
    <option>Green</option>
  </select>
  <p>{{ color }}</p>
</div>

セレクトボックスでは option 要素のいずれにも selected 属性を指定していない場合、最初の option 要素が選択状態になります(v-model では selected 属性は指定しません)。 この例の場合、初期状態では「選択してください」の option 要素が選択されます。

「選択してください」の option 要素には disabled 属性を指定し、value 属性の値を空に(data の初期値を指定)しています。

※ v-model の式の初期値がいずれのオプションとも一致しない場合、value を持たない disabled なオプションを追加しておくことが推奨されています。

選択されると option 要素に記述されている文字(Red, Blue, Green)が color の値に更新されます。

JavaScript
Vue.createApp({
  data() {
    return {
      color: ''  //初期値
    }
  }
}).mount('#app');

以下のように value 属性に値を指定することもできます。この場合、選択されると value 属性の値(red, blue, green)が color の値に更新されます。

HTML
<select id="color" v-model="color">
  <option disabled value="">選択してください</option>
  <option value="red">Red</option>
  <option value="blue">Blue</option>
  <option value="green">Green</option>
</select>

複数選択できるようにするには multiple 属性を指定します。プロパティ(color)には配列が格納されます。

HTML
<div id="app">
  <label for="color">Color</label>
  <select id="color" v-model="color" multiple>
    <option disabled value="">選択してください</option>
    <option>Red</option>
    <option>Blue</option>
    <option>Green</option>
  </select>
  <p>{{ color }}</p>
</div>

初期状態でいずれかを選択状態にするには配列で指定します(以下は何も選択しない場合)。

JavaScript
Vue.createApp({
  data() {
    return {
      color: ''  //初期値 (選択状態にするには配列で指定。例 ['Red'] 複数指定可能)
    }
  }
}).mount('#app');

動的なオプション

v-for を使って動的なオプション(option 要素)をレンダリングすることができます。

HTML
<div id="app">
  <label for="color">Color</label>
  <select id="color" v-model="color">
    <option v-for="option in options" v-bind:value="option.value">
      {{ option.text }}
    </option>
  </select>
  <p>{{ color }}</p>
</div>

この例の場合、option 要素のテキストには option.text が、value 属性の値には option.value がレンダリングされます。

JavaScript
Vue.createApp({
  data() {
    return {
      color: 'green',  //初期値
      options: [
        { text: '赤', value: 'red' },
        { text: '青', value: 'blue' },
        { text: '緑', value: 'green' }
      ]
    }
  }
}).mount('#app');

以下は v-bind にオブジェクトを指定して、複数の属性を設定し、最初の要素のみ disabled 属性を出力する例です。

HTML
<div id="app">
  <label for="color">Color</label>
  <select id="color" v-model="color">
    <option v-for="option in options" v-bind="{ value: option.value, disabled: option.disabled }">
      {{ option.text }}
    </option>
  </select>
  <p>{{ color }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      color: '',  //初期値
      options: [
        { text: '選択してください', value: '' , disabled: true},
        { text: '赤', value: 'red' },
        { text: '青', value: 'blue' },
        { text: '緑', value: 'green' }
      ]
    }
  }
}).mount('#app');

値のバインディング

ラジオボタンやチェックボックス、セレクトボックスなどには v-bind を使用することで入力値に文字列以外の値もバインドすることができます(値のバインディング)。

以下は v-bind:value を使って値にオブジェクトをバインドする例です。

HTML
<div id="app">
  <label for="wine">Wine</label>
  <input type="radio" id="wine" v-model="drink" v-bind:value="{en:'Wine',jp:'ワイン'}">
  <label for="beer">Beer</label>
  <input type="radio" id="beer" v-model="drink" v-bind:value="{en:'Beer',jp:'ビール'}">
  <label for="sake">Sake</label>
  <input type="radio" id="sake" v-model="drink" v-bind:value="{en:'Sake',jp:'日本酒'}">
  <p>{{ drink.en + ':' + drink.jp }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      drink: { en: '', jp: ''}
    }
  }
}).mount('#app');

例えば、Beer のラジオボタンを選択すると、以下のような出力になります。

<div id="app" data-v-app="">
  <label for="wine">Wine</label>
  <input type="radio" id="wine" value="[object Object]">
  <label for="beer">Beer</label>
  <input type="radio" id="beer" value="[object Object]">
  <label for="sake">Sake</label>
  <input type="radio" id="sake" value="[object Object]">
  <p>Beer:ビール</p>
</div>
動作オプション(修飾子)

v-model ではバインド時の挙動を制御するためのオプション(修飾子)が用意されています。

修飾子はディレクティブの後に .(ドット)に続けて指定します(例:v-model.lazy ※複数連結することも可能)。

.lazy

デフォルトでは v-model は input イベント(キー入力のタイミング)で入力値とデータを同期しますが、lazy 修飾子を指定することで、change イベント(フォーカスが外れたタイミング)で同期するよう変更できます。

<div id="app">
  <input v-model.lazy="name"/>
  <p>Hello, {{ name }}</p>
</div>

.number

ユーザ入力を自動的に number へ型キャストさせたい場合は、v-model で管理している input に number 修飾子を指定します。

input 要素の type が "text" または type を省略した場合によく使われます。 type="number" の場合、Vue は文字列値を自動的に数値へ変換できるため、v-model に .number を追加する必要はありません。

値が parseFloat() で解析できない場合は、元の値(文字列)が返されます。

HTML
<div id="app">
  <input v-model.number="age" type="text" v-on:input="onchange" />
  <p>{{ age }}</p>
</div>

この例の場合、v-on:input で input イベントで age の値が数値かどうかを判定してコンソールに出力しています。

JavaScript
Vue.createApp({
  data() {
    return {
      age: 0
    }
  },
  methods: {
    onchange() {
      console.log(typeof this.age ==='number')
    }
  }
}).mount('#app');

.trim

ユーザ入力から前後の空白を自動で取り除きたい場合は、trim 修飾子を指定します。

HTML
<div id="app">
  <input v-model.trim="msg"/>
  <p>{{ msg }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      msg: ''
    }
  }
}).mount('#app');

v-if v-else v-else-if

ブロック(要素)を条件に応じてレンダリング(出力)したい場合には、v-ifv-elsev-else-if ディレクティブを利用することができます。また、v-show を使って条件に応じて表示・非表示を切り替えることもできます。

条件付きレンダリング

v-if

v-if は式が真(true)を返す場合のみ、そのブロックをレンダリングします。

HTML
<div id="app">
  <div v-if="show">
    show が true の場合に表示されます
  </div>
</div>

この場合、以下のデータオブジェクトの show は true なので上記の div ブロックはレンダリングされます。

JavaScript
Vue.createApp({
  data() {
    return {
      show: true
    }
  }
}).mount('#app');

以下はチェックボックスで表示・非表示を切り替える例です。v-model でチェックボックスを show に紐付けているので、チェックが入れば show は true になり、チェックを外せば show は false になり、表示・非表示が切り替わります。

HTML
<div id="app">
  <label for="check">表示</label>
  <input type="checkbox" id="check" v-model="show">
  <div v-if="show">
    チェックを入れると表示されます
  </div>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      show: false
    }
  }
}).mount('#app');

v-else

v-else で "else ブロック" を追加することができます。

v-else は、v-if または v-else-if 要素の直後になければなりません(前の兄弟要素に v-if または v-else-if を持たなければなりません)。それ以外の場合は認識されません。

HTML
<div id="app">
  <label for="check">表示</label>
  <input type="checkbox" id="check" v-model="show">
  <div v-if="show">
    チェックを入れると表示されます
  </div>
  <div v-else >
    チェックを外すと表示されます
  </div>
</div>

この例の場合、初期状態では show は false なので「チェックを外すと表示されます」のブロックがレンダリングされます。

JavaScript
Vue.createApp({
  data() {
    return {
      show: false
    }
  }
}).mount('#app');

v-else-if

v-else-if は、v-if の "else if block" として機能します。また、複数回連結することもできます。

v-else と同様、v-else-if 要素は v-if 要素または v-else-if 要素の直後になければなりません。

以下はボタンをクリックすると乱数を生成して、その値により表示するブロックを切り替える例です。

HTML
<div id="app">
  <button v-on:click="onclick">Click</button>
  <div v-if="rand >= 0 &&  rand < 0.5">
    rand の値が 0.5 未満の場合に表示されるブロック
  </div>
  <div v-else-if="rand >= 0.5 && rand < 0.8">
    rand の値が 0.5 以上 0.8 未満の場合に表示されるブロック
  </div>
  <div v-else>
    rand の値が 0.8 以上の場合に表示されるブロック
  </div>
  <p>rand: {{ rand }}</p>
</div>

初期状態では rand の値は 0 なので「rand の値が 0.5 未満の場合に表示されるブロック」が表示されます。ボタンをクリックすると、生成される乱数の値により表示されるブロックが切り替わります。

JavaScript
Vue.createApp({
  data() {
    return {
      rand: 0  // 初期値
    }
  },
  methods: {
    onclick() {
       //クリックすると rand の値を乱数で更新
      this.rand = Math.random();
    }
  }
}).mount('#app');
</script>
template タグ

<template> タグ(template 要素)に v-if や v-else、v-else-if、及び v-for ディレクティブを使用して複数の要素を束ねる(ラップする)ことができます。

これらのディレクティブと使用する場合、template 要素自身は出力はされないので(余計な div 要素を出力せずに)、複数の要素を簡単に切り替えることができます。

HTML
<div id="app">
  <template v-if="show">
    <h3>Lorem</h3>
    <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. </p>
  </template>
  <template v-else>
    <h3>Ratione</h3>
    <p>Ratione at omnis voluptate cupiditate consequuntur dolore molestiae? </p>
  </template>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      show: false
    }
  }
}).mount('#app');

この例の場合、show が false なので、<template v-else>〜</template> の内側部分が出力されます。

最終的にレンダリングされる結果には、<template> 要素は含まれません。

<div id="app">
  <h3>Ratione</h3>
  <p>Ratione at omnis voluptate cupiditate consequuntur dolore molestiae? </p>
</div>

v-if や v-else、v-else-if、v-for ディレクティブを指定しない <template> 要素は、HTML 要素として扱われ、マークアップとして HTML に残りますが描画されません(<template> タグで囲われたマークアップは DocumentFragment になります)。

v-show

v-show は式が真(true)を返す場合に、その要素を表示します。v-if 同様、条件に応じて要素を表示・非表示にすることができ、使用方法はほとんど同じです。

但し、v-show による要素は常レンダリングされて(DOM に維持して)、要素の display CSS プロパティを切り替えます。

また、v-show は <template> 要素をサポートせず、v-else とも連動しません。

以下はチェックボックスで表示・非表示を切り替える例です。v-model でチェックボックスを show に紐付けているので、チェックが入れば show は true になり、チェックを外せば show は false になり、表示・非表示が切り替わります(v-if のサンプルと同じもので、v-if を v-show に書き換えただけです)。

HTML
<div id="app">
  <label for="check">表示</label>
  <input type="checkbox" id="check" v-model="show">
  <div v-show="show">
    チェックを入れると表示されます
  </div>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      show: false
    }
  }
}).mount('#app');

この例の場合、初期状態では show は false なので div ブロックは CSS のインラインスタイル display:none により非表示になっています。

<div style="display: none;"> チェックを入れると表示されます </div>

v-if と v-show の使い分け

v-if は初期表示において false の場合、何もしません。条件付きブロックは、条件が最初に true になるまでレンダリング(出力)されません(遅延レンダリング)。

v-show は要素は初期条件に関わらず常にレンダリングされ、CSS display プロパティを使って表示・非表示を切り替えます。

そのため、頻繁に何かを切り替える必要があれば v-show を使用し、条件が実行時に変更することがほとんどない場合は、v-if を使用するのが一般的です。

v-if vs v-show

v-for

v-for は、指定された配列やオブジェクトから要素を順番に取り出し、ループ処理します。

配列から要素を順番に取得

v-for を使用して、配列に基づいてアイテムのリストをレンダリングすることができます。 v-for ディレクティブの値は、item in items の形式の特別な構文が必要で、 items はソースデータの配列、item は繰り返される(取り出される)配列要素のエイリアス(仮変数)です。

<elem v-for="item in items"> ...</elem> <!-- elem は任意の要素 -->

in の代わりに of を使用することもできます。

<elem v-for="item of items"> ...</elem>

以下は data プロパティに用意した配列 fruits の要素 fruit を順番に取得して li 要素をループしてレンダリングする例です。

HTML
<div id="app">
  <ul>
    <li v-for="fruit in fruits">
      {{ fruit }}
    </li>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      fruits: ['apple', 'banana', 'orange']
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <ul>
    <li>apple</li>
    <li>banana</li>
    <li>orange</li>
  </ul>
</div>

以下はオブジェクトの配列 contacts から順番にオブジェクト(contact)を取り出し、tr 要素をループしてオブジェクトのプロパティを td 要素に出力する例です。

HTML
<div id="app">
  <table>
    <tr>
      <th>Name</th>
      <th>Tel</th>
      <th>Email</th>
    </tr>
    <tr v-for="contact in contacts">
      <td>{{ contact.name }}</td>
      <td>{{ contact.tel }}</td>
      <td>{{ contact.email }}</td>
    </tr>
  </table>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      contacts: [
        {
          name: 'Foo',
          tel: '123-456-789',
          email: 'foo@example.com'
        },
        {
          name: 'Bar',
          tel: '987-654-321',
          email: 'bar@example.com'
        }
      ]
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <table>
    <tbody>
      <tr>
        <th>Name</th>
        <th>Tel</th>
        <th>Email</th>
      </tr>
      <tr>
        <td>Foo</td>
        <td>123-456-789</td>
        <td>foo@example.com</td>
      </tr>
      <tr>
        <td>Bar</td>
        <td>987-654-321</td>
        <td>bar@example.com</td>
      </tr>
    </tbody>
  </table>
</div>

v-for にオブジェクトの分割代入を使って以下のように記述することもできます。

HTML
<div id="app">
  <table>
    <tr>
      <th>Name</th>
      <th>Tel</th>
      <th>Email</th>
    </tr>
    <tr v-for="{name, tel, email} in contacts">
      <td>{{ name }}</td>
      <td>{{ tel }}</td>
      <td>{{ email }}</td>
    </tr>
  </table>
</div>

インデックス番号の取得

v-for は現在のアイテムに対する配列のインデックスを、任意の 2 つ目の引数としてサポートしています。

以下の構文を使うことで、item には配列の要素、index にはインデックス番号をセットすることができます。

<elem v-for="(item, index) in items"> ...</elem>

以下はインデックス番号を一緒に出力する例です。

HTML
<div id="app">
  <ul>
    <li v-for="(item, index) in items">
      {{ index }} : {{ item.name }}
    </li>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      items: [
        { name: 'Foo' },
        { name: 'Bar' },
        { name: 'Baz' }
      ]
    }
  }
}).mount('#app');

インデックス番号は0から始まるので、以下のように出力されます。

<div id="app">
  <ul>
    <li>0 : Foo</li>
    <li>1 : Bar</li>
    <li>2 : Baz</li>
  </ul>
</div>

v-for と forEach

配列に基づいて項目のリストをレンダリングする場合、v-for は配列のメソッド forEach と同じような処理になります。

HTML
<div id="app">
  <ul>
    <li v-for="(fruit, index) in fruits">
      {{ index }} : {{ fruit }}
    </li>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      fruits: ['apple', 'banana', 'orange']
    }
  }
}).mount('#app');

//配列
const fruits = ['apple', 'banana', 'orange'];

// forEach() でコンソールに出力
fruits.forEach((fruit, index) => {
  console.log(`${ index } : ${ fruit }`)
})
出力
<div id="app" data-v-app="">
  <ul>
    <li>0 : apple</li>
    <li>1 : banana</li>
    <li>2 : orange</li>
  </ul>
</div>

<!--
コンソールへの出力
0 : apple
1 : banana
2 : orange
-->

オブジェクトのプロパティを取得

オブジェクトのプロパティに対して、v-for を使って反復処理することもできます。

但し、オブジェクトを反復処理するとき、順序は全ての JavaScript エンジンの実装で一貫性が保証されていません(必ずしも定義順に並ぶわけではないので注意が必要です)。

オブジェクトの v-for

以下はオブジェクト guitar のプロパティを順にリスト表示する例です。

HTML
<div id="app">
  <ul>
    <li v-for="value in guitar">
      {{ value }}
    </li>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      guitar: {
        model: 'ES-175',
        type: 'Full Accoustic',
        brand: 'Gibson'
      }
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <ul>
    <li>ES-175</li>
    <li>Full Accoustic</li>
    <li>Gibson</li>
  </ul>
</div>

2 つ目の引数としてプロパティ名(キー)も取得できます。

HTML
<div id="app">
  <ul>
    <li v-for="(value, key) in guitar">
      {{ key }} : {{ value }}
    </li>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      guitar: {
        model: 'ES-175',
        type: 'Full Accoustic',
        brand: 'Gibson'
      }
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <ul>
    <li>model : ES-175</li>
    <li>type : Full Accoustic</li>
    <li>brand : Gibson</li>
  </ul>
</div>

3 つ目の引数としてインデックス番号も取得できます。

HTML
<div id="app">
  <ul>
    <li v-for="(value, key, i) in guitar">
      {{ i }}: {{ key }} : {{ value }}
    </li>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      guitar: {
        model: 'ES-175',
        type: 'Full Accoustic',
        brand: 'Gibson'
      }
    }
  }
}).mount('#app');

以下のように出力されます。

<div id="app">
  <ul>
    <li>0: model : ES-175</li>
    <li>1: type : Full Accoustic</li>
    <li>2: brand : Gibson</li>
  </ul>
</div>

オブジェクトに適用する場合は Object.keys() の結果を反復処理

v-for で、オブジェクトの各プロパティを反復処理する場合は、オブジェクトに対して Object.keys() を呼び出した結果を反復処理します。

Object.keys() はオブジェクトが持つプロパティの名前(キー)の配列を返します。

const guitar = {
  model: 'ES-175',
  type: 'Full Accoustic',
  brand: 'Gibson'
}
console.log(Object.keys(guitar))
//以下が出力される
(3) ['model', 'type', 'brand']
HTML
<div id="app">
  <ul>
    <li v-for="(value, key, i) in guitar">
      {{ i }}: {{ key }} : {{ value }}
    </li>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      guitar: {
        model: 'ES-175',
        type: 'Full Accoustic',
        brand: 'Gibson'
      }
    }
  }
}).mount('#app');

//オブジェクト
const guitar = {
  model: 'ES-175',
  type: 'Full Accoustic',
  brand: 'Gibson'
}

//オブジェクトのキーの配列を取得して反復処理
Object.keys(guitar).forEach( (key, index) => {
  console.log(` ${ index }: ${ key } : ${ guitar[key] } `)
});
<div id="app" data-v-app="">
  <ul>
    <li>0: model : ES-175</li>
    <li>1: type : Full Accoustic</li>
    <li>2: brand : Gibson</li>
  </ul>
</div>

<!--
コンソールへの出力
0: model : ES-175
1: type : Full Accoustic
2: brand : Gibson
-->

Map のエントリーの取得

v-for は、ネイティブの MapSet を含む、反復処理プロトコルを実装した値でも動作します。

以下はマップ myMap のエントリー(キーと値からなる2要素の配列)を順にリスト表示する例です。

HTML
<div id="app">
  <ul>
    <li v-for="entry in myMap">
      {{ entry }}
    </li>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      myMap : new Map([
        ['NY', 'New York'],
        ['SF', 'San Francisco'],
        ['LA', 'Los Angeles']
      ])
    }
  }
}).mount('#app');

以下のように出力されます。Mustache 構文で {{entry[0]}} とするとキーが、{{entry[1]}} とすれば値を取得することができます。

<div id="app">
  <ul>
    <li>["NY","New York"]</li>
    <li>["SF","San Francisco"]</li>
    <li>["LA","Los Angeles"]</li>
  </ul>
</div>

以下のように配列の分割代入を使えば、キーと値をそれぞれ取得できます。

HTML
<div id="app">
  <ul>
    <li v-for="[key, val] in myMap">
      {{ key }} : {{ val }}
    </li>
  </ul>
</div>

以下のように出力されます。

<div id="app">
  <ul>
    <li>NY : New York</li>
    <li>SF : San Francisco</li>
    <li>LA : Los Angeles</li>
  </ul>
</div>

以下はインデックスを取得する例です。以下の場合、マップのキーと値はエントリーとして取得してから配列の要素(entry[0]、entry[1])としてアクセスしています。

<div id="app">
  <ul>
    <li v-for="(entry, index) in myMap">
      {{ index }} : {{ entry[0] }} : {{ entry[1] }}
    </li>
  </ul>
</div>

または、エントリーに分割代入を使って、以下のように記述しても同じです。

<div id="app">
  <ul>
    <li v-for="([key, val], index) in myMap">
      {{ index }} : {{ key }} : {{ val }}
    </li>
  </ul>
</div>

以下のように出力されます。

<div id="app">
  <ul>
    <li>0 : NY : New York</li>
    <li>1 : SF : San Francisco</li>
    <li>2 : LA : Los Angeles</li>
  </ul>
</div>

数値を列挙

v-for は整数値を取ることもできます。指定された数だけテンプレートが繰り返されます。

HTML
<div id="app">
  <span v-for="n in 5">{{ n * 10 + ' '}}</span>
</div>
JavaScript
Vue.createApp({
}).mount('#app');

以下のように出力されます。

<div id="app">
  <span>10 </span><span>20 </span><span>30 </span><span>40 </span><span>50 </span>
</div>
template タグを使う

v-if 同様、複数の要素のブロックをレンダリングするために、v-for で <template> タグを使うこともできます。

HTML
<div id="app">
  <template v-for="guitar in guitars">
    <h3>{{ guitar.model }}</h3>
    <ul>
      <li>Type: {{ guitar.type }}</li>
      <li>Brand: {{ guitar.brand }}</li>
    </ul>
  </template>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      guitars: [
        {
          model: 'ES-175',
          type: 'Full Accoustic',
          brand: 'Gibson'
        },
        {
          model: 'ES-335',
          type: 'Semi Accoustic',
          brand: 'Gibson'
        }
      ]
    }
  }
}).mount('#app');

以下のように出力されます。<template> 要素は出力されません。

<div id="app" data-v-app="">
  <h3>ES-175</h3>
  <ul>
    <li>Type: Full Accoustic</li>
    <li>Brand: Gibson</li>
  </ul>
  <h3>ES-335</h3>
  <ul>
    <li>Type: Semi Accoustic</li>
    <li>Brand: Gibson</li>
  </ul>
</div>
配列の変更

Vue 2x では、配列におけるインデックスと一緒にアイテムを直接セットする変更は検知されませんでしたが、3x では検知されるようになっています。

以下はボタンをクリックすると、配列 users の最初の要素をインデックスを使って変更する例です。

HTML
<div id="app">
  <input type="button" value="Change" @click="onclick">
  <ul>
    <li v-for="user in users">{{ user }}</li>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      users: ['Foo', 'Bar', 'Baz']
    }
  },
  methods: {
    onclick() {
      this.users[0] = "Fox"
      //または this.users.splice(0,1, "Fox");
    }
  }
}).mount('#app');

または、Vue によって拡張された配列のメソッドの1つ splice を使っても、変更は Vue に通知されます。

以下は配列の要素のプロパティを変更する例です。

HTML
<div id="app">
  <input type="button" value="Change" @click="onclick">
  <ul>
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      users: [
        {id: '001', name: 'Foo', age: 22},
        {id: '002', name: 'Bar', age: 30},
        {id: '003', name: 'Baz', age: 56},
      ]
    }
  },
  methods: {
    onclick() {
      this.users[0].name = "Fox"
      //または this.users[0] = {id: '001', name: 'Fox', age: '22'}
    }
  }
}).mount('#app');

以下は splice を使って配列の要素を変更する例です。

Vue.createApp({
  data() {
    return {
      users: [
        {id: '001', name: 'Foo', age: 22},
        {id: '002', name: 'Bar', age: 30},
        {id: '003', name: 'Baz', age: 56},
      ]
    }
  },
  methods: {
    onclick() {
      this.users.splice(0,1, {id: '001', name: 'Fox', age: '24'});
      console.log(this.users[0].age)
    }
  }
}).mount('#app');

Vue によって拡張された配列の変更メソッドには以下のようなものがあります。

Vue によって拡張された配列の変更メソッド
メソッド 概要
push() 配列の最後に1個または複数の要素を追加
pop() 配列の最後の要素を取り除く
shift() 配列の先頭にある要素を取り除く
unshift() 配列の先頭に1個または複数の要素を追加
splice() 配列の要素を取り除いたり、置き換えたり、新しい要素を追加
sort() 配列の要素をソート(並べ替え)
reverse() 配列の要素の順番を反転

配列の置き換え

filter()、concat()、slice() などのメソッドは、元の配列を変更せず、常に新しい配列を返します。これらのメソッドを使用する場合は、新しいもので古い配列を置き換えることで変更できます。配列の置き換え

以下は concat() を使って要素を配列に追加する例です。concat() は配列に要素を追加して、新たな配列を生成して返すメソッドなので、元の配列は変更されないため、Vue は変更を検知できないません。そのため、以下では元の配列を新たな配列で置き換えています。

HTML
<div id="app">
  <input type="button" value="Change" @click="onclick">
  <ul>
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      users: [
        {id: '001', name: 'Foo', age: 22},
        {id: '002', name: 'Bar', age: 30},
        {id: '003', name: 'Baz', age: 56},
      ]
    }
  },
  methods: {
    onclick() {
      //元の配列を新たな配列で置き換える
      this.users = this.users.concat({id: '004', name: 'Qux', age: 18})
    }
  }
}).mount('#app');
配列のフィルターやソート

元のデータを実際に変更またはリセットせずに、フィルタリングやソートされたバージョンの配列を表示する場合は算出プロパティを利用できます。算出プロパティが使えない場合はメソッドを利用することができます。

フィルタ/ソートされた結果の表示

以下は age が 30以上のユーザーを表示する例です。

算出プロパティ overThirty は filter() の結果を返すことで、フィルターした配列を v-for に渡しています。filter() はコールバック関数の条件に合致する(結果が true の)要素のみを返します。

HTML
<div id="app">
  <ul>
    <li v-for="user in overThirty" :key="user.id">
      Name: {{ user.name }} Age: {{ user.age }}
    </li>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      users: [
        {id: '001', name: 'Foo', age: 22},
        {id: '002', name: 'Bar', age: 30},
        {id: '003', name: 'Baz', age: 56},
      ]
    }
  },
  computed: {
    overThirty() {
      return this.users.filter(user => user.age >=30);
    }
  }
}).mount('#app');
v-for と v-if

v-if と v-for を同時に利用することは推奨されません。v-for と v-if

例えば、前述の例は v-if と v-for を使って以下のように書き換えても良さそうですが、実際には「Uncaught TypeError: Cannot read properties of undefined (reading 'age')」のようなエラーになります。

<div id="app">
  <ul>
    <li v-for="user in users" v-if="user.age >=30"><!-- エラーになる -->
      Name: {{ user.name }} Age: {{ user.age }}
    </li>
  </ul>
</div>

v-if と v-for が同じノードに存在するとき、 v-if は v-for よりも高い優先度を持ちます。そのため、上記の場合では v-for で生成される仮変数 user が v-if を評価するタイミングではまだ生成されていないため(user は undefined)、user のプロパティ age にアクセスできないのでエラーになります。

これは以下のように v-for を <template> タグで囲み、移動させることで修正できます。

HTML
<div id="app">
  <ul>
    <template v-for="user in users">
      <li v-if="user.age >=30" :key="user.id">
        Name: {{ user.name }} Age: {{ user.age }}
      </li>
    </template>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      users: [
        {id: '001', name: 'Foo', age: 22},
        {id: '002', name: 'Bar', age: 30},
        {id: '003', name: 'Baz', age: 56},
      ]
    }
  }
}).mount('#app');
key 属性

key 属性は要素を識別するための情報を Vue に通知するための属性で、ノードの新しいリストを古いリストに対して比較するときに、VNodes を識別するヒントとして主に使われます。

可能なときはいつでも v-for に key 属性を与えることが推奨されています。

以下はボタンをクリックすると shift() で配列の先頭の要素を削除する例です。Chrome のデベロッパーツールの[要素]タブで確認すると、ボタンをクリックすると、ul 要素と全ての li 要素が色が変わり、再生成されるのがわかります。

HTML
<div id="app">
  <input type="button" value="Remove" @click="onclick">
  <ul>
    <li v-for="user in users">{{ user.name }}</li>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      users: [
        {id: '001', name: 'Foo', age: 22},
        {id: '002', name: 'Bar', age: 30},
        {id: '003', name: 'Baz', age: 56},
      ]
    }
  },
  methods: {
    onclick() {
      this.users.shift();
    }
  }
}).mount('#app');

以下のように v-bind で key 属性に一意の値を指定して、デベロッパーツールの[要素]タブで確認すると、対象の要素のみが削除され、その他の li 要素は維持されているのが確認できます(ul 要素のみが色が変わる)。

<div id="app">
  <input type="button" value="Remove" @click="onclick">
  <ul>
    <li v-for="user in users" v-bind:key="user.id">{{ user.name }}</li>
  </ul>
</div>

以下は、ボタンをクリックすると name プロパティの値でソートする例です。

key 属性を指定しているので、ボタンをクリックすると、ソートされる li 要素のみが更新され、その他の li の状態は維持されます。key 属性を指定しない場合は、全ての li 要素が再生成されます。

HTML
<div id="app">
  <input type="button" value="Sort" @click="onclick">
  <ul>
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
  </ul>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      users: [
        {id: '001', name: 'Foo', age: 22},
        {id: '002', name: 'Bar', age: 30},
        {id: '003', name: 'Baz', age: 56},
      ]
    }
  },
  methods: {
    onclick() {
      this.users = this.users.sort((a,b) => {
        if(a.name === b.name){
          return 0;
        }
        if(typeof a.name === typeof b.name){
          return a.name < b.name ? -1 : 1;
        }
        return typeof a.name < typeof b.name ? -1 : 1;
      })
    }
  }
}).mount('#app');

key 属性の値

key 属性の値には一意の値(文字列や数値)を指定する必要があります。オブジェクトや配列のような非プリミティブ値を v-for のキーとして使うことはできません。

また、配列のインデックスは要素の追加や削除、ソートで変化するため使用することはできません。

v-once

v-once を使用することで、データ変更時の更新はおこなわず、要素やコンポーネントを一度だけ展開(レンダリング)することができます。

コンテンツが初期値から変更されないことがわかっている場合、v-once を使用することでそれ以降の再レンダリングでは、要素やコンポーネントとそのすべての子は、静的コンテンツとして扱われて省略されるので、更新パフォーマンスを最適化できます。

以下の場合、v-once を指定した p 要素の {{ text }} は「テキスト」のまま更新されませんが、v-once を指定していない p 要素の {{ text }} はテキストボックスに入力された値で更新されます。

HTML
<div id="app">
  <input type="text" v-model="text">
  <p v-once>{{ text }}</p>
  <p>{{ text }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      text: 'テキスト'
    }
  }
}).mount('#app');

v-cloak

v-cloak を利用することで Mustache 構文 {{ }} がページを起動したときに一瞬だけ表示されてしまう問題を解消することができます。

v-cloak を利用する場合、CSS で v-cloak 属性付きの要素を display: none で非表示にします。

CSS
/* v-cloak 属性の指定されている要素を非表示に */
[v-cloak] {
  display: none;
}
HTML
<div id="app">
  <p v-cloak>{{ message }}</p><!-- v-cloak 属性を指定 -->
  <p v-text="message"></p><!-- v-text を使えばチラツカない -->
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      message: 'Hello Vue!'
    }
  }
}).mount('#app');

Vue はコンパイルが完了したタイミングで v-cloak 属性があると、それを破棄します。そのため、display: none の適用が外れるのでコンパイル前には非表示だった要素が表示状態になります。

また、Mustache 構文 の代わりに v-text を使えばチラツキは発生しません。

コンポーネントの基本

以下は「Hello, Vue!」と表示するコンポーネントの例です。(コンポーネントの基本

//Vue アプリケーションを作成
const app = Vue.createApp({});

//グローバルな hello-div というコンポーネントをアプリケーションに登録(定義)
app.component('hello-div', {
  data() {
    return {
      name: 'Vue'
    };
  },
  //文字列テンプレート(バッククォートで文字列を囲むテンプレートリテラルで記述)
  template: `<div> Hello, {{name}}!</div>`,
});

//アプリケーションを DOM 要素にマウント
app.mount('#app');

アプリケーションの component メソッドを利用して、アプリケーションにコンポーネントをグローバルに登録(定義)しています。

component メソッド

以下は component メソッドの構文です。app はアプリのインスタンス(createApp() の返り値)です。

app.component(name, definition)
引数
  • name :コンポーネントの名前
  • definition :コンポーネントの定義(オプション)
返り値

definition 引数(コンポーネントの定義)が渡されている場合、アプリケーションのインスタンスが返されます。

コンポーネント名

コンポーネントに付ける名前(app.component の第一引数)は、DOM を直接操作する場合 (文字列テンプレートや 単一ファイルコンポーネントを除く) は、以下のような W3C rules に従ったカスタムタグ名が推奨されています。

  • 全て小文字
  • ハイフンを含める (複数の単語をハイフンを用いて繋げる)

コンポーネント名

コンポーネントを文字列テンプレートか単一ファイルコンポーネントで定義する際は、名前の付け方に以下の 2 つのオプションがあります (Name Casing)。

  • ケバブケース
    app.component('my-component-name', {
      /* ... */
    })
    ケバブケースで定義する場合は、そのカスタム要素を参照する際も <my-component-name> のようにケバブケースを用いなければなりません。
  • パスカルケース
    app.component('MyComponentName', {
      /* ... */
    })

    パスカルケースで定義する場合は、そのカスタム要素を参照する際どちらのケースも用いることができます。<my-component-name> と <MyComponentName> のどちらも利用可能です。

    但し、DOM 内で直接使用する場合 (つまり、文字列テンプレート以外の場合) はケバブケースの方が適しています。

スタイルガイド:テンプレート内でのコンポーネント名の形式

コンポーネントの定義

コンポーネントの定義(app.component の第二引数)は、「オプション名 : 値」形式のオブジェクトとして指定します。利用できるオプションは、data や computed、methods、watch など createApp メソッドと同じです。テンプレート(コンポーネントのマークアップ)は template オプションに記述します。

template オプション

template オプションはコンポーネントインスタンスのマークアップとして使われる文字列のテンプレート(HTML 文字列)で、マウントされた要素の innerHTML を置換します。

template オプションにはコンポーネントによって描画されるテンプレート(HTML)を指定します。バッククォートで HTML 文字列を囲むテンプレートリテラルを使えば改行を含め複数行の記述が可能です。

テンプレート内では、Mustache タグ {{ }} やディレクティブを利用することができます。

また、Vue 2x ではルート要素は単一にする必要がありましたが、Vue 3x ではその制限は解消され、複数要素を許容します。

以下は Vue 3x では有効ですが、Vue 2x では全体を <div> で束ねる必要がありました。

template: `
  <div> Hello, {{name}}!</div>
  <div> Goodbye, {{name}}!</div>
`,

関連項目:x-template

コンポーネントの呼び出し

定義したコンポーネントは、mount メソッドで指定した要素の中(ルートインスタンス)でカスタム要素(コンポーネント名を使ったタグ)として使用することができます。

HTML
<div id="app">
  <hello-div></hello-div><!-- カスタム要素として呼び出し -->
</div>
JavaScript
//コンポーネントを定義してマウント(createApp、component、mount をチェーン)
Vue.createApp({})
  .component('hello-div', {
    data() {
      return {
        name: 'Vue'
      };
    },
    //文字列テンプレート(template オプション)
    template: `<div> Hello, {{name}}!</div>`,
}).mount('#app');

デベロッパーツールで確認すると、以下のように出力されます。カスタム要素 <hello-div></hello-div> の部分が、template オプションの内容(テンプレート)に置き換わっているのが確認できます。

<div id="app" data-v-app="">
  <div> Hello, Vue!</div>
</div>

閉じタグ(終了タグ)の省略

コンポーネントの呼び出しでは、コンテンツがない場合、閉じタグ(終了タグ)を省略して(終わりにスラッシュを入れて)以下のように記述することもできます。

<div id="app">
  <hello-div /><!-- カスタム要素として呼び出し -->
</div>

但し、CDN 経由で Vue を実行している場合に閉じタグを省略すると、以下のようにルートコンポーネントで要素を追加しても、追加した要素が出力されません。SFC(単一ファイルコンポーネント)などコンパイルしている場合は問題なく出力されます(バグ?)。

CDN 経由で Vue を実行している場合
<div id="app">
  <hello-div /><!-- 閉じタグを省略すると -->
  <p>Root Component</p><!-- この要素が出力されない -->
</div>

以下のように閉じタグを記述すると、以降の要素も出力されます。

<div id="app">
  <hello-div></hello-div><!-- 閉じタグを記述 -->
  <p>Root Component</p><!-- この要素は出力される -->
</div>

DOM テンプレートパース時の注意

HTML の <div id="app">〜</div> に記述しているルートコンポーネントのテンプレートなどの部分は、まずブラウザによって HTML として解析されるので、HTML の制約を受けます。

そのため、テンプレートを DOM に直接書いている場合、以下のような点に注意する必要があります。

大文字・小文字が区別されない(無視される)

HTML の属性名は大文字小文字を区別しないので、ブラウザは全ての大文字を小文字として解釈します。つまり、DOM 内テンプレートを使用している場合、キャメルケースのプロパティ名やイベントハンドラのパラメータはそれと同等のケバブケース(ハイフンで区切られた記法)を使用する必要があります。

要素の配置制限(階層関係の制限)

<ul>、<ol>、<table>、<select> のようないくつかの HTML 要素にはその内側でどの要素が現れるかに制限があり、<li>、<tr>、<option> のようないくつかの属性は特定の要素の中にしか現れません。

例えば、以下の blog-post-row は tr 要素を出力することを想定したコンポーネントです。

JavaScript
Vue.createApp({})
  .component('blog-post-row', {
    data() {
      return {
        name: 'Vue'
      };
    },
    template: `
      <tr>
        <td>name</td>
        <td>{{name}}</td>
      </tr>
    `,
}).mount('#app');

以下の場合、HTML では table 要素の配下に <blog-post-row> 要素を許可しないので、無効なコンテンツとして巻き取られてしまいます。

HTML
<div id="app">
  <table>
    <blog-post-row></blog-post-row>
  </table>
</div>

以下のような出力になってしまいます。

<div id="app" data-v-app="">
  <tr>
    <td>name</td>
    <td>Vue</td>
  </tr>
  <table></table>
</div>

回避策として特別な is 属性 を使います。

<div id="app">
  <table>
    <tr is="vue:blog-post-row"></tr>
  </table>
</div>

上記の場合は、以下のように出力されます。

<div id="app" data-v-app="">
  <table>
    <tbody>
      <tr>
        <td>name</td>
        <td>Vue</td>
      </tr>
    </tbody>
  </table>
</div>

ネイティブの HTML 要素で使われるとき、Vue コンポーネントとして解釈されるためには is値の前vue: を付ける必要があります(is 属性は Web Components のカスタマイズされた組み込み要素で使用します)。

グローバル登録

app.component を使ったコンポーネントの定義はアプリケーションへのグローバル登録になり、あらゆるコンポーネントインスタンス(マウントされた要素の配下全て)のテンプレート内で使用できます。

JavaScript
const app = Vue.createApp({});

//グローバル登録
app.component('hello-vue', {
  data() {
    return {
      name: 'Vue'
    };
  },
  template: `<div> Hello, {{name}}!</div>`,
});

//グローバル登録
app.component('hello-world', {
  data() {
    return {
      name: 'World'
    };
  },
  template: `<div> Hello, {{name}}!</div>`,
});

app.mount('#app');
HTML
<div id="app">
  <hello-vue></hello-vue>
  <hello-world></hello-world>
</div>

但し、Webpack のようなビルドシステムを利用した場合、全てのコンポーネントをグローバル登録すると、使用していないコンポーネントも最終ビルドに含まれてしまうため、 JavaScript の量を不必要に増やしてしまいます。

ローカル登録

JavaScript としてコンポーネントを定義して、components オプション内に使用したいコンポーネントを定義することで、特定のコンポーネント配下でのみコンポーネントを有効にすることができ、これをローカル登録と呼びます。

JavaScript
//JavaScript としてコンポーネントを定義
const HelloVue = {
  data() {
    return {
      name: 'Vue'
    };
  },
  template: `<div> Hello, {{name}}!</div>`,
}

//JavaScript としてコンポーネントを定義
const HelloWorld = {
  data() {
    return {
      name: 'World'
    };
  },
  template: `<div> Hello, {{name}}!</div>`,
}

const app = Vue.createApp({
  //components オプションに使用したいコンポーネントを定義
  components: {
    'hello-vue' : HelloVue,
    'hello-world' : HelloWorld,
  }
});

app.mount('#app');
HTML
<div id="app">
  <hello-vue></hello-vue>
  <hello-world></hello-world>
</div>

上記の JavaScript は以下のように記述しても同じです。

JavaScript
const app = Vue.createApp({
  //components オプションに使用したいコンポーネントを定義
  components: {
    'hello-vue': {
      data() {
        return {
          name: 'Vue'
        };
      },
      template: `<div> Hello, {{name}}!</div>`,
    },
    'hello-world': {
      data() {
        return {
          name: 'World'
        };
      },
      template: `<div> Hello, {{name}}!</div>`,
    },
  }
});

app.mount('#app');

ローカル登録されたコンポーネントはサブコンポーネントでは利用できない

例えば、ComponentA を ComponentB 内で使用可能にしたいときは、以下のように使用する必要があります。

JavaScript
const ComponentA = {
  /* ... */
}

const ComponentB = {
  components: {
    'component-a': ComponentA
  }
  // ...
}

ローカルコンポーネントの有効範囲は、定義されたコンポーネントの直下だけなので、以下のコードは期待通りに動作しません([Vue warn]: Failed to resolve component: hello-vue というような警告が出ます)。

JavaScript
const HelloVue = {
  template: `<div> Hello, Vue!</div>`,
}

const HelloWorld = {
  template: `<div> Hello, World !</div>
  <hello-vue></hello-vue>`,
}

const app = Vue.createApp({
  components: {
    'hello-vue' : HelloVue,
    'hello-world' : HelloWorld,
  }
});

app.mount('#app');
HTML
<div id="app">
  <hello-world></hello-world>
</div>

以下のように、配下で使用するコンポーネントを登録することで動作します。

const HelloVue = {
  template: `<div> Hello, Vue!</div>`,
}

const HelloWorld = {
  //HelloVue をコンポーネントとして登録
  components: {
    'hello-vue' : HelloVue,
  },
  template: `<div> Hello, World !</div>
  <hello-vue></hello-vue>`,
}

const app = Vue.createApp({
  components: {
    'hello-world' : HelloWorld,
  }
});

app.mount('#app');

props オプション

props オプションはコンポーネントに登録できるカスタム属性で、値が渡されるとそのコンポーネントインスタンスのプロパティになります。

この props オプションを使って親コンポーネントから子コンポーネントへデータを渡すことができます。

プロパティを用いた子コンポーネントへのデータの受け渡し

データを子コンポーネントに渡すには、props オプションを使ってコンポーネントが受け取るプロパティのリストを登録します。

//子コンポーネントで props オプションを指定
props: ['プロパティ'] 

コンポーネントは必要に応じて複数のプロパティを持つことができ、配列やオブジェクトとして定義することができます(プロパティの型 参照)。

//必要に応じて複数のプロパティを指定可能
props: ['プロパティ1', 'プロパティ2', ...] 

プロパティ名が複数の単語で構成される場合は、キャメルケース(例 postTitle)で指定し、属性名はケバブケース(例 post-title)で記述するのが基本です。

以下では title という1つのプロパティを登録し、その値をテンプレートに出力しています。

JavaScript
const app = Vue.createApp({});

app.component('blog-post', {
  //props オプションに title というプロパティを登録
  props: ['title'],
  template: `<h4>{{ title }}</h4>`
});

app.mount('#app');

以下は親コンポーネント側(ルートコンポーネント)で、子コンポーネントで定義した title プロパテに対応するカスタム属性(title 属性)に値を指定しています。

※カスタム属性に指定された値は文字列と見なされます(文字列以外を渡す場合は v-bind を使用)。

HTML
<div id="app">
  <blog-post title="サンプルタイトル"></blog-post>
</div>

親コンポーネント側で title 属性に指定された値の文字列「サンプルタイトル」が、子コンポーネントの title プロパティに渡され、以下のように出力されます。

<div id="app" data-v-app="">
  <h4>サンプルタイトル</h4>
</div>

プロパティの値は、他のコンポーネントのプロパティと同様、テンプレート内とコンポーネントの this コンテキストでアクセス可能です。

JavaScript
Vue.createApp({})
.component('blog-post', {
  //プロパティ名が複数の単語で構成される場合、プロパティはキャメルケース
  props: ['postTitle'],
  template: `<h4>{{ blogTitle }}</h4>`,
  computed: {
    blogTitle() {
      //プロパティに this でアクセス可能
      return 'ブログ:' + this.postTitle
    }
  },
}).mount('#app');
HTML
<div id="app">
  <!-- プロパティ名が複数の単語で構成される場合、カスタム属性名はケバブケース -->
  <blog-post post-title="サンプルタイトル"></blog-post>
</div>

以下のように出力されます。

<div id="app" data-v-app="">
  <h4>ブログ:サンプルタイトル</h4>
</div>

以下はルートコンポーネントの data プロパティに投稿(posts)の配列を持っている場合の例です。v-forv-bind を使って動的にプロパティをカスタム属性に渡しています。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue', author: 'Foo' },
        { id: 2, title: 'Blogging with Vue', author: 'Bar' },
        { id: 3, title: 'Why Vue is so fun', author: 'Baz' }
      ]
    }
  }
})

app.component('blog-post', {
  props: ['title', 'author'],
  template: `<h4>{{ title }}</h4>
  <p>by {{ author }}</p>`
});

app.mount('#app');

属性に文字列以外の値(JavaScript の式)を渡す場合は、v-bind を使用します。

HTML
<div id="app">
  <blog-post
    v-for="post in posts"
    v-bind:key="post.id"
    v-bind:title="post.title"
    v-bind:author="post.author"
  ></blog-post>
</div>

以下のように出力されます。

<div id="app" data-v-app="">
  <h4>My journey with Vue</h4>
  <p>by Foo</p>
  <h4>Blogging with Vue</h4>
  <p>by Bar</p>
  <h4>Why Vue is so fun</h4>
  <p>by Baz</p>
</div>
単方向データフロー

すべてのプロパティは、子プロパティと親プロパティの間に 単方向のバインディング を形成します: 親のプロパティが更新される場合は子へと流れ落ちますが、その逆はありません。これにより、子コンポーネントが誤って親の状態を変更することを防ぎます(単方向データフロー)。

コンポーネント内でプロパティの値を変更してはいけない

親コンポーネントが更新されるたびに、子コンポーネントのすべてのプロパティは最新の値に更新されます。そのため、子コンポーネント内でプロパティの値を変化させてはいけません。変化させた場合、Vue はコンソールで警告を表示します。

プロパティの値を操作したい場合は、算出プロパティを利用したり、プロパティの値を使うローカルの data プロパティを定義します。

例えば、以下のようにプロパティ initialCount の値を操作しようとすると、「[Vue warn]: Attempting to mutate prop "initialCount". Props are readonly. 」のような警告が表示され、値は変更されません。

HTML
<div id="app">
  <counter-div initial-count="0"></counter-div>
</div>
JavaScript
const app = Vue.createApp({});

app.component('counter-div', {
  props: ['initialCount'],
  template: `<div>Count: {{ initialCount }}
  <input type="button" v-on:click="onclick" value="Increase">
  </div>`,
  methods: {
    onclick() {
      //プロパティの値を変更しようとすると警告が出て、変更できない
      this.initialCount++;
    }
  }
});

app.mount('#app');

この場合、プロパティ initialCount の値を初期値として使うローカルの data プロパティ count を定義して、count を操作します。

JavaScript
const app = Vue.createApp({});

app.component('counter-div', {
  props: ['initialCount'],
  template: `<div>Count: {{ count }}
  <input type="button" v-on:click="onclick" value="Increase">
  </div>`,
  data() {
    return {
      //プロパティの値を初期値として使う data プロパティを定義
      count: this.initialCount
    }
  },
  methods: {
    onclick() {
      // data プロパティを操作
      this.count++;
    }
  }
});

app.mount('#app');
プロパティの型

プロパティを特定の型の値にしたい場合は、プロパティをオブジェクトとして列挙し、プロパティのキーにプロパティの名前を、値にその型を設定します。

props: {
  //プロパティ名: 型
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise
}

このように設定すると、間違った型を渡した場合に、ブラウザの JavaScript コンソールで警告が表示されます。

但し、渡された値の型を変更するわけではないので、文字列以外を指定する場合は v-bind: を使用します。

属性値は文字列として扱われる

プロパティのカスタム属性に指定された値は文字列と見なされます。数値や真偽値、配列、オブジェクトなど(JavaScript 式)として値を渡したい場合は、v-bind: を使用します。

静的あるいは動的なプロパティの受け渡し

以下はカスタム属性(my-value)に v-bind: を使用しているので、値(0)は文字列ではなく JavaScript の式(数値)として扱われます。

<div id="app">
  <sample-div v-bind:my-value="0"></sample-div>
</div>
const app = Vue.createApp({});

app.component('sample-div', {
  props: ['myValue'],
  template: `<p>{{ myValue }}</p>
  <button v-on:click="onclick">Check</button>`,
  methods: {
    onclick() {
      console.log(typeof this.myValue); //number
    }
  }
});

app.mount('#app');

プロパティの型に Boolean を設定してある場合、値のないプロパティ(カスタム属性)は true を意味します。以下の場合、my-value は true になります。

<div id="app">
  <sample-div my-value></sample-div>
</div>
const app = Vue.createApp({});

app.component('sample-div', {
  props: {
    //型に Boolean を設定
    myValue : Boolean
  },
  template: `<p>{{ myValue }}</p>
  <button v-on:click="onclick">Check</button>`,
  methods: {
    onclick() {
      console.log(this.myValue);  //true
      console.log(typeof this.myValue);  //boolean
    }
  }
});

app.mount('#app');

但し、false の場合、文字列ではなく JavaScript の式(真偽値)であると伝えるには v-bind: を使う必要があります。単に my-value="false" とすると、文字列の "false" として扱われます。

<div id="app">
  <sample-div v-bind:my-value="false"></sample-div>
</div>
プロパティのバリデーション

プロパティのバリデーション(検証)を指定するには、文字列の配列の代わりに、 props の値についてのバリデーション要件をもったオブジェクト(ルール名:値)を渡します(プロパティのバリデーション)。

ルール名には以下を指定することができます。

ルール名 概要
type データ型(String、Number、Boolean、Array、Object、Date、Function、Symbol)
required プロパティが必須かどうか(true または false)
default その属性が省略された場合のデフォルト値(既定値)
validator 独自の検証関数

プロパティのバリデーションが失敗した場合、 Vue はコンソールに警告を表示します (開発用ビルドを利用している場合)。

以下はプロパティのバリデーションを指定する例です。

app.component('my-component', {
  props: {
    // 基本的な型チェック (数値型)
    propA: Number,
    // 複数の型の許容
    propB: [String, Number],
    // 文字列型を必須で要求する
    propC: {
      type: String,
      required: true
    },
    // デフォルト値つきの数値型
    propD: {
      type: Number,
      default: 100
    },
    // デフォルト値つきのオブジェクト型
    propE: {
      type: Object,
      // オブジェクトもしくは配列のデフォルト値は必ずファクトリ関数(既定値を返す関数)を返します。
      default() {
        return { message: 'hello' }
      }
    },
    // カスタムバリデーション関数
    propF: {
      validator(value) {
        // 例:プロパティの値は、必ずいずれかの文字列でなければならない
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    // デフォルト値つきの関数型
    propG: {
      type: Function,
      // オブジェクトや配列のデフォルトとは異なり、これはファクトリ関数ではありません。これは、デフォルト値としての関数を取り扱います。
      default() {
        return 'Default function'
      }
    }
  }
})

default 既定値の指定

以下は数値型の既定値を指定する例です。

JavaScript
const app = Vue.createApp({});
app.component('sample-div', {
  props: {
    myValue : {
      type: Number,
      default: 123
    }
  },
  template: `<p>{{ myValue }}</p>`,
});
app.mount('#app');

以下のようにプロパティのカスタム属性を省略した場合、既定値(123)が適用されます。

HTML
<div id="app">
  <sample-div></sample-div>
</div>
出力
<div id="app" data-v-app="">
  <p>123</p>
</div>

但し、カスタム属性の値を空にすると「[Vue warn]: Invalid prop: type check failed for prop "myValue". Expected Number with value 0, got String with value "". 」のような警告が表示され、既定値が適用されません。

また、v-bind: を指定して「v-bind:my-value=""」のようにすると、更に「[Vue warn]: Template compilation error: v-bind is missing expression.」のような警告が表示されます。

<div id="app">
  <sample-div my-value=""></sample-div><!-- 値を省略するとエラーになる -->
</div>

以下は、オブジェクト型のプロパティの例です。

HTML
<div id="app">
  <sample-div :my-value="{value: 'sample value'}"></sample-div>
</div>

既定値が配列やオブジェクトの場合は、値ではなく、既定値を返す関数を渡します。

JavaScript
const app = Vue.createApp({});
app.component('sample-div', {
  props: {
    myValue : {
      type: Object,
      default() {
        return { value: 'Default Value' }
      }
    }
  },
  template: `<p>{{ myValue.value }}</p>`,
});
app.mount('#app');

default() の引数

default() は引数として props を受け取ることができます。以下は myValue プロパティが、別に定義された foo プロパティの値を既定値とする例です。

JavaScript
const app = Vue.createApp({});
app.component('sample-div', {
  props: {
    foo: {
      type: String,
      default: 'Foo'
    },
    myValue : {
      type: String,
      default(props) {
        return props.foo
      }
    }
  },
  template: `<p>{{ myValue }}</p>`,
});
app.mount('#app');

以下の場合、foo 属性も省略されているので、myValue には foo プロパティの既定値(Foo)が適用されます。

HTML
<div id="app">
  <sample-div></sample-div>
</div>

以下の場合は、foo 属性に渡された値(FooFoo)が myValue に適用されます。

HTML
<div id="app">
  <sample-div foo="FooFoo"></sample-div>
</div>

カスタムバリデーション関数

validator() を使って、独自の検証ルールを指定することができます。validator() は引数にプロパティの値を受け取り、返り値として検証結果の真偽値(true または false)を返すように定義します。

以下は myValue プロパティの値が10文字未満かを検証する例です。

JavaScript
const app = Vue.createApp({});
app.component('sample-div', {
  props: {
    myValue : {
      type: String,
      validator(val) {
        return val.length < 10
      }
    }
  },
  template: `<p>{{ myValue }}</p>`,
});
app.mount('#app');
              

以下のように my-value 属性に10文字以上の文字列を指定すると「 [Vue warn]: Invalid prop: custom validator check failed for prop "myValue".」のような警告がコンソールに表示されます(値は出力されます)。

HTML
<div id="app">
  <sample-div my-value="abcdefghijkl"></sample-div>
</div>

props でない属性

コンポーネントには props や emits オプションで定義されていない任意の属性(プロパティでない属性)を渡すこともできます。

テンプレートが一つのルート要素をもつコンポーネントの場合、プロパティでない属性はルート要素にそのまま追加(継承)されます。

プロパティでない属性

※ 基本的には、コンポーネントに渡す値は props や emits オプションで定義したほうがコンポーネントの仕様が明確になります。

以下はプロパティでない(props オプションで定義されていない)属性がある場合の例です。

HTML
<div id="app">
  <hello-div name="foo" class="mt-3" my-attr="123"></hello-div>
</div>
JavaScript
const app = Vue.createApp({});

app.component('hello-div', {
  template: `
  <div name="bar" title="hello" class="wrapper">
    <p class="message">Hello, Vue</p>
  </div>
`
})
.mount('#app');

props オプションで定義されていない属性は、テンプレートのルート要素(この例の場合は div 要素)に付与され、以下のように出力されます。

出力
<div id="app" data-v-app="">
  <div name="foo" title="hello" class="wrapper mt-3" my-attr="123">
    <p class="message">Hello, Vue</p>
  </div>
</div>

属性が重複している場合

属性が重複している場合は、呼び出し側の値で上書きされます。上記の例の場合、name 属性が重複しているので、呼び出し側の値 foo になっています。

class 属性や style 属性は既存の値とマージされます。

※ 但し、プロパティでない属性はルート要素に追加されます(且つ、テンプレートが一つのルート要素をもつコンポーネントの場合)

以下はコンポーネントのテンプレートで button 要素の type 属性の値に button を指定しています。

JavaScript
const app = Vue.createApp({});
app.component('my-button', {
  props: {
    label: {
      String,
      default: 'Click'
    }
  },
  template: `
    <button type="button"><!-- type 属性を指定 -->
      {{ label }}
    </button>
  `
}).mount('#app');

呼び出し側で type 属性を指定しなければ、type="button" として出力されます。

HTML
<div id="app">
  <my-button></my-button>
</div>

以下が出力です(ボタンのラベルは props を使用しています)。

<div id="app" data-v-app="">
  <button type="button">Click</button>
</div>

呼び出し側で type 属性を指定すれば、テンプレートの button 要素はルート要素なので、上書きされて指定された値が反映されます。

HTML
<div id="app">
  <my-button type="submit" label="Submit"></my-button>
</div>
出力
<div id="app" data-v-app="">
  <button type="submit">Submit</button>
</div>

その要素がルート要素の場合は、上記のように単純に呼び出し側で属性を上書きすることができますが、以下のようにルート要素出ない場合は、期待通りになりません。

JavaScript
const app = Vue.createApp({});
app.component('my-button', {
  props: {
    label: {
      String,
      default: 'Click'
    }
  },
  template: `
    <div> <!-- ルート要素 -->
      <button type="button">
        {{ label }}
      </button>
    </div>
  `
}).mount('#app');
HTML
<div id="app">
  <my-button type="submit" label="Submit"></my-button>
</div>

以下のように my-button に指定した type="submit" はルート要素の div 要素に追加されます。

<div id="app" data-v-app="">
  <div type="submit">
    <button type="button">Submit</button>
  </div>
</div>

この場合は、以下のように明示的に props を使って属性を指定することができます。

JavaScript
const app = Vue.createApp({});
app.component('my-button', {
  props: {
    //type 属性の値のプロパティ
    buttonType: {
      String,
      default: 'button'
    },
    label: {
      String,
      default: 'Click'
    }
  },
  template: `
    <div>
      <button :type=buttonType>
        {{ label }}
      </button>
    </div>
  `
}).mount('#app');
HTML
<div id="app">
  <my-button button-type="submit" label="Submit"></my-button>
</div>

以下のように出力されます。

<div id="app" data-v-app="">
  <div>
    <button type="submit">Submit</button>
  </div>
</div>
inheritAttrs

コンポーネントのオプション内で、inheritAttrs オプションを false に設定することで、属性の継承を無効化する(ルート要素に反映させない)ことができます。

以下は前述の hello-div コンポーネントに inheritAttrs: false を指定した例です。

app.component('hello-div', {
  inheritAttrs: false, //属性の継承を無効化
  template: `
  <div name="bar" title="hello" class="wrapper">
    <p class="message">Hello, Vue</p>
  </div>
  `
})
HTML
<div id="app">
  <hello-div name="foo" class="mt-3" my-attr="123"></hello-div>
</div>

出力は以下のようになります。コンポーネントを使う側(親)で指定したプロパティでない属性は継承されていません。

<div id="app" data-v-app="">
  <div name="bar" title="hello" class="wrapper">
    <p class="message">Hello, Vue</p>
  </div>
</div>
$attrs

$attrs プロパティには、コンポーネントの props や emits オプションで宣言されていないすべての属性 (例えば class や style、v-on など) が含まれます。

プロパティでない属性はテンプレートで $attrs としてアクセスすることができます。

ルート要素以外の要素にプロパティでない属性を適用

ルート要素以外の要素にプロパティでない属性を適用する必要がある場合は、inheritAttrs オプションを false に設定し、v-bind を使って $attrs をルートでない要素に明示的にバインドすることができます。

以下の場合、プロパティでない属性はルート要素に追加されてしまいます。

HTML
<div id="app">
  <my-button class="info" type="button"></my-button>
</div>
JavaScript
const app = Vue.createApp({});

app.component('my-button', {
  template: `
  <div class="wrapper">
    <button class="btn">Click !</button>
  </div>
  `
});
app.mount('#app');
出力
<div id="app" data-v-app="">
  <div class="wrapper info" type="button"><!-- ルート要素に追加される -->
    <button class="btn">Click !</button>
  </div>
</div>

以下のように、inheritAttrs オプションを false に設定し、v-bind を使って $attrs を button 要素にバインドすることができます。

v-bind="$attrs" は $attrs オブジェクトのすべてのプロパティをターゲット要素の属性としてバインドします(オブジェクトで複数の属性をまとめてバインド)。

JavaScript
const app = Vue.createApp({});
app.component('my-button', {
  inheritAttrs: false, //属性の継承を無効化
  template: `
  <div class="wrapper">
    <!-- v-bind を使って $attrs をルートでない要素にバインド -->
    <button class="btn" v-bind="$attrs">Click !</button>
  </div>
  `
});
app.mount('#app');

上記の場合、以下のような出力になります。

出力
<div id="app" data-v-app="">
  <div class="wrapper">
    <!-- button 要素に属性が追加される -->
    <button class="btn info" type="button">Click !</button>
  </div>
</div>

特定の属性のみを継承

特定の属性のみを継承するには、inheritAttrs: false を指定して、特定の属性に以下のように指定することができます(ルート要素に対しても同様)。

v-bind:属性="$attrs.属性"

例えば、前述の例で type 属性のみを継承するには、v-bind:type="$attrs.type" とします。

JavaScript
const app = Vue.createApp({});
app.component('my-button', {
  inheritAttrs: false, //属性の継承を無効化
  template: `
  <div class="wrapper">
    <!-- v-bind を使って type 属性に $attrs の type プロパティをバインド -->
    <button class="btn" v-bind:type="$attrs.type">Click !</button>
  </div>
  `
});
app.mount('#app');
HTML
<div id="app">
  <my-button class="info" type="button"></my-button>
</div>

以下のように type 属性のみを継承することができます(この場合、class 属性は継承されません)。

<div id="app" data-v-app="">
  <div class="wrapper">
    <button class="btn" type="button">Click !</button>
  </div>
</div>

v-on の例

$attrs プロパティには v-on も含まれているので、前述の例の my-button コンポーネントの場合、my-button のルート要素で click イベントを扱うこともできます。

以下の場合、click イベントリスナは親(ルート要素)から子(my-button)へ渡され、button 要素の click イベントにより発火されます。

HTML
<div id="app">
  <my-button class="info" v-on:click="dateTime"></my-button>
  {{ now }}
</div>

my-button のルート要素で指定した v-on 属性は v-bind="$attrs" により、button 要素に追加されます。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      now: ''
    }
  },
  methods: {
    //click イベントのリスナー
    dateTime() {
      this.now = new Date().toLocaleString();
    }
  }
});

//button 要素に v-bind="$attrs" を指定(前述の例と同じコード)
app.component('my-button', {
  inheritAttrs: false, //属性の継承を無効化
  template: `
  <div class="wrapper">
    <button class="btn" v-bind="$attrs">Click !</button>
  </div>
  `
});

app.mount('#app');

※ 但し、上記の方法は例外的なもので、通常は以下のようにカスタムイベント($emit)を使います。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      now: ''
    }
  },
  methods: {
    //カスタムイベント btn-click のリスナー
    dateTime() {
      this.now = new Date().toLocaleString();
    }
  }
});
app.component('my-button', {
  //emits オプションに発行するカスタムイベントを宣言
  emits: [ 'btnClick'],
  template: `
  <div class="wrapper">
    <button type="button" v-on:click="$emit('btnClick')" v-bind="$attrs">
    Click !
    </button>
  </div>
  `
});
app.mount('#app');
HTML
<div id="app">
  <my-button class="info" v-on:btn-click="dateTime"></my-button>
  {{ now }}
</div>
ルート要素が複数の場合の属性の継承

コンポーネントのルート要素が 1 つでなく複数のルート要素からなる場合には、自動的な属性の継承は行われず、$attrs を使って明示的にバインドを行わない場合、warning が発行されます。

以下の場合、「[Vue warn]: Extraneous non-props attributes (class) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. 」のような警告が発行されます。

HTML
<div id="app">
  <my-divs class="post"></my-divs><!-- props でない属性を指定 -->
</div>

どこに属性を適用すればよいか分からないため警告が発行されます。

const app = Vue.createApp({});
app.component('my-divs', {
  //複数のルート要素
  template: `
    <div class="foo">Foo</div>
    <div class="bar">Bar</div>
    <div class="baz">Baz</div>
  `
});
app.mount('#app');

$attrs が明示的にバインドされている場合は警告は抑制されます。以下では1つの要素に v-bind="$attrs" を指定していますが、複数の要素に指定することもできます。

const app = Vue.createApp({});
app.component('my-divs', {
  template: `
    <div class="foo">Foo</div>
    <div class="bar" v-bind="$attrs">Bar</div> <!-- $attrs を明示的にバインド -->
    <div class="baz">Baz</div>
  `
});
app.mount('#app');

カスタムイベント $emit

カスタムイベントという仕組みを利用すると、子コンポーネントで何らかの処理を行った際に、親コンポーネントに独自のイベント(カスタムイベント)を通知してデータを渡すことができます。

子コンポーネントのイベントを購読する

カスタムイベントを利用するには、子コンポーネントで emits オプションに発行するイベント名のリストを登録します。イベント名はプロパティ名と同様、複数の単語で構成される場合は、キャメルケース(例 clickCount)で指定し、属性名はケバブケース(例 click-count)で記述するのが基本です。

emits: [ 'イベント名']  //複数指定する場合はカンマ区切りで指定

emits オプションは省略することもできますが、明示的に定義しておく方がコンポーネントの仕様を明確にしたり、ネイティブのイベントと区別することができます。

そしてイベントリスナーなどを使って何らかのタイミングで、$emit メソッドにイベントの名前を渡して呼び出すことでイベントを発行します(オプションで第2引数に値を渡すことができます)。

$emit(eventName, args)
  • eventName:イベント名(カスタムイベントの名前)
  • args:リスナーのコールバック関数に渡す値(親コンポーネントに渡すデータ)。オプション

親コンポーネントでは、通常の DOM イベントの場合同様、 v-on を使って子コンポーネントのインスタンスでのカスタムイベントを購読する(受け取る)ことができます(v-on)。

以下はイベント名のみで $emit を使う例です。

emits オプションに発行するイベント clickCount を宣言し、クリックイベントのリスナー(v-on:click)で $emit メソッドを使って、clickCount というイベントを発行しています(13行目)。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      count: 0
    };
  }
});

app.component('counter-button', {
  //emits オプションに発行するイベントを列挙
  emits: [ 'clickCount'],
  template: `
    <button type="button" v-on:click="$emit('clickCount')">
      count up
    </button>`
});

app.mount('#app');

親コンポーネントでは、カスタムイベントを v-on:click-count で購読し、イベントリスナ(count ++)を設定して、count の値を更新しています。

これにより、子コンポーネントでのイベントを親コンポーネントで受け取ることができます。

HTML
<div id="app">
  <p>Count : {{count}}</p>
  <counter-button v-on:click-count="count ++"></counter-button>
</div>

以下は上記のリスナを methods を使って書き換えたものです。 methods オプションでは、$emit() に this でアクセスします。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      count: 0
    };
  },
  methods: {
    onclickcount() {
      //clickCount 発生時にカウントを1増加
      this.count ++;
    },
  }
});

app.component('counter-button', {
  //emits オプションに発行するイベントを列挙
  emits: [ 'clickCount'],
  template: `
  <button type="button" v-on:click="onclick">
    count up
  </button>`,
  methods: {
    onclick() {
      //クリック時に clickCount イベントを発生
      this.$emit('clickCount');
    }
  }
});

app.mount('#app');
HTML
<div id="app">
  <p>Count : {{count}}</p>
  <counter-button v-on:click-count="onclickcount"></counter-button>
</div>

イベントと値を発行する

$emit の第二引数を使ってリスナーに値を渡すことができます(イベントと値を発行する)。

以下は前述の例のコンポーネントに amount というプロパティ(クリック時に増減する値)を追加して、その値を $emit の第二引数に指定して親コンポーネントのコールバック関数に渡しています。

イベントハンドラが methods の場合、値はそのメソッドの第一引数として渡されます(9行目)。

また、ボタンのテキストは amount の値を出力するようにしています。

const app = Vue.createApp({
  data() {
    return {
      count: 0
    };
  },
  methods: {
    //$emit の第二引数を、引数に受け取る(引数名は任意の文字列)
    onclickcount(value) {
      this.count += value;
    },
  }
});

app.component('counter-button', {
  //ボタンをクリックした際に増減する値を指定するプロパティ
  props: {
    amount: {
      type: Number, //数値型
      default: 1  //デフォルト値
    }
  },
  //emits オプションに発行するイベントを宣言
  emits: [ 'clickCount'],
  template: `
  <button type="button" v-on:click="onclick">
    {{ amount }}
  </button>`,
  methods: {
    onclick() {
      //第二引数に親コンポーネントのコールバック関数に渡す値(データ)を指定
      this.$emit('clickCount', this.amount);
    }
  }
});

app.mount('#app');

プロパティ amount は数値として扱いたいので、v-bind: を指定しています。

<div id="app">
  <p>Count : {{count}}</p>
  <counter-button v-on:click-count="onclickcount" v-bind:amount="1"></counter-button>
  <counter-button v-on:click-count="onclickcount" v-bind:amount="-1"></counter-button>
</div>

親コンポーネントでカスタムイベントを購読する際に、methods を使わず、イベントハンドラを直接記述する場合は、$event でイベントの値($emit の第二引数)にアクセスすることができます。

<div id="app">
  <p>Count : {{count}}</p>
  <counter-button v-on:click-count="count += $event" v-bind:amount="1"></counter-button>
  <counter-button v-on:click-count="count += $event" v-bind:amount="-1"></counter-button>
</div>
カスタムイベントの検証

プロパティのバリデーション(型検証)と同様、emits オプションを配列構文ではなくオブジェクト構文で定義することで発行されたイベントを検証することができます(発行されたイベントを検証する)。

emits オプションで「イベント名: 検証関数」形式のオブジェクトを渡すことで、カスタムイベントで引き渡す値が正しいかを検証します。

以下は、前述の clickcount() に渡す amount の値が存在し、且つ整数値であるかどうかを検証する例です。

検証関数は、$emit の第二引数(args)で渡された値を受け取り、イベントが有効かどうかを示す真偽値を返します(妥当である場合は true、そうでない場合は false を返します)。

妥当でない場合は理由をコンソールに警告(またはログ)として表示するとわかりやすいです。

app.component('counter-button', {
  props: {
    amount: {
      type: Number,
      default: 1
    }
  },
  //emits オプションでカスタムイベントを検証
  emits: {
    //イベント名: 検証関数
    clickCount: (amount) => {
      if(amount && Number.isInteger(amount)) {
        //値が存在し、且つ整数値である場合は有効(true を返す)
        return true;
      }else{
        //妥当でない場合は理由をコンソールに警告(またはログ)として表示して false を返す
        console.warn('clickCount argument must be integar');
        return false;
      }
    }
  },
  template: `
  <button type="button" v-on:click="onclick">
    {{ amount }}
  </button>`,
  methods: {
    onclick() {
      this.$emit('clickCount', this.amount);
    }
  }
});

emits オプションは、ES6 のメソッド定義の短縮構文を使って以下のように記述することもできます。

emits: {
  clickCount(amount) {
    if(amount && Number.isInteger(amount)) {
      //値が存在し、且つ整数値である場合は有効(true を返す)
      return true;
    }else{
      //妥当でない場合は理由をコンソールに警告(またはログ)として表示して false を返す
      console.warn('clickCount argument must be integar');
      return false;
    }
  }
}

コンポーネントで v-model を使う

コンポーネントで v-model を使う

以下はテキストボックス(input 要素)に入力された値を v-model を使用して表示する例です。

HTML
<div id="app">
<input v-model="myText">
<p>{{ myText }}</p>
</div>
JavaScript
Vue.createApp({
  data() {
    return {
      myText: ''
    }
  }
}).mount('#app');

上記の HTML は以下のように、value 属性に myText をバインドし、input イベントで入力された値($event.target.value)を myText にバインドして更新するのと同じことです。

v-model は内部的には value 属性(プロパティ)と input イベントを使用しています。

<div id="app">
<input v-bind:value="myText" v-on:input="myText = $event.target.value">
<p>{{ myText }}</p>
</div>

コンポーネントで v-model を使用する場合、標準的な input 要素などとは異なり、v-model はデフォルトでは modelValue プロパティ(属性)と update:modelValue イベントを使用します。

例えば、以下のコンポーネント custom-input で v-model を使用する場合、

<custom-input v-model="myText"></custom-input>

上記は以下と同じことです。

<custom-input
  v-bind:model-value="myText"
  v-on:update:model-value="newValue => myText = newValue"
></custom-input>

親コンポーネントでカスタムイベントを購読する際に、イベントハンドラを直接記述する場合は、$event でイベントの値にアクセスすることができるので、以下でも同じことです。

<custom-input
  v-bind:model-value="myText"
  v-on:update:model-value="myText = $event"
></custom-input>

コンポーネントで v-model を使用する方法

コンポーネントで v-model を使用するには、対応する props オプションと emits オプションを用意します(以下を実施します)。

  • props オプションに modelValue を登録
  • emits オプションに update:modelValue を定義
  • value 属性を modelValue プロパティにバインドする
  • input イベントでは、 update:modelValue イベントを新しい値と共に $emit で発行する

プロパティは文字列テンプレートではケバブケース(model-value)ですが JavaScript ではキャメルケース(modelValue)を使用します。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      myText: ''
    }
  }
});

app.component('custom-input', {
  // props に modelValue プロパティを登録(JavaScript ではキャメルケース)
  props: ['modelValue'],
  // emits オプションに発行するイベント update:modelValue を定義
  emits: ['update:modelValue'],
  // 文字列テンプレート
  // value 属性を modelValue プロパティにバインド
  // input イベントで update:modelValue イベントを新しい値と共に $emit で発行
  template: `
    <input
      v-bind:value="modelValue"
      v-on:input="$emit('update:modelValue', $event.target.value)"
    >
  `
})

app.mount('#app');
HTML
<div id="app">
  <custom-input v-model="myText"></custom-input>
  <p>{{ myText }}</p>
</div>

コンポーネント内で v-model を実装するもう 1 つの方法

コンポーネント内で v-model を実装するもう 1 つの方法は、算出プロパティ(computed)の機能を使ってゲッターとセッターを定義します。

以下の myValue はゲッターとセッターを定義した算出プロパティです。

get メソッド(ゲッター)では現在の modelValue プロパティを取得して返して、 set メソッド(セッター)では $emit を使って対応するイベント(update:modelValue)を発行し、第二引数を使って値を渡します。

そして、myValue をテンプレートで v-model としてバインドします。

const app = Vue.createApp({
  data() {
    return {
      myText: ''
    }
  }
});

app.component('custom-input', {
  // modelValue プロパティ
  props: ['modelValue'],
  // emits オプション
  emits: ['update:modelValue'],
  // テンプレートで算出プロパティを v-model としてバインド
  template: `<input v-model="myValue">`,
  // 算出プロパティを定義(modelValue プロパティを操作)
  computed: {
    myValue: {
      //ゲッター
      get() {
        return this.modelValue
      },
      //セッター
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
})

app.mount('#app');
HTML
<div id="app">
  <custom-input v-model="myText"></custom-input>
  <p>{{ myText }}</p>
</div>

v-model の引数

デフォルトでは、コンポーネントの v-model はプロパティとして modelValue を使用し、イベントとして update:modelValue を使用します。

v-model に引数を渡してこれらの名前(デフォルトの modelValue)を変更できます(v-model の引数)。

以下は v-model に引数として text を渡しています。

<div id="app">
<custom-input v-model:text="myText"></custom-input>
<p>{{ myText }}</p>
</div>

子コンポーネントでは props オプションに text を定義し、emits オプションでは update:text を定義し、$emit では update:text イベントを発行します。

const app = Vue.createApp({
data() {
  return {
    myText: ''
  }
}
});

app.component('custom-input', {
// props オプション
props: ['text'],
// emits オプション
emits: ['update:text'],
template: `
  <input
    v-bind:value="text"
    v-on:input="$emit('update:text', $event.target.value)"
  >
`
})

app.mount('#app');

複数の v-model のバインディング

v-model に引数を指定することで(前項)、modelValue 以外のプロパティ名を使って、単一のコンポーネントインスタンスに対して、複数の v-model バインディングを作成できます。

HTML
<div id="app">
<p>First name: {{ firstName }}</p>
<p>Last name: {{ lastName }}</p>
<user-name
  v-model:first-name="firstName"
  v-model:last-name="lastName"
></user-name>
</div>
JavaScript
const app = Vue.createApp({
data() {
  return {
    firstName: 'John',
    lastName: 'Doe',
  };
}
});

app.component('user-name', {
props: {
  firstName: String,
  lastName: String
},
emits: ['update:firstName', 'update:lastName'],
template: `
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)">

  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)">
`
})

app.mount('#app');
v-model カスタム修飾子

v-model ではバインド時の挙動を制御するための .trim や .lazy などの修飾子が用意されていますが、独自のコンポーネントでは、独自の修飾子を用意することができます。

v-model 修飾子の処理

v-model に追加された修飾子は、modelModifiers プロパティを介してコンポーネントに提供されます。

modelModifiers プロパティは v-model に追加された修飾子を格納するための予約プロパティで、v-model のカスタム修飾子を利用するには、modelModifiers プロパティを定義する必要があります。

例えば、.capitalize という修飾子を指定する場合、何も修飾子を指定しない場合のために、デフォルトで空のオブジェクトになる modelModifiers プロパティを定義します(属性が省略された場合のデフォルト値を定義)。

以下の場合、created ライフサイクルフックがトリガーされると、v-model.capitalize="myText" が設定されているため、modelModifiers プロパティの値は { capitalize: true } となります。

HTML
<div id="app">
  <my-component v-model.capitalize="myText"></my-component>
  {{ myText }}
  </div>
JavaScript
const app = Vue.createApp({
  data() {
    return {
      myText: ''
    }
  }
  })

  app.component('my-component', {
  props: {
    // modelValue プロパティを定義(プロパティの型も指定)
    modelValue: String,
    //デフォルトで空のオブジェクトになる modelModifiers プロパティを定義
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  template: `
    <input type="text"
      v-bind:value="modelValue"
      v-on:input="$emit('update:modelValue', $event.target.value)">
  `,
  //modelModifiers の値の確認用
  created() {
    console.log(this.modelModifiers) // { capitalize: true }とコンソールに出力される
  }
  });

  app.mount('#app');

修飾子 .capitalize が指定された場合の動作を methods に定義し、そのメソッドを v-on:input に指定します。

.capitalize が指定されている場合、modelModifiers.capitalize は true になるので、以下では先頭の文字を大文字に変換しています(23〜26行目)。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      myText: ''
    }
  }
  })

  app.component('my-component', {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  methods: {
    //入力時に実行する処理(v-on:input に指定するメソッド)
    emitValue(e) {
      //入力された値
      let value = e.target.value
      //modelModifiers.capitalize が true の(.capitalize が指定されている)場合
      if (this.modelModifiers.capitalize) {
        //先頭の文字を大文字に変換
        value = value.charAt(0).toUpperCase() + value.slice(1)
        //先頭の文字を大文字にし、それ以外を小文字にする場合は以下
        //value = value.charAt(0).toUpperCase() + value.toLowerCase().slice(1)
      }
      //値と共に update:modelValue イベントを $emit で発行
      this.$emit('update:modelValue', value)
    }
  },
  template: `
    <input type="text"
      v-bind:value="modelValue"
      v-on:input="emitValue">
  `
  });

  app.mount('#app');

引数を持つ v-model バインディングの場合

引数を持つ v-model の場合、修飾子を受け取るプロパティ名は modelModifiers ではなく、arg + "Modifiers" になります。例えば、v-model に引数として text を渡す場合(v-model:text)は、修飾子を受け取るプロパティ名は textModifiers になります。

HTML
<div id="app">
  <!-- v-model に引数として text を渡す場合 -->
  <my-component v-model:text.capitalize="myText"></my-component>
  {{ myText }}
  </div>

以下の例では、修飾子 .capitalize に加えて .upper と .lower を指定した場合の動作も定義しています。

const app = Vue.createApp({
  data() {
    return {
      myText: ''
    }
  }
  })

  app.component('my-component', {
  props: {
    //props オプションに text を定義
    text: String,
    //修飾子を受け取るプロパティ名は textModifiers
    textModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:text'],
  methods: {
    emitValue(e) {
      let value = e.target.value
      //this.textModifiers.修飾子
      if (this.textModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.toLowerCase().slice(1)
      }
      //修飾子が .upper の場合
      if (this.textModifiers.upper) {
        value = value.toUpperCase()
      }
      //修飾子が .lower の場合
      if (this.textModifiers.lower) {
        value = value.toLowerCase()
      }
      this.$emit('update:text', value)
    }
  },
  template: `
    <input type="text"
      v-bind:value="text"
      v-on:input="emitValue">
  `
  });

  app.mount('#app');
  

Provide/Inject

通常、親コンポーネントから子コンポーネントにデータを渡すとき、props を使いますが、深くネストされたコンポーネントの場合、データのバケツリレーが発生します。

Provide / Inject を使うことで、特定の階層で提供されたデータを、配下の任意の階層で取り込むことができ、コンポーネント間でのデータの共有をシンプルにすることができます。

例えば、以下のような階層のコンポーネント間で、provide オプションと inject オプションを使って値を共有することができます。

Root
└─ parent-div
   └─ foo-div
      └─ child-div

以下は parent-div で provide オプションを使って title という値を提供し、foo-div と child-div で inject オプションを使って title を共有する(取り込む)例です。

JavaScript
const app = Vue.createApp({});

app.component('parent-div', {
  //値を提供(provide オプション)
  provide: {
    title: 'タイトルです!'
  },
  template: `
    <div id="parent">
      <foo-div />
    </div>`
}).component('foo-div', {
  //値を注入(inject オプション)
  inject: ['title'],
  template: `
    <div id="foo">
      foo: {{ title }}
      <child-div />
    </div>`
}).component('child-div', {
  //値を注入(inject オプション)
  inject: ['title'],
  template: `
    <div id="child">
      child: {{ title }}
    </div>`
})

app.mount('#app');
HTML
<div id="app">
  <parent-div />
</div>

以下のように出力されます。

<div id="app" data-v-app="">
  <div id="parent">
    <div id="foo"> foo: タイトルです!
      <div id="child"> child: タイトルです!</div>
    </div>
  </div>
</div>

インスタンスプロパティの提供

上記の例では、provide オプションを「名前: 値」の形式で指定いますが、データオブジェクト(data)やプロパティ(props)などのインスタンスプロパティを提供するには、provide オプションをオブジェクトを返す関数へ変換する必要があります。

以下はデータオブジェクトに定義した title とプロパティで定義した subTitle を共有する例です。

this. でアクセスするインスタンスプロパティを渡す際は、provide オプションはオブジェクトを返す関数として定義する必要があります。

JavaScript
const app = Vue.createApp({});
app.component('parent-div', {
  //データオブジェクト
  data() {
    return {
      title: 'タイトルです!'
    }
  },
  //プロパティ
  props: ['subTitle'],
  //provide オプション(ブジェクトを返す関数として定義)
  provide() {
    return {
      title: this.title,
      subTitle: this.subTitle
    }
  },
  template: `
    <div id="parent">
      <foo-div />
    </div>`
}).component('foo-div', {
  //値を注入(inject オプション)
  inject: ['title', 'subTitle'],
  template: `
    <div id="foo">
      foo: {{ title + ' : ' + subTitle}}
      <child-div />
    </div>`
}).component('child-div', {
  //値を注入(inject オプション)
  inject: ['title', 'subTitle'],
  template: `
    <div id="child">
      child: {{ title + ' : ' + subTitle }}
    </div>`
})

app.mount('#app');
HTML
<div id="app">
  <parent-div sub-title="サブタイトル!"/>
</div>

以下のように出力されます。

<div id="app" data-v-app="">
  <div id="parent">
    <div id="foo"> foo: タイトルです! : サブタイトル!
      <div id="child"> child: タイトルです! : サブタイトル!</div>
    </div>
  </div>
</div>

リアクティブな値の提供

以下の場合、provide/inject のバインドがデフォルトでリアクティブ でないため、テキストフィールドに入力された値は注入された title には反映されません。

JavaScript
const app = Vue.createApp({});

 app.component('parent-div', {
   data() {
     return {
       title: 'タイトルです!'
     }
   },
   provide() {
     return {
       //データオブジェクトに定義した title
       title: this.title
     }
   },
   template: `
     <div id="parent">
       <input v-model="title">
       <foo-div />
     </div>`
 }).component('foo-div', {
   inject: ['title'],
   template: `
     <div id="foo">
       foo: {{ title }}
       <child-div />
     </div>`
 }).component('child-div', {
   inject: ['title'],
   template: `
     <div id="child">
       child: {{ title }}
     </div>`
 })

 app.mount('#app');
HTML
<div id="app">
  <parent-div />
</div>

ref で定義されたプロパティや reactive で作成されたオブジェクトを provide に渡すことにより、この振る舞いを変更することができます。

祖先コンポーネントから提供されたデータをリアクティブにする(変更を inject 側に反映させる)ためには、Composition API の computed メソッドで定義したプロパティを割り当てることができます。

リアクティブと連携する

以下は computed() メソッドを使って、title プロパティの値を返す算出(computed)プロパティを定義しています。これにより、title への変更は inject 側にも反映されるようになります。

computed メソッドは、リアクティブな ref オブジェクトを返すので、値は .value プロパティでアクセスできます(※)。

JavaScript
const app = Vue.createApp({});

app.component('parent-div', {
  //データオブジェクト
  data() {
    return {
      title: 'タイトルです!'
    }
  },
  provide() {
    return {
      //明示的に算出(computed)プロパティを指定
      title: Vue.computed(() => this.title)
    }
  },
  template: `
    <div id="parent">
      <input v-model="title">
      <foo-div />
    </div>`
}).component('foo-div', {
  inject: ['title'],
  template: `
    <div id="foo">
      foo: {{ title.value }}
      <child-div />
    </div>`
}).component('child-div', {
  inject: ['title'],
  template: `
    <div id="child">
      child: {{ title.value }}
    </div>`
})

app.mount('#app');
HTML
<div id="app">
  <parent-div />
</div>

以下のように出力されます。この例の場合、テキストボックスに入力された値が title に反映されます。

<div id="app" data-v-app="">
  <div id="parent">
    <input kl_vkbd_parsed="true">
    <div id="foo"> foo: タイトルです!
      <div id="child"> child: タイトルです!</div>
    </div>
  </div>
</div>

※Vue 3.3 からは仕様が変更されるようで、Vue 3.2.36 で上記を実行すると「[Vue warn]: injected property "title" is a ref and will be auto-unwrapped and no longer needs `.value` in the next minor release. To opt-in to the new behavior now, set `app.config.unwrapInjectedRef = true` (this config is temporary and will not be needed in the future.) 」のような警告が出ます(自動的にアンラップされるので .value は不要になる)。

上記で、.value を指定しない場合は、「 foo: "タイトルです!"」のように title の内容がダブルクォートで囲まれて出力されます。

オブジェクトの変更はリアクティブ

以下は、provide でオブジェクトを提供する例です。この場合、オブジェクトの変更は inject 側にも反映されます。

JavaScript
const app = Vue.createApp({});

app.component('parent-div', {
  //データオブジェクト
  data() {
    return {
      //オブジェクト
      obj: {
        title: 'オブジェクトのタイトルです!'
      }
    }
  },
  provide() {
    return {
      //オブジェクトを提供
      obj: this.obj
    }
  },
  template: `
    <div id="parent">
      <input v-model="obj.title">
      <foo-div />
    </div>`
}).component('foo-div', {
  inject: ['obj'],
  template: `
    <div id="foo">
      foo: {{ obj.title }}
      <child-div />
    </div>`
}).component('child-div', {
  inject: ['obj'],
  template: `
    <div id="child">
      child: {{ obj.title }}
    </div>`
})

app.mount('#app');
HTML
<div id="app">
  <parent-div />
</div>

以下のように出力されます。この例の場合も、テキストボックスに入力された値が title に反映されます。

<div id="app" data-v-app="">
  <div id="parent">
    <input kl_vkbd_parsed="true">
    <div id="foo"> foo: オブジェクトのタイトルです!
      <div id="child"> child: オブジェクトのタイトルです!</div>
    </div>
  </div>
</div>

provide の値の更新

provide の値の更新は、原則として provide 側のコンポーネントで行います。

もし、inject 側で値を操作したい場合は、provide 側で更新メソッドを定義して、inject 側で更新メソッドも inject します。

JavaScript
const app = Vue.createApp({});

app.component('parent-div', {
  data() {
    return {
      obj: {
        title: 'タイトルです!'
      }
    }
  },
  methods: {
    //データの obj.title を更新するメソッド
    updateTitle() {
      this.obj.title = '新しいタイトルです!';
    }
  },
  provide() {
    return {
      obj: this.obj,
      //obj.title を更新するメソッドを提供
      updateTitle: this.updateTitle
    }
  },
  template: `
    <div id="parent">
      <foo-div />
    </div>`
}).component('foo-div', {
  template: `
    <div id="foo">
      <child-div />
    </div>`
}).component('child-div', {
  //値(この場合はオブジェクト)とその更新メソッドを注入
  inject: ['obj', 'updateTitle'],
  template: `
    <div id="child">
      child: {{ obj.title }}
      <button  v-on:click="updateTitle">タイトル更新</button>
    </div>`
})

app.mount('#app');
HTML
<div id="app">
  <parent-div />
</div>

以下のように出力され、ボタンをクリックすると title の「タイトルです!」が「新しいタイトルです!」に変わります。

<div id="app" data-v-app="">
  <div id="parent">
    <div id="foo">
      <div id="child"> child: タイトルです!
        <button>タイトル更新</button></div>
    </div>
  </div>
</div>

テンプレート参照(ref 属性)

ref 属性を使うことでコンポーネントや HTML 要素を参照することができます(テンプレート参照)。

参照(取得)したいコンポーネントや DOM 要素のタグ内に ref 属性で名前を宣言(値に任意の名前を指定)し、$refs オブジェクトを介して参照します。

参照は親コンポーネントの $refs オブジェクトの下に登録されます。単純に DOM 要素で使った場合は参照はその要素になり、子コンポーネントで使った場合は参照はコンポーネントインスタンスになります。

登録された要素(ref 属性を指定した要素やコンポーネント)は、ref 属性で指定した名前 xxxx を使って、$refs.xxxx や $refs['xxxx'] で参照します。

以下の例ではテンプレートの div 要素の ref 属性に foo という名前を指定し、その要素を $refs.foo(または $refs['foo'])で参照しています。

JavaScript
const app = Vue.createApp({});

app.component('foo-div', {
  //ref 属性に名前を指定
  template: `<div ref="foo">This is Foo.</div>`,
  //ライフサイクルの mounted で $refs にアクセス
  mounted() {
    console.log(this.$refs.foo);  //<div>This is Foo.</div> が出力される
    //console.log(this.$refs['foo']); でも同じ
  }
});

app.mount('#app');
HTML
<div id="app">
  <foo-div></foo-div>
</div>

以下はテンプレートの div 要素の ref 属性に target という名前を指定し、イベントリスナーで ref 属性を指定した div 要素を $refs.target で参照しています。

JavaScript
const app = Vue.createApp({});

app.component('sample-div', {
  template: `
    <div ref='target'> {{ message }}</div>
    <button type="button" v-on:click="changeColor">Change Color</button>
  `,
  data() {
    return {
      message: 'Hello!'
    }
  },
  methods:{
    changeColor(){
      this.$refs.target.style.setProperty('color', 'red');
    }
  }
});
app.mount('#app');
HTML
<div id="app">
  <sample-div></sample-div>
</div>

以下はコンポーネントのマウント時に input 要素をフォーカスさせる例です。

JavaScript
const app = Vue.createApp({});

app.component('base-input', {
  template: `
    Text : <input ref="bar" />
  `,
  methods: {
    focusInput() {
      this.$refs.bar.focus();
    }
  },
  mounted() {
    this.focusInput();
  }
});
app.mount('#app');
HTML
<div id="app">
  <base-input />
</div>

以下は v-for を使った例で、ref の名前は team としています。それぞれの要素にはインデックスで参照することができます。

JavaScript
Vue.createApp({
  data() {
    return {
      users: ['Foo', 'Bar', 'Baz']
    }
  },
  methods: {
    onclick() {
      this.$refs.team[0].style.setProperty('color', 'red');
      this.$refs.team[1].style.setProperty('color', 'blue');
      this.$refs.team[2].style.setProperty('color', 'green');
    }
  }
}).mount('#app');
HTML
<div id="app">
  <ul>
    <li v-for="user in users" ref="team">{{ user }}</li>
  </ul>
  <button type="button" @click="onclick">Change Color</button>
</div>

注意点

※ $refs はコンポーネントがレンダリングされた後にのみ生成されます。そのため、ライフサイクルにおける beforeMount 以前では使えません(mounted で初めてアクセスできるようになります)。

以下は、テンプレートの要素の ref 属性に指定した値を使って this.$refs.xxxx でその要素や子コンポーネントのインスタンスを参照する例です。created や beforeMount では undefined になります。

HTML
<div id="app">
  <p ref="hello">hello</p>
  <my-child ref="child"></my-child>
</div>
const app = Vue.createApp({
  created() {
    console.log(this.$refs.hello);  //undefined
    console.log(this.$refs.child);  //undefined
  },
  beforeMount() {
    console.log(this.$refs.hello);  //undefined
    console.log(this.$refs.child);  //undefined
  },
  mounted() {
    console.log(this.$refs.hello);  //<p ref="hello">hello</p> (DOM 要素)
    console.log(this.$refs.child);  //Proxy {…}  (子コンポーネントのインスタンス)
    //子コンポーネントのインスタンスの $el プロパティでその要素を参照
    console.log(this.$refs.child.$el);  //<my-child ref="child"></my-child>
  },
});

app.component('my-child', {
  template: `<div class="child">My Child</div>`,
});
app.mount('#app');

また、$refs はリアクティブではないため、テンプレート内や computed プロパティから $refs にアクセスするべきではありません。

テンプレート参照について

$refs と $parent

props$emitProvide/Inject などを利用する代わりに、$refs$parent インスタンスプロパティを利用して、親子間でコンポーネントを取得してデータを交換することもできます。

※ 但し、$refs と $parent を使ったデータの交換は例外的な手法で、コンポーネント間の通信は props や $emit、Provide/Inject を利用するのが基本になります。

プロパティ 概要
$refs ref 属性 で登録された DOM 要素のオブジェクトとコンポーネントインスタンス Object
$parent 現在のインスタンスに親インスタンスがある場合は、その親インスタンス Component instance
$root 現在のコンポーネントツリーのルートコンポーネントインスタンス。現在のインスタンスが親を持たない場合、この値は自分自身になります。 Component instance

以下はルートコンポーネントの配下に子コンポーネント child-div が配置されていて、それぞれ子から親の、親から子のデータオブジェクトを設定しています。

親コンポーネントから子コンポーネントを参照するには、$refs プロパティを利用し、子コンポーネントから親コンポーネントを取得するには $parent を利用しています。

$refs プロパティを利用するには、ref 属性で参照するコンポーネントの名前を宣言する必要があります。

また、この例の場合、親はルートコンポーネントなので、$root インスタンスプロパティで参照することもできます。

HTML
<div id="app">
  <div>Parent: {{ message }}</div>
  <child-div ref="child"></child-div><!-- ref 属性で名前を宣言 -->
</div>
JavaScript
const app = Vue.createApp({
  //ルートコンポーネントのデータオブジェクト
  data() {
    return {
      message: ''
    }
  },
  //マウント時に子コンポーネントの message を設定
  mounted() {
    //ref 属性で宣言した名前で参照
    this.$refs.child.message = 'Message From Parent'
  }
}).component('child-div', {
  //子コンポーネントのデータオブジェクト
  data() {
    return {
      message: ''
    }
  },
  template: `<div>Child: {{ message }} </div>`,
  //マウント時に親(ルート)コンポーネントの message を設定
  mounted() {
    this.$parent.message = 'Message From Child'
    //この例の場合は親はルートコンポーネントなので以下でも同じ
    //this.$root.message = 'Message From Child'
  }
});
app.mount('#app');

以下のように出力されます。

<div id="app" data-v-app="">
  <div>Parent: Message From Child</div>
  <div>Child: Message From Parent</div>
</div>

スロット

スロットを使うと、コンポーネントの呼び出し側で指定したコンテンツをテンプレートに埋め込むことができ、コンポーネントにコンテンツを渡すことができます。

コンテンツを配置したいところにプレースホルダとして <slot> を使います。

<slot> 配下のコンテンツ(以下の場合は World.)は、呼び出し側でコンテンツが指定されなかった場合に出力されるフォールバックコンテンツ(デフォルトのコンテンツ)です。

JavaScript
const app = Vue.createApp({});

app.component('hello-div', {
  template: `
    <div>
      Hello, <slot>World.</slot>
    </div>`
})

app.mount('#app');

呼び出し側で指定したコンテンツ(以下の場合 Foo)が <slot> の場所に埋め込まれます(テンプレートのフラグメントを子コンポーネントに渡して、子コンポーネントのテンプレート内でそのフラグメントをレンダリングしてもらうことができます)。

HTML(呼び出し側)
<div id="app">
  <!-- 呼び出し側のテンプレート-->
  <hello-div>Foo</hello-div>
</div>

以下のように出力されます。

<div id="app" data-v-app="">
  <div> Hello, Foo</div>
</div>

スロットのコンテンツを指定せずに以下のように <hello-div> を呼び出すと、

HTML(呼び出し側)
<div id="app">
  <hello-div></hello-div>
</div>

テンプレートの <slot> タグの間に指定した「World.」がレンダリングされ、以下のように出力されます。

<div id="app" data-v-app="">
  <div> Hello, World.</div>
</div>

フォールバックコンテンツ

前述のようにスロットには、何もコンテンツが与えられなかった場合にのみレンダリングされるフォールバック (デフォルト) コンテンツを指定することができます。

以下はスロットコンテンツに何も与えなかった場合に <button> 内に "クリック" というテキストをレンダリングする例です。フォールバックコンテンツにするテキストを <slot> タグの間に置きます。

JavaScript
const app = Vue.createApp({});
app.component('my-button', {
  template: `
    <button type="button">
      <slot>
        クリック <!-- フォールバックコンテンツ -->
      </slot>
    </button>
  `
}).mount('#app');

スロットに何もコンテンツを提供せずに my-butto を親コンポーネント内で使用すると

HTML(呼び出し側)
<div id="app">
  <my-button></my-button>
</div>

フォールバックコンテンツの "クリック" がレンダリングされます。

<div id="app" data-v-app="">
  <button type="button"> クリック</button>
</div>;

コンテンツを与えた場合は、

HTML(呼び出し側)
<div id="app">
  <my-button type="submit">送信</my-button>
</div>

与えたコンテンツが代わりにレンダリングされます。

<div id="app" data-v-app="">
  <button type="submit">送信</button>
</div>

上記の例では呼び出し側で type 属性を上書きしていますが、これは button 要素がルート要素で、且つテンプレートが一つのルート要素をもつ場合にのみ可能です。関連項目:属性が重複している場合

スロットのスコープ

スロットコンテンツは親で定義されているため、親コンポーネントのデータスコープへアクセスできます。また、スロットコンテンツには、HTML や子コンポーネント、 Mustache 構文 {{}}、ディレクティブなどを含めることができます。

以下は親コンポーネントのテンプレートで自身の label プロパティにアクセスしています。

JavaScript
const app = Vue.createApp({
  //親コンポーネント(ルートコンポーネント)のデータ
  data() {
    return {
      label: 'Click Me!'
    }
  },
});

app.component('my-button', {
  template: `
      <button type="button">
       <slot></slot>
      </button>
  `
}).mount('#app');
HTML(呼び出し側)
<div id="app">
  <!-- 親(ルートコンポーネント)自身のデータオブジェクトにアクセス-->
  <my-button>{{ label }}</my-button>
</div>

以下のように出力されます。

<div id="app" data-v-app="">
<button type="button">Click Me!</button>
</div>

スロットはテンプレートの残りの部分と同じインスタンスのプロパティにアクセスできます(その式が定義されたスコープ内のみアクセスできます)。

しかし、スロットコンテンツは子コンポーネントのデータへはアクセスできません。

Mustache 構文 {{}} やディレクティブの式は、現在のインスタンスに属するため、親コンポーネントのテンプレートから子コンポーネントのデータオブジェクトにアクセスすることはできません。

例えば、以下は親コンポーネントのテンプレートから子コンポーネントの label プロパティ(データオブジェクト)にアクセスしようとしていますが、エラーになり「Property "label" was accessed during render but is not defined on instance. 」のような警告が出ます。

const app = Vue.createApp({});

app.component('my-button', {
  //子コンポーネントのデータ
  data() {
    return {
      label: 'Click Me!'
    }
  },
  template: `
      <button type="button">
       <slot></slot>
      </button>
  `
}).mount('#app');
HTML(前述の例と同じ)
<div id="app">
  <!-- 子コンポーネントのデータオブジェクトにアクセスできない(エラー)-->
  <my-button>{{ label }}</my-button>
</div>

以下は子コンポーネントのテンプレートで、フォールバックコンテンツとして子コンポーネント(同じスコープ)のデータ label にアクセスしているので問題ありません。

const app = Vue.createApp({});

app.component('my-button', {
  //子コンポーネントのデータ
  data() {
    return {
      label: 'Click Me!'
    }
  },
  template: `
      <button type="button">
       <slot>{{ label }}</slot><!-- 子コンポーネントのデータオブジェクトにアクセス-->
      </button>
  `
}).mount('#app');

以下が基本的なルールです。

親のテンプレート内の全てのものは親のスコープでコンパイルされ(親コンポーネントが管理し)、子のテンプレート内の全てのものは子のスコープでコンパイルされる(子コンポーネントが管理する)。

スコープ付きスロットを使うと親コンポーネントのテンプレートから子コンポーネントのプロパティにアクセスすることができます。

名前付きスロット

テンプレートに複数のスロットを用意して、呼び出し側から複数のコンテンツを埋め込むこともできます。

その場合、<slot> 要素は name という特別な属性を持っていて、それぞれのスロットにユニークな ID を割り当てることによってコンテンツがどこにレンダリングされるべきかを指定することができます。

名前付きスロット

以下は複数のスロットを利用する例です。

複数のスロットを利用する場合は、それぞれを区別できるように <slot> 要素に name 属性を指定します。名前のないスロットは「name="default"」として扱われます。

以下の場合、header、default、footer の3つのスロットが用意されています。

JavaScript
const app = Vue.createApp({});

app.component('base-layout', {
  template: `
    <div class="container">
      <header>
        <slot name="header">Default Header</slot>
      </header>
      <main>
        <slot>Default Main</slot>
      </main>
      <footer>
        <slot name="footer">Default Footer</slot>
      </footer>
    </div>`
})

app.mount('#app');

<template> 要素と v-slot ディレクティブ

呼び出し側では、それぞれのスロットに対応するテンプレートを <template> 要素を使って用意します。

名前付きスロットにコンテンツを渡すには、<template> 要素に対して v-slot ディレクティブを使い、スロット名をその引数として指定します。

HTML(呼び出し側)
<div id="app">
  <base-layout>
    <template v-slot:header>
      <h3>Vue.js Sample</h3>
    </template>
    <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. </p>
    <p>Nihil quidem fugit aut sed neque itaque accusamus ids.</p>
    <template v-slot:footer>
      <small>Copyright ©xxxx. All rights reserved.</small>
    </template>
  </base-layout>
</div>

以下のように出力されます。<template> 要素と v-slot で明示的に指定されなかった要素は、名前(name 属性)のないスロット(この例では main)に埋め込まれます。

<div id="app" data-v-app="">
  <div class="container">
    <header>
      <h3>Vue.js Sample</h3>
    </header>
    <main>
      <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. </p>
      <p>Nihil quidem fugit aut sed neque itaque accusamus ids.</p>
    </main>
    <footer><small>Copyright ©xxxx. All rights reserved.</small></footer>
  </div>
</div>

呼び出し側で、明示的に <template> 要素と v-slot:default を指定して以下のように記述しても同じです。

HTML(呼び出し側)
<div id="app">
  <base-layout>
    <template v-slot:header>
      <h3>Vue.js Sample</h3>
    </template>
    <template v-slot:default>
      <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. </p>
      <p>Nihil quidem fugit aut sed neque itaque accusamus ids.</p>
    </template>
    <template v-slot:footer>
      <small>Copyright ©xxxx. All rights reserved.</small>
    </template>
  </base-layout>
</div>

動的なスロット名

v-slot でも動的なスロット名の定義(角括弧で囲むことで JavaScript 式をディレクティブの引数に使うこと)が可能です。

<template v-slot:[dynamicSlotName]>
    ...
</template>

名前付きスロットの省略記法

v-on や v-bind と同様に v-slot にも省略記法があり、v-slot:# で置き換えます。

前述の例は、以下のように記述しても同じです。

<div id="app">
  <base-layout>
    <template #header>
      <h3>Vue.js Sample</h3>
    </template>
    <template #default>
      <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. </p>
      <p>Nihil quidem fugit aut sed neque itaque accusamus ids.</p>
    </template>
    <template #footer>
      <small>Copyright ©xxxx. All rights reserved.</small>
    </template>
  </base-layout>
</div>
$slots

それぞれの名前付きスロットは自身に対応するプロパティを持ち、this.$slots から取得できます。

例えば、以下の v-slot:header のコンテンツは this.$slots.header() でアクセスでき、名前付きスロットに含まれないノードか、v-slot:default のコンテンツは this.$slots.default() でアクセスできます。

$slots | Render 関数/スロット

HTML(呼び出し側)
<div id="app">
  <base-layout>
    <template v-slot:header>
      <h3>Vue.js Sample</h3>
    </template>
    <template v-slot:default>
      <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. </p>
      <p>Nihil quidem fugit aut sed neque itaque accusamus ids.</p>
    </template>
    <template v-slot:footer>
      <small>Copyright ©xxxx. All rights reserved.</small>
    </template>
  </base-layout>
</div>

this.$slots へのアクセスは、render 関数でコンポーネントを書くときに便利です。

const { createApp, h } = Vue;
const app = createApp({});

app.component('base-layout', {
  render() {
    return h('div', {class:'container' }, [
      h('header', this.$slots.header()),
      h('main', this.$slots.default()),
      h('footer', this.$slots.footer())
    ])
  }
});
app.mount('#app');

上記は、以下と同じことです。

const { createApp } = Vue;
const app = createApp({});

app.component('base-layout', {
  template: `
    <div class="container">
      <header>
        <slot name="header"></slot>
      </header>
      <main>
        <slot>n</slot>
      </main>
      <footer>
        <slot name="footer"></slot>
      </footer>
    </div>`
})
app.mount('#app');

以下はスロットのフォールバックコンテンツを指定する例です。length プロパティが 0 の場合に、フォールバックコンテンツを指定するようにしています。

const { createApp, h } = Vue;
const app = createApp({});

app.component('base-layout', {
  render() {
    return h('div', {class:'container' },  [
      h('header',
        this.$slots.header().length ? this.$slots.header() : 'Default Header'
      ),
      h('main',
        this.$slots.default().length ? this.$slots.default() : 'Default Main'
      ),
      h('footer',
        this.$slots.footer().length ? this.$slots.footer() : 'Default Footer'
      )
    ])
  }
});
app.mount('#app');

以下は mounted ライフサイクルフックで $slots の内容を出力して確認する例です。

const { createApp, h } = Vue;
const app = createApp({});

app.component('base-layout', {
  // $slots を出力
  mounted() {
    console.log(this.$slots);
    //Proxy {_: 1, __vInternal: 1, header: ƒ, default: ƒ, footer: ƒ}
    console.log(this.$slots.header());
    //{__v_isVNode: true, __v_skip: true, type: 'h1', props: null, key: null, …}
    console.log(this.$slots.header);
    //警告?が出力される
  },

  render() {
    return h('div', {class:'container'},  [
      h('header', this.$slots.header()),
      h('main', this.$slots.default()),
      h('footer', this.$slots.footer())
    ])
  }
});
app.mount('#app');

スコープ付きスロット

スロットのスコープで説明したように、スロットのコンテンツは子コンポーネント内の状態(データ)にアクセスできません。

親コンポーネント内でスロットコンテンツとして子コンポーネントのデータアクセスできるようにするには、<slot> 要素の属性として v-bind: を使ってデータをバインドします。

<slot> 要素にバインドされた属性のことをスロットプロパティと呼びます。

v-bind:スロットプロパティ(属性名)="式(データ)"

スコープ付きスロット

以下では、<slot> 要素に v-bind: で text という独自の属性を追加し、値に label(データオブジェクト)を設定することで、スロットプロパティとして呼び出し側に公開しています。

JavaScript
const app = Vue.createApp({});

app.component('my-button', {
  //子コンポーネントのデータ
  data() {
    return {
      label: 'Click Me!'
    }
  },
  template: `
    <button type="button">
      <!-- v-bind:属性名="データ" でスロットプロパティとして呼び出し側に公開 -->
      <slot v-bind:text="label"></slot>
    </button>
  `
}).mount('#app');

スロットプロパティを受け取る親スコープ内では v-slot:スロット名 に任意の名前を指定して、渡されるスロットプロパティを持つオブジェクトの名前を定義します。

v-slot:スロット名="スロットプロパティオブジェクトの任意の名前"

この例の場合、名前のないスロット(デフォルトスロット)なので v-slot:default としていますが、名前付きスロットの場合は v-slot: の引数に default の代わりにスロット名を指定します。

以下ではスロットプロパティを持つオブジェクトの名前を slotProp としているので、スロットプロパティには slotProp.スロットプロパティ名(属性名)、つまり slotProp.text でアクセスすることができます。

スロットプロパティを持つオブジェクトには任意の名前を指定でき、slotProp である必要はありません。

HTML(呼び出し側)
<div id="app">
  <my-button>
    <!-- v-slot:スロット名="オブジェクト名" でオブジェクトの名前を定義 -->
    <template v-slot:default="slotProp">
      {{ slotProp.text }}  <!-- オブジェクト名.スロットプロパティ名-->
    </template>
  </my-button>
</div>

以下のように出力されます。

<div id="app" data-v-app="">
  <button type="button">Click Me!</button>
</div>

props で書き換え

前述の例は、渡すデータは文字列なので、以下のように props を使って書き換えることもできます。props を定義して対応するカスタム属性をスロットに指定します。

呼び出し側ではスロットプロパティとして受け取れるので、呼び出し側のコードは前述の例と同じです。

const app = Vue.createApp({});

app.component('my-button', {
  // props を定義 (省略可能)
  props: ['text'],
  template: `
    <button type="button">
      <slot text="Click Me!"></slot> <!--スロットにカスタム属性を指定 -->
    </button>
  `
}).mount('#app');

上記ではカスタム属性に文字列を渡していますが、データを渡す場合は以下のようになります(カスタム属性に文字列以外を指定する場合は、v-bind を使用)。

内容は props の定義が増えただけで前述の例と同じです。実際には props の定義は不要で、スロットプロパティはスロットの props ということになります。

const app = Vue.createApp({});

app.component('my-button', {
  // props を定義 (省略可能)
  props: ['text'],
  //子コンポーネントのデータ
  data() {
    return {
      label: 'Click Me!'
    }
  },
  template: `
    <button type="button">
      <slot v-bind:text="label"></slot><!-- スロットのカスタム属性にデータを指定 -->
    </button>
  `
}).mount('#app');

デフォルトスロットしかない場合の省略記法

コンポーネント配下にデフォルトスロットしかない場合、コンポーネントのタグをスロットのテンプレートとして使うことができます(コンポーネントタグに対して v-slot を直接使えます)。

前述の例は、<template> 要素の代わりにコンポーネントタグに v-slot を指定して以下のように記述することができます。

<div id="app">
  <my-button v-slot:default="slotProp">
    {{ slotProp.text }}
  </my-button>
</div>

また、この場合、:default も省略して以下のように記述することができます。

<div id="app">
  <my-button v-slot="slotProp">
    {{ slotProp.text }}
  </my-button>
</div>

※ 但し、デフォルトスロットに対する省略記法は、名前付きスロットと混在させることはできません。

スロットプロパティの分割代入

オブジェクトの分割代入を使うと、スロットプロパティの呼び出しのコードを簡潔に記述できます(特に、スロットが多くのプロパティを提供している場合)。

以下は上記の例の slotProp を分割代入を使って { text } に書き換えた例です。

スロットプロパティオブジェクトの text プロパティ(slotProp.text)を分割代入で取得して変数 text に代入しているので、{{ slotProp.text }} の代わりに、{{ text }} と記述することができます。

<div id="app">
  <my-button>
    <template v-slot:default="{ text }">
      {{ text }}
    </template>
  </my-button>
</div>

必要な数の属性を slot にバインドすることができます。

以下は、item 属性と index 属性をスロットプロパティとして設定する例です(item と index は items プロパティを v-for で出力するための仮引数です)。

JavaScript
const app = Vue.createApp({});

app.component('todo-list', {
  data() {
    return {
      items: ['Feed a cat', 'Buy milk']
    }
  },
  template: `
    <ul>
      <li v-for="(item, index) in items">
        <slot v-bind:item="item" v-bind:index="index"></slot>
      </li>
    </ul>
  `
})

app.mount('#app');
HTML
<div id="app">
  <todo-list>
    <template v-slot:default="slotProps">
      <span>{{ (slotProps.index +1 ) + ":" + slotProps.item }}</span>
    </template>
  </todo-list>
</div>

v-for 同様、スロットプロパティの呼び出しで分割代入を使うと以下のように記述できます。

HTML
<div id="app">
  <todo-list>
    <template v-slot:default="{ index, item }">
      <span>{{ (index +1 ) + ":" + item }}</span>
    </template>
  </todo-list>
</div>

以下のように出力されます。

<div id="app" data-v-app="">
  <ul>
    <li><span>1:Feed a cat</span></li>
    <li><span>2:Buy milk</span></li>
  </ul>
</div>

名前付きのスコープ付きスロット

今までの例は名前のないデフォルトスロットでしたが、名前付きのスロットでもほぼ同様です。

デフォルトスロット同様、それぞれの <slot> 要素にスロットプロパティをバインドします。

JavaScript
const app = Vue.createApp({});

app.component('base-layout', {
  data() {
    return {
      heading: 'Data Header',
      content: 'Data Content',
      footing: 'Data Footing',
    }
  },
  template: `
    <div class="container">
      <header>
        <slot name="header" v-bind:heading="heading">Default Header</slot>
      </header>
      <main>
        <slot name="default" v-bind:content="content">Default Main</slot>
      </main>
      <footer>
        <slot name="footer" v-bind:footing="footing">Default Footer</slot>
      </footer>
    </div>`
})

app.mount('#app');

template 要素では v-slot: の引数にスロット名を指定します。

HTML
<div id="app">
  <base-layout>
    <template v-slot:header="headerProps">
      <h3>{{ headerProps.heading }}</h3>
    </template>
    <template v-slot:default="defaultProps">
      {{ defaultProps.content }}
    </template>
    <template v-slot:footer="footerProps">
      <small>{{ footerProps.footing }}</small>
    </template>
  </base-layout>
</div>

スロットプロパティの呼び出しで分割代入を使うと以下のように記述できます。

HTML
<div id="app">
  <base-layout>
    <template v-slot:header="{heading}">
      <h3>{{ heading }}</h3>
    </template>
    <template v-slot:default="{content}">
      {{ content }}
    </template>
    <template v-slot:footer="{footing}">
      <small>{{ footing }}</small>
    </template>
  </base-layout>
</div>

以下のように出力されます。

<div id="app" data-v-app="">
  <div class="container">
    <header>
      <h3>Data Header</h3>
    </header>
    <main>Data Content</main>
    <footer><small>Data Footing</small></footer>
  </div>
</div>

props を使う例

以下は前述の例をプロパティ(props)を使って書き換えた例です。

名前付きスロットで props を使う場合、slot 要素には name 属性が使われるので、カスタム属性として name を使用(指定)することはできません。

JavaScript
const app = Vue.createApp({});

app.component('base-layout', {
  props: ['heading','content', 'footing'],
  template: `
    <div class="container">
      <header>
        <slot name="header" heading="Props Header">Default Header</slot>
      </header>
      <main>
        <slot name="default" content="Props Content">Default Main</slot>
      </main>
      <footer>
        <slot name="footer" footing="Props Footing">Default Footer</slot>
      </footer>
    </div>`
})

app.mount('#app');
HTML(前述の例と同じ)
<div id="app">
  <base-layout>
    <template v-slot:header="headerProps">
      <h3>{{ headerProps.heading }}</h3>
    </template>
    <template v-slot:default="defaultProps">
      {{ defaultProps.content }}
    </template>
    <template v-slot:footer="footerProps">
      <small>{{ footerProps.footing }}</small>
    </template>
  </base-layout>
</div>

以下のように出力されます。

<div id="app" data-v-app="">
  <div class="container">
    <header>
      <h3>Props Header</h3>
    </header>
    <main>Props Content</main>
    <footer><small>Props Footing</small></footer>
  </div>
</div>

組み込みコンポーネント

Vue には特別に登録することなく、テンプレート内で直接使用することができる以下のような組み込み(ビルトイン)コンポーネントが用意されています。

コンポーネント 概要
<component> 動的なコンポーネントをレンダリングするためのメタコンポーネント(meta component:コンポーネントを制御するためのコンポーネント)
<keep-alive> 非アクティブなコンポーネントインスタンスを破壊することなく状態を維持します
<transition> 単一の要素やコンポーネントにアニメーションを適用します
<transition-group> 複数の要素やコンポーネントにアニメーションを適用します
<slot> コンポーネントの呼び出し側で指定したコンテンツをテンプレートに埋め込みます
teleport 要素をページ内の指定した位置に移動します(Teleport
suspense 非同期処理の間、待機処理できるように代替手段を提供します(実験的機能)

動的なコンポーネント component 要素

<component> 要素と is 属性を使って、予め用意したコンポーネントを動的に切り替えることができます。

どのコンポーネントをレンダリング(表示)するかは、component 要素の is 属性によって決定されます。

is 属性の値は、コンポーネント名またはHTMLタグ名、コンポーネントのオプションオブジェクトのいずれかを指定することができます。

以下は用意した3つのバナーのコンポーネントを3秒ごとに切り替えて表示する例です。

component 要素はコンポーネントの表示領域を表します。 component 要素には v-bind: で is 属性を指定します。

HTML
<div id="app">
  <!-- is 属性には算出プロパティ currentBanner が返す値を指定 -->
  <component v-bind:is="currentBanner" />
</div>

動的に切り替えるコンポーネントを用意

以下では、動的に切り替えるバナーのコンポーネントを component メソッドを使って用意(定義)しています(29〜46行目)。

data と computed で切り替えるコンポーネントを管理

この例では data オプションでバナーのコンポーネントのリスト(名前の後半部分の配列)と現在表示されているバナーのインデックスを定義し、computed オプションでそれらの値を使って表示するコンポーネントの名前を生成して currentBanner として返し、is 属性の値を更新しています。

is 属性に指定する値(コンポーネントの名前)は動的に取得できれば、computed オプション(算出プロパティ)以外を使用することもできます。

また、ライフサイクルフックを使って切り替え用のタイマーの設定及び破棄をしています。

JavaScript
const app = Vue.createApp({
  //データの初期化後に切り替え用のタイマーを設定
  created() {
    this.timer = setInterval( ()=> {
      this.currentIndex = (this.currentIndex + 1) % this.banners.length;
      //console.log(this.currentBanner);
    }, 3000)
  },
  //コンポーネントが破棄される直前にタイマーをクリア
  beforeUnmount() {
    clearInterval(this.timer);
  },
  //算出プロパティで表示するコンポーネント名を生成
  computed: {
    currentBanner() {
      return 'banner-' + this.banners[this.currentIndex];
    }
  },
  data() {
    return {
      //表示するバナーのコンポーネントのインデックス
      currentIndex : 0,
      //表示するバナーのコンポーネントのリスト(名前の後半部分)
      banners: ['foo', 'bar', 'baz']
    }
  }
});

//バナーのコンポーネント
app.component('banner-foo', {
  template: `<div class="banner">
    <h3>Foo</h3>
    <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
    </div>`
})
.component('banner-bar', {
  template: `<div class="banner">
    <h3>Bar</h3>
    <p>Nam voluptates qui ipsum architecto impedit! In magni.</p>
    </div>`
})
.component('banner-baz', {
  template: `<div class="banner">
    <h3>Baz</h3>
    <p>maxime accusamus nemo quibusdam dolor animi, veniam consequuntur et.</p>
    </div>`
});

app.mount('#app');

初期状態では以下のように出力されます。

3秒毎に is 属性の値(currentBanner)のコンポーネント名が変わり、class="banner" の div 要素(バナーのコンポーネント)が切り替わります。

<div id="app" data-v-app="">
  <div class="banner">
    <h3>Foo</h3>
    <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
  </div>
</div>

3秒後には以下のように切り替わります。

<div id="app" data-v-app="">
  <div class="banner">
    <h3>Bar</h3>
    <p>Nam voluptates qui ipsum architecto impedit! In magni.</p>
  </div>
</div>

タブパネルの例

以下では <component> 要素を使って、タブをクリックすると表示が切り替わるタブパネルを生成します。

前述の例同様、動的に切り替えるタブパネルのコンポーネントを component メソッドを使って用意しています(25〜42行目)。

data オプションでは、タブの名前のリスト(配列)と現在表示されているタブの名前を定義しています。

computed オプションで表示するコンポーネントの名前(tab- とタブの名前を小文字に変換)を生成しています。

methods オプションにはクリックイベントのリスナを設定し、引数で受け取るタブの名前を現在表示しているタブ currentTab に設定することで、computed オプションの表示するコンポーネントの名前(is 属性の値に指定する currentTabComponent)が更新されます。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      //現在表示しているタブ
      currentTab: 'Foo',
      //タブのリスト
      tabs: ['Foo', 'Bar', 'Baz']
    }
  },
  computed: {
    //表示するタブのコンポーネントの名前を生成
    currentTabComponent() {
      return 'tab-' + this.currentTab.toLowerCase()
    }
  },
  methods: {
    //クリック時にタブを切り替え
    onclick(tab) {
      this.currentTab = tab;
    }
  }
});

//タブパネルのコンポーネント
app.component('tab-foo', {
  template: `<div class="tab">
    <h3>Foo</h3>
    <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
    </div>`
})
.component('tab-bar', {
  template: `<div class="tab">
  <h3>Bar</h3>
  <p>Nam voluptates qui ipsum architecto impedit! In magni.</p>
  </div>`
})
.component('tab-baz', {
  template: `<div class="tab">
  <h3>Baz</h3>
  <p>maxime accusamus nemo quibusdam dolor animi, veniam consequuntur et.</p>
  </div>`
});

app.mount('#app');

タブは data オプションのタブのリスト(tabs)から v-for: で生成しています。生成したタブが currentTab と等しい場合は active クラスを追加します。

この例ではタブを li 要素と a 要素を使って作成し、a 要素をクリックするとイベントリスナー onclick(tab) が呼び出されてタブを切り替わるようにしています。

a 要素に v-on: で設定したイベントリスナーは、リンク先に移動しないようにイベント修飾子 .prevent でデフォルトの動作をキャンセルしています。

HTML
<div id="app">
  <!-- タブ -->
  <ul class="tab-menu">
    <li
      v-for="tab in tabs"
      v-bind:key="tab"
      v-bind:class="['tab-menu-item', { active: currentTab === tab }]"
    >
      <a href="#" v-on:click.prevent="onclick(tab)">
        {{ tab }}
      </a>
    </li>
  </ul>
  <!-- タブパネル -->
  <component v-bind:is="currentTabComponent" class="tab"></component>
</div>
CSS
.tab-menu {
  display: flex;
}
.tab-menu-item {
  list-style-type: none;
  line-height: 160%;
  width: 100px;
  height: 40px;
}

.tab-menu a {
  display: block;
  text-align: center;
  text-decoration: none;
  background-color: #cbf1e6;
  color: #000;
  border: solid 1px #ccc;
}

.tab-menu-item.active a {
  background-color: #cded94;
}

初期状態では以下のように出力され、リンクをクリックするとタブが切り替わります。

<div id="app" data-v-app="">
  <ul class="tab-menu">
    <li class="tab-menu-item active"><a href="#">Foo</a></li>
    <li class="tab-menu-item"><a href="#">Bar</a></li>
    <li class="tab-menu-item"><a href="#">Baz</a></li>
  </ul>
  <div class="tab">
    <h3>Foo</h3>
    <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
  </div>
</div>

keep-alive 要素

<component> 要素を使った動的なコンポーネントの切り替えでは、不要になったコンポーネントは破棄され、コンポーネントは再生成されます。

コンポーネントの状態を保持したり、パフォーマンスの理由から再レンダリングを避けたい場合は <keep-alive> 要素を利用することができます。

例えば、前述の例のタブパネルの1つに以下のようなラジオボタンを追加し、v-model で選択した値を表示するようにした場合、ラジオボタンを追加したタブでラジオボタンを選択後、他のタブを表示し、戻ってくると、選択された内容は消えてしまっています。

const app = Vue.createApp({
  data() {
    //省略(前述と同じ)
  },
  computed: {
    //省略(前述と同じ)
  },
  methods: {
    //省略(前述と同じ)
  }
});

//タブパネルのコンポーネント
app.component('tab-foo', {
  template: `<div class="tab">
    <h3>Foo</h3>
    <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
    <h4>Choice</h4>
    <label for="wine">Wine</label>
    <input type="radio" id="wine" value="wine" v-model="drink">
    <label for="beer">Beer</label>
    <input type="radio" id="beer" value="beer" v-model="drink">
    <label for="sake">Sake</label>
    <input type="radio" id="sake" value="sake" v-model="drink">
    <p>{{ drink }}</p>
    </div>`,
  data() {
    return {
      drink: ''
    }
  }
})
.component('tab-bar', {
  //省略(前述と同じ)
})
.component('tab-baz', {
  //省略(前述と同じ)
});

app.mount('#app');

以下のように動的コンポーネント(component 要素)を keep-alive 要素で囲むことで、コンポーネントはキャッシュされます。

この場合、ラジオボタンを追加したタブでラジオボタンを選択後、他のタブを表示し、戻って来ても選択された内容は維持され、消えません。

HTML
<div id="app">
  <ul class="tab-menu">
    <li v-for="tab in tabs" v-bind:key="tab" v-bind:class="['tab-menu-item', { active: currentTab === tab }]">
        <a href="#" v-on:click.prevent="onclick(tab)">
          {{ tab }}
        </a>
    </li>
  </ul>
  <!-- keep-alive 要素で component 要素を囲む -->
  <keep-alive>
    <component v-bind:is="currentTabComponent" class="tab"></component>
  </keep-alive>
</div>

<keep-alive> 要素の属性

<keep-alive> 要素では、コンポーネントのキャッシュ方法を制御するための以下のような属性を指定することができます。

属性 概要
max キャッシュするコンポーネントの最大数を指定。この数に達すると、新しいインスタンスを作成する前にキャッシュされたコンポーネントのうち最近アクセスされなかったキャッシュされたインスタンスが破棄されます。
include キャッシュすべきコンポーネントの名前を文字列(カンマ区切り)、配列、正規表現のいずれかで指定(配列、正規表現で指定する場合は v-bind を使用)。指定する名前は、name オプションで指定された名前(匿名のコンポーネントを照合することはできません)。
exclude キャッシュすべきでないコンポーネントの名前を文字列(カンマ区切り)、配列、正規表現のいずれかで指定(配列、正規表現で指定する場合は v-bind を使用)。指定する名前は、name オプションで指定された名前(匿名のコンポーネントを照合することはできません)。

include/exclude 属性

include/exclude 属性で指定する名前は、component メソッドの第1引数で指定した名前ではなく、name オプションで指定した名前になります。

app.component('tab-foo', {
  template: `<div class="tab">
    <h3>Foo</h3>
    <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
    <h4>Choice</h4>
    <label for="wine">Wine</label>
    <input type="radio" id="wine" value="wine" v-model="drink">
    <label for="beer">Beer</label>
    <input type="radio" id="beer" value="beer" v-model="drink">
    <label for="sake">Sake</label>
    <input type="radio" id="sake" value="sake" v-model="drink">
    <p>{{ drink }}</p>
    </div>`,
  data() {
    return {
      drink: ''
    }
  },
  //name オプションを設定
  name: 'foo'
})

以下は文字列で指定する場合の例です。

<keep-alive include="foo">
  <component v-bind:is="currentTabComponent" class="tab"></component>
</keep-alive>

配列や正規表現で指定する場合は include や exclude 属性に v-bind を使用する必要があります。

<keep-alive v-bind:include="['foo','bar']">
  <component v-bind:is="currentTabComponent" class="tab"></component>
</keep-alive>

<keep-alive> 要素のライフサイクルフック

<keep-alive> 要素の配下のコンポーネント(kept-alive コンポーネント)では activated 及び deactivated ライフサイクルフックを使用することができます。

  • activated フック:コンポーネントが待機状態からアクティブになった(表示される)ときに呼び出されます。
  • deactivated フック:コンポーネントがアクティブから待機状態になったときに呼び出されます。

前述のタブパネルの1つに以下のライフサイクルフックを追加すると、そのタブが表示されるときに「tab-baz がアクティブになりました。」、その他のタブが表示される(そのタブが待機状態になる)ときに「tab-baz が非アクティブになりました。」とコンソールに出力されます。

component('tab-baz', {
  template: `<div class="tab">
  <h3>Baz</h3>
  <p>maxime accusamus nemo quibusdam dolor animi, veniam consequuntur et.</p>
  </div>`,
  //activated フック
  activated() {
    console.log('tab-baz がアクティブになりました。')
  },
  //deactivated フック
  deactivated() {
    console.log('tab-baz が非アクティブになりました。')
  }
})

※ 但し、include や exclude 属性が指定されて、そのコンポーネントが対象外となっている場合は、このライフサイクルフックは呼び出されません。

Teleport

Teleport を使うと、コンポーネントのテンプレートの一部を、ページ内の任意の場所(DOM 内の別の場所)や Vue アプリの外へ移動(Teleport)させることができます。

Teleport を利用するには、移動対象の要素を <teleport> 要素で囲み、to 属性に移動先を指定します。

以下はモーダルを開くための button 要素とモーダルウィンドウ(内部にモーダルを閉じるための button 要素を持つ .modal クラスの div 要素)から成るコンポーネント(modal-button)の例です。

HTML
<div id="app" style="position: relative;">
  <h3>モーダルサンプル</h3>
  <div>
    <modal-button></modal-button>
  </div>
</div>
CSS
.modal {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  background-color: rgba(0,0,0,.5);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.modal div {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: white;
  width: 300px;
  height: 300px;
  padding: 5px;
}

データオブジェクトの modalOpen プロパティが true であれば、モーダルウィンドウを開き、false であればモーダルウィンドウを閉じます。

この例の場合、.modal の div 要素(モーダルウィンドウ)に position: absolute を指定していますが、絶対配置の基準は style="position: relative;" を指定した id="app" のマウント先の div 要素になります。

JavaScript
const app = Vue.createApp({});

//モーダルを開くための button 要素とモーダルウィンドウから成るコンポーネント
app.component('modal-button', {
  template: `
    <button v-on:click="openModal">
        モーダルウィンドウを開く
    </button>
    <div v-if="modalOpen" class="modal">
      <div>
        モーダルウィンドウです。
        <button v-on:click="closeModal">
          閉じる
        </button>
      </div>
    </div>
  `,
  data() {
    return {
      // モーダルウィンドウを開くかどうか
      modalOpen: false
    }
  },
  methods: {
    openModal() {
      this.modalOpen = true;
    },
    closeModal() {
      this.modalOpen = false;
    }
  }
});

app.mount('#app');

モーダルウィンドウ(.modal の div 要素)をフルスクリーン表示するには、body 要素を基準に絶対配置できればよいのですが、このような場合、<teleport> 要素でモーダルウィンドウの div 要素を囲み、その to 属性に body を指定することで簡単に実現できます(6行目と15行目)。

JavaScript
app.component('modal-button', {
  template: `
    <button v-on:click="openModal">
        モーダルウィンドウを開く
    </button>
    <teleport to="body">
      <div v-if="modalOpen" class="modal">
        <div>
          モーダルウィンドウです。
          <button v-on:click="closeModal">
            閉じる
          </button>
        </div>
      </div>
    </teleport>
  `,
  ・・・以下省略・・・
});

teleport 要素の to 属性

<teleport> 要素の to 属性には、移動先の要素のセレクタを指定します(teleport)。

<teleport to="#some-id" />
<teleport to=".some-class" />
<teleport to="[data-xxxx]" />

teleport 要素にコンポーネントを含む場合

<teleport> 要素の配下にコンポーネントを配置することもでき、その場合でも(Teleport による移動先に関わらず)、<teleport> の親の、論理的な子コンポーネントという関係は変わりません。

HTML
<div id="foo"></div>

<div id="app">
  <h1>Root instance</h1>
  <parent-component />
</div>

この例の場合、child-component は <teleport> 要素により、コンポーネントの外側に移動しますが、論理的には parent-component の子のままなので、name プロパティを受け取ります。

JavaScript
const app = Vue.createApp({});

app.component('parent-component', {
  template: `
    <div class="parent">
      <h2>This is a parent component</h2>
      <teleport to="#foo">
        <child-component name="Bar" />
      </teleport>
    </div>
  `
}).component('child-component', {
  props: ['name'],
  template: `
    <div class="child">Hello, {{ name }}</div>
  `
})

app.mount('#app');

以下のように出力されますが、Vue Devtools 上では実際のコンテンツが移動した場所に配置されるのではなく、親コンポーネントの下に配置されるのが確認できます。

<div id="foo">
  <div class="child">Hello, Bar</div>
</div>

<div id="app" data-v-app="">
  <h1>Root instance</h1>
  <div class="parent">
    <h2>This is a parent component</h2>
    <!--teleport start-->
    <!--teleport end-->
  </div>
</div>

アニメーション

Vue ではアニメーション効果を付けるのに利用できる <transition> や <transition-group> 要素が標準で用意されていますが、クラスのバインディング(条件付きクラス)と CSS アニメーションを使ってアニメーションを設定することもできます。

トランジションとアニメーション(概要)

以下はボタンをクリックすると、Hello とフェードインで表示する例です。ボタンをクリックすると v-on で showThis の値(真偽値)を反転させることで、v-bind:class に指定したクラス show の着脱をして CSS の transition アニメーションを実行します。

HTML
<div id="app">
  <button v-on:click="showThis = !showThis">Toggle</button>
  <div class="fade" v-bind:class="{ show: showThis }">Hello!</div>
</div>

JavaScript
const app = Vue.createApp({
  data() {
    return {
      showThis: false
    }
  }
});

app.mount('#app');

CSS
.fade {
  transition: opacity .5s ease;
  opacity: 0;
}
.fade.show {
  opacity: 1;
}

以下は、同様にクラスのバインディング (条件付きクラス)と CSS の animation を使ってボタンをクリックすると対象の要素を回転させるアニメーションの例です。

transition と異なり、animation の場合、奇数回のクリックでは、rotateThis の真偽値は false から true に反転して、rotate クラスが追加されて、アニメーションが実行されますが、偶数回のクリックでは、true から false に反転されるので、アニメーションが実行されません。

そのため、アニメーション対象の要素に v-on:animationend を指定して、アニメーションが終了したら、rotateThis の真偽値を false にすることで、毎回クリックするとアニメーションが実行されるようにしています。この場合、button 要素の v-on:click="rotateThis = !rotateThis" を v-on:click="rotateThis = true" としても同じです。

HTML
<div id="app">
  <button v-on:click="rotateThis = !rotateThis">Rotate</button>
  <p v-bind:class="{ rotate: rotateThis }" v-on:animationend="removeRotate">Hello!</p>
</div>
JavaScript
const app = Vue.createApp({
  data() {
    return {
      rotateThis: false
    }
  },
  methods: {
    removeRotate() {
      this.rotateThis = false;
    }
  }
});

app.mount('#app');
CSS
.rotate {
  animation: rotate720 .5s;
  width: 100px;;
}

@keyframes rotate720 {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(720deg);
  }
}
transition 要素

<transition> 要素は単一の要素やコンポーネントにアニメーションを適用する際に利用できる組み込みコンポーネントで、以下のような要素やコンポーネントにアニメーション(entering/leaving トランジション)を追加することを可能にします。

  • 条件付きレンダリング (v-if を使用)
  • 条件付きの表示 (v-show を利用)
  • 動的コンポーネント
  • コンポーネントルートノード (Component root nodes)

以下は前述のクラスのバインディング (条件付きクラス)と CSS transiton アニメーションを使った例を <transition> 要素を使って書き換えたものです。

アニメーションを有効にするには、対象の要素を transition タグで括ります。

※<transition> 要素の直下は単一の要素またはコンポーネントでなければなりません。

ボタンをクリックすると v-on で showThis の値(真偽値)を反転させ、対象の要素の v-if の値を操作します。

HTML
<div id="app">
  <button v-on:click="showThis = !showThis">Toggle</button>
  <transition> <!-- 対象の要素を transition タグで括る-->
    <div v-if="showThis">Hello!</div>
  </transition>
</div>
JavaScript
const app = Vue.createApp({
  data() {
    return {
      showThis: false
    }
  }
});

app.mount('#app');

<transition> 要素により付与される enter/leave トランジションクラスを使ってアニメーションを設定します。

CSS
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}

/*opacity の既定値は 1 なので以下は省略可能*/
.v-enter-to,
.v-leave-from {
  opacity: 1;
}

複数の要素を排他的に表示

<transition> 要素の直下は単一の要素またはコンポーネントでなければなりませんが、以下のように複数の要素を排他的に表示することはできます。

以下の場合は、v-if と v-else により両方の div 要素が同時に表示されることはなく、いずれかのみが表示されるので(一度だけレンダリングされる複数のノードの場合は)問題ありません。

v-if/v-else-if/v-else を使ったり、ひとつの要素に対して動的プロパティでバインディングを行う場合も同様です(要素間のトランジション)。

<div id="app">
  <button v-on:click="showThis = !showThis">Toggle</button>
  <transition mode="out-in">
    <div v-if="showThis">Hello!</div>
    <div v-else>Bye!</div>
  </transition>
</div>

mode="out-in" は両方の要素が同時にレンダリングされる(デフォルトの動作)を制御する mode 属性です。

関連項目:コンポーネント間のトランジション

トランジションクラス

<transition> 要素の直下の要素には Enter と Leave のそれぞれの状態に応じて以下のようなクラスが付与されます。

Enter 表示時のアニメーション opacity: 0 .v-enter-from Enter の開始状態 opacity: 1 .v-enter-to Enter の終了状態 .v-enter-active Enter トランジションのフェーズ中 Leave 非表示時のアニメーション opacity: 1 .v-leave-from Leave の開始状態 opacity: 0 .v-leave-to Leave の終了状態 .v-leave-active Leave トランジションのフェーズ中

トランジションクラスは、Enter(要素が追加や表示される時に付与)と Leave(要素が破棄または非表示される時に付与)に分類されます(Enter & Leave トランジション)。

クラス名(デフォルト) 概要
Enter v-enter-from enter の開始状態。その要素が挿入される前に適用され、その要素が挿入された 1 フレーム後に削除されます。
v-enter-active enter のアクティブ状態。トランジションに入るフェーズ中に適用されます。その要素が挿入される前に追加され、そのトランジション/アニメーションが終了すると削除されます。このクラスはトランジションの開始に対して、CSS transition や animation でアニメーションを設定(期間、遅延、およびイージングカーブを定義)するために使用できます。
v-enter-to enter の終了状態。その要素が挿入された 1 フレーム後に追加され (同時に v-enter-from が削除されます)、そのトランジション/アニメーションが終了すると削除されます。
Leave v-leave-from leave の開始状態。トランジションの終了がトリガされるとき、直ちに追加され、1 フレーム後に削除されます。
v-leave-active leave のアクティブ状態。トランジションが終わるフェーズ中に適用されます。leave トランジションがトリガされるとき、直ちに追加され、トランジション/アニメーションが終了すると削除されます。このクラスはトランジションの終了に対して、CSS transition や animation でアニメーションを設定(期間、遅延、およびイージングカーブを定義)するために使用できます。
v-leave-to leave の終了状態。leave トランジションがトリガされた 1 フレーム後に追加され (同時に v-leave-from が削除されます)、トランジション/アニメーションが終了すると削除されます。

上記表に記載されている先頭に v- が付くクラス名は、transition 要素に name 属性の指定がない場合のデフォルトのクラス名です。

transition 要素 name 属性

transition 要素に name 属性が指定している場合は、デフォルトのトランジションクラスのクラス名の v の代わりに name 属性の値を指定します。

name 属性を指定することでそれぞれの transition 要素を識別することができ、同一のページやコンポーネントに transition 要素を複数配置することができます。

例えば、<transition name="foo"> の場合、v-enter-from ではなく、foo-enter-from となります。

以下はボタンをクリックするとパネルがスライドアップ・ダウンする CSS トランジションを使った例で、transition 要素に name 属性を指定しています。

<div id="app">
  <button v-on:click="open = !open">Toggle</button>
  <transition name="toggle-panel">
    <div class="panel" v-if="open">
      <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.・・・</p>
    </div>
  </transition>
</div>
JavaScript
const app = Vue.createApp({
  data() {
    return {
      open: true
    }
  }
});

app.mount('#app');

この例では <transition> 要素に name 属性を指定しているので、トランジションクラスの名前は v- から始まるのではなく、name 属性に指定した値(toggle-panel)から始まります。

CSS
.panel {
  width: 300px;
  border: 1px solid #999;
  padding: 20px;
  overflow: hidden;
}

.toggle-panel-enter-active,
.toggle-panel-leave-active {
  transition: height 0.6s ease-in-out;
}

.toggle-panel-enter-from,
.toggle-panel-leave-to {
  height: 0px;
}

.toggle-panel-enter-to,
.toggle-panel-leave-from {
  height: 300px;
}

CSS アニメーション(animation)

CSS アニメーション(animation)は、CSS トランジション(transition)と同じように適用されますが、v-enter-from が要素が挿入された直後に削除されない点が異なります(animationend イベント時には削除されています)。

以下はボタンをクリックすると、transform: scale() を使ったキーフレームで拡大・縮小表示する CSS アニメーションの例です。

HTML
<div id="app">
  <button v-on:click="show = !show">Toggle</button>
  <transition name="bounce">
    <p v-if="show">Lorem ipsum dolor sit amet consectetur adipisicing elit.・・・</p>
  </transition>
</div>
JavaScript
const app = Vue.createApp({
  data() {
    return {
      show: true
    }
  }
});

app.mount('#app');

CSS アニメーションでは、キーフレームを定義し、v-enter-active と v-leave-active クラスで定義したキーフレームを animation で指定します。

この例の場合、v-leave-active(要素が非表示される際のアニメーションの設定)で reverse を指定してアニメーションの方向を逆方向に再生させることで見えなくなるまで縮小しています。

.bounce-enter-active {
  animation: bounce-in 0.5s;
}
.bounce-leave-active {
  /* reverse で逆方向に再生 */
  animation: bounce-in 0.5s reverse;
}

@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.25);
  }
  100% {
    transform: scale(1);
  }
}
transition 要素の属性

<transition> 要素には name 属性の他にも以下のような属性を指定(バインド)することができます(一部抜粋)。

属性 概要 値の型
appear 初回レンダリング時にトランジションを適用するかどうか。デフォルトは false。 boolean
css CSSのトランジションクラスを適用するかどうか。デフォルトは true。false に設定すると、コンポーネントイベントを通じて登録された JavaScript フックのみをトリガーします。 boolean
type トランジション終了タイミングを決定するために待機するトランジションイベントの種類を指定する。指定できる値は、"transition "と "animation "です。デフォルトでは、継続時間の長いタイプを自動的に検出します。 string
mode leaving/entering のトランジションタイミングを制御します。"out-in" または "in-out" を指定(デフォルトは simultaneous) string
duration トランジションの継続時間を指定します。デフォルトではルート transition 要素上の最初の transitionend または animationend イベントを待ちます(自動的に終了を検出)。 number|object
初期レンダリング時のトランジション(appear 属性)

<transition> 要素は、デフォルトでは表示や非表示の切替のタイミングでのみアニメーションを実行します。

初期レンダリング時(ページの初回表示の際)にアニメーションを実行させるには、<transition> 要素に appear 属性を指定します。

HTML
<div id="app">
  <button v-on:click="showThis = !showThis">Toggle</button>
  <transition appear><!-- appear 属性を指定 -->
    <div v-if="showThis">Hello!</div>
  </transition>
</div>
CSS
.v-enter-active,
.v-leave-active {
  transition: opacity 1s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}
JavaScript
const app = Vue.createApp({
  data() {
    return {
      showThis: true,
    }
  }
});

app.mount('#app');
明示的なトランジション期間の設定(duration 属性)

Vue は自動的にアニメーションが終了したことを検出することは可能ですが、duration 属性を使って明示的にアニメーションにかかる時間(ミリ秒単位)を指定することが可能です。

<transition v-bind:duration="1000">...</transition>

Enter と Leave の期間を、以下のように個別に指定することも可能です。

<transition v-bind:duration="{ enter: 500, leave: 800 }">...</transition>
トランジションモード(mode 属性)

例えば、以下のような "on" ボタンと "off" ボタン間でトランジションを行うとき、片方のボタンが非表示になり(トランジションアウトして)、別の片方が表示される(トランジションインする)とき、両方のボタンがレンダリングされてしまいます。

これは、<transition> 要素のデフォルトの振る舞いで、enter と leave は同時(simultaneous)に起きます。

HTML
<div id="app">
  <transition>
    <button v-if="on" key="on" v-on:click="on = false">
      on
    </button>
    <button v-else key="off" v-on:click="on = true">
      off
    </button>
  </transition>
</div>
CSS
.v-enter-active,
.v-leave-active {
  transition: opacity 1s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}
JavaScript
const app = Vue.createApp({
  data() {
    return {
      on: false,
    }
  }
}).mount('#app');

この例の場合は、ボタン要素を CSS で絶対配置(position:absolute)にすれば問題ありませんが、切り替える要素を同時にトランジションさせたくない場合もあります。

排他的な要素の切り替えでの enter と leave が同時に発生するデフォルトの振る舞いを mode 属性を使って制御することができます。mode 属性には以下の値を指定することができます。

mode 属性の値
概要
in-out 最初に新しい要素がトランジションインして、それが完了したら、現在の要素がトランジションアウトする(Enter の後に Leave を実施)
out-in 最初に現在の要素がトランジションアウトして、それが完了したら、新しい要素がトランジションインする(Leave の後に Enter を実施。こちらを使うこと多い)

以下は out-in を使って、前述の on/off ボタンを書き換えた例です(CSS と JavaScript は同じ)。この場合、現在表示されているボタンが非表示になるのを待ってから、もう一方のボタンが表示されます。

HTML
<div id="app">
  <transition mode="out-in">
    <button v-if="on" key="on" v-on:click="on = false">
      on
    </button>
    <button v-else key="off" v-on:click="on = true">
      off
    </button>
  </transition>
</div>
カスタムトランジションクラス

以下の属性を <transition> 要素に指定することで、デフォルトのトランジションクラスのクラス名の規約を上書きし、カスタムトランジションクラスを設定することができます。

Animate.css のような既存の CSS アニメーションライブラリを利用する場合などに便利です。

属性 既定のクラス名
enter-from-class v-enter-from
enter-active-class v-enter-active
enter-to-class v-enter-to
leave-from-class v-leave-from
leave-active-class v-leave-active
leave-to-class v-leave-to
appear-class
appear-active-class
appear-to-class

以下は CSS アニメーションライブラリ Animate.css を利用する例です。この例では Animate.css を link 要素で CDN 経由で読み込んでいます。

Animate.css を使ってアニメーションを実装するには、以下のように必須のクラス animate__animated と任意のアニメーションのクラス(以下の場合は animate__bounce)を要素に追加します。

Animate.css の基本的な使い方
<h1 class="animate__animated animate__bounce">An animated element</h1>

以下では、ボタンをクリックして表示する際に animate__tada のアニメーションを、非表示にする際に animate__bounceOutRight のアニメーションを適用するように、カスタムトランジションクラスの属性を設定しています。

具体的には、enter-active-class 属性に animate__animated と animate__tada クラスを、leave-active-class 属性に animate__animated と animate__bounceOutRight クラスを指定しています。

HTML
<!-- Animate.css の読み込み -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" />

<div id="app">
  <button v-on:click="show = !show">{{ show ? '非表示': '表示' }}</button>
  <!-- animate__animated は Animate.css の必須のクラス -->
  <transition
    name="custom-classes-transition"
    enter-active-class="animate__animated animate__tada"
    leave-active-class="animate__animated animate__bounceOutRight"
  >
    <p v-if="show">Lorem ipsum dolor sit amet consectetur adipisicing elit. ・・・</p>
  </transition>
</div>
JavaScript
const app = Vue.createApp({
  data() {
    return {
      show: true,
    }
  }
});

app.mount('#app');
JavaScript フック

<transition> 要素に用意されている以下のイベントを利用して、JavaScript でアニメーションを制御することもできます。

これらのイベントを通常のイベント同様、v-on: で <transition> 要素に指定して登録し、イベントリスナ(JavaScript フック)を定義することができます。

イベント 発生するタイミング
before-enter 要素が挿入される前
before-leave 要素が非表示になる前
enter 要素の挿入後、アニメーション開始前
leave before-leave の後で、非表示のアニメーション開始前
appear 要素の初回レンダリング時
after-enter 要素が挿入された後
after-leave 要素を非表示にした後
after-appear 要素の初回レンダリング後
enter-cancelled 要素の挿入をキャンセルした時
leave-cancelled 要素の非表示をキャンセルした時(v-show only)
appear-cancelled 要素の初回レンダリングをキャンセルした時

これらのフックは、CSS トランジション/アニメーション、または別のライブラリなどと組み合わせて使うことができます。

methods: {
  // --------
  // ENTERING
  // --------

  beforeEnter(el) {
    // ...
  },
  // CSS と組み合わせて使う時、done コールバックはオプションです
  enter(el, done) {
    // ...
    done() //done コールバックでトランジションの終了を通知
  },
  afterEnter(el) {
    // ...
  },
  enterCancelled(el) {
    // ...
  },

  // --------
  // LEAVING
  // --------

  beforeLeave(el) {
    // ...
  },
  // CSS と組み合わせて使う時、done コールバックはオプションです
  leave(el, done) {
    // ...
    done() //done コールバックでトランジションの終了を通知
  },
  afterLeave(el) {
    // ...
  },
  // v-show と共に使うときだけ leaveCancelled は有効です
  leaveCancelled(el) {
    // ...
  }
}

JavaScript のみを利用したトランジションの場合は、done コールバックを enter と leave フックで呼ぶ必要があります。done はフックによるトランジションの終了を通知するための関数で、呼ばない場合は、フックは同期的に呼ばれ、トランジションはただちに終了します。

また、:css="false" を追加することで、CSS の検出をスキップすることを Vue に伝え、CSS アニメーションを無効化することができます(JavaScript のみでアニメーションを制御する場合)。

以下は GreenSock を使った JavaScript トランジションの例です。

HTML
<!-- GreenSock の読み込み-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.10.4/gsap.min.js"></script>

<div id="app">
  <button v-on:click="show = !show">
    Toggle
  </button>

  <transition
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:leave="leave"
    :css="false"
  >
    <p v-if="show" class="box"></p>
  </transition>
</div>

この例では、GreenSock の onComplete オプションで、done コールバックがトランジション完了時に呼び出されるようにしています。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      show: false
    }
  },
  methods: {
    //<transition> 要素に v-on: で登録した JavaScript フックの定義
    beforeEnter(el) {
      gsap.set(el, {
        scaleX: 0.8,
        scaleY: 1.2
      })
    },
    enter(el, done) {
      gsap.to(el, {
        duration: 1,
        scaleX: 1.5,
        scaleY: 0.7,
        opacity: 1,
        x: 150,
        ease: 'elastic.inOut(2.5, 1)',
        onComplete: done //done コールバック
      })
    },
    leave(el, done) {
      gsap.to(el, {
        duration: 0.7,
        scaleX: 1,
        scaleY: 1,
        x: 300,
        ease: 'elastic.inOut(2.5, 1)'
      })
      gsap.to(el, {
        duration: 0.2,
        delay: 0.5,
        opacity: 0,
        onComplete: done //done コールバック
      })
    }
  }
}).mount('#app');
CSS
.box {
  width: 50px;
  height: 30px;
  background: darkslateblue;
  margin-top: 20px;
}
コンポーネント間のトランジション

コンポーネント間のトランジションは、動的コンポーネントでラップするだけです。

以下はラジオボタンで選択された値のコンポーネントをアニメーションで表示する例です。

表示するコンポーネントは components オプションでローカル登録し、component 要素で囲み、is 属性にラジオボタンで選択された値(v-model の値)を指定しています。

また、表示するコンポーネントが同時にレンダリングされないように mode 属性に "out-in" を指定しています。

HTML
<div id="app">
  <input v-model="choice" type="radio" value="compo-a" id="a"><label for="a">A</label>
  <input v-model="choice" type="radio" value="compo-b" id="b"><label for="b">B</label>
  <transition name="fade" mode="out-in">
    <component v-bind:is="choice"></component>
  </transition>
</div>
const app = Vue.createApp({
  data() {
    return {
      //v-model の初期値
      choice: 'compo-a'
    }
  },
  components: {
    //表示するコンポーネントを components オプションでローカル登録
    'compo-a': {
      template: '<div>Component A</div>'
    },
    'compo-b': {
      template: '<div>Component B</div>'
    }
  }
}).mount('#app');
CSS
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
リストのトランジション transition-group

<transition> 要素では、以下のような単一の要素やコンポーネントにアニメーションを適用することができます。

  • 個別のノード
  • 一度だけレンダリングされる複数のノード(排他的に表示・切り替える複数のノード)

v-for のような複数のリストに対してアニメーションを適用するには <transition-group> 要素(コンポーネント)を利用します。

<transition-group> を利用することで、リストへの追加や削除、移動に伴ってアニメーションを適用することができます。

リストのトランジション

<transition-group> は <transition> 要素同様、組み込みコンポーネントで、以下のような特徴があります。

  • デフォルトでは、ラッパー要素(transition-group)はレンダリングされませんが、 tag 属性でレンダリングする要素を指定することで、タグを出力することができます。
  • 排他的に要素が切り替わっているわけではないため、トランジションモードは利用できません。
  • 要素の内部では、常に固有の key 属性を持つ必要があります。
  • CSS トランジションクラスは内部の要素に適用され、グループやコンテナには適用されません。

以下はテキストボックスに入力された項目を追加・削除することができる単純な Todo リストの例です。

リストを transition-group 要素で囲み、tag 属性に ul を指定して、ul タグを出力するようにしています。

配下の要素(この場合は li 要素)には、key 属性を指定します。

HTML
<div id="app">
  <div id="list">
    <label for="todo">Todo:</label>
    <input id="todo" type="text" size="40" v-model.trim="todo">
    <button v-on:click="add">追加</button>
    <button v-on:click="remove">削除</button>
    <!-- v-for のリストを transition-group 要素で囲みアニメーションを適用  -->
    <transition-group name="list" tag="ul">
      <li v-for="item in items" v-bind:key="item">{{ item }}</li>
    </transition-group>
  </div>
</div>

この例では、予めサンプルの項目を用意しています。テキストボックスに値を入力して「追加」をクリックすると add() により、入力された値がリストに追加されます。

テキストボックスに値を入力して「削除」をクリックすると、remove() によりその値に該当する項目が削除されます。

JavaScript
const app = Vue.createApp({
  data() {
    return {
      todo: '',
      items: [
      '猫の餌やり',
      '買い物',
      '洗濯',
      '草むしり'
      ]
    };
  },
  methods: {
    add() {
      this.items.unshift(this.todo);
      this.todo = '';
    },
    remove() {
      this.items = this.items.filter(value => value !== this.todo);
      this.todo = '';
    }
  }
});

app.mount('#app');

CSS で enter/leave トランジションクラスを使ってアニメーションを設定します。

CSS
.list-enter-active, .list-leave-active{
  transition: transform 1s, opacity .7s;
}

.list-enter-from, .list-leave-to {
  transform: translateX(50%);
  opacity: 0;
}

初期状態では以下のように出力され、項目を追加・削除する際には、項目がスライドとフェードのアニメーションで表示されます。

<div id="app" data-v-app="">
  <div id="list">
    <label for="todo">Todo:</label>
    <input id="todo" type="text" size="40">
    <button>追加</button>
    <button>削除</button>
    <ul>
      <li>猫の餌やり</li>
      <li>買い物</li>
      <li>洗濯</li>
      <li>草むしり</li>
    </ul>
  </div>
</div>

前述の例では、key 属性に項目の値を指定しているため、同じ名前(値)の項目を入力することはできません。以下は key 属性を別途設定する例です。

また、以下では項目とその id を表示し、項目または id を入力して削除できるようにしています。

HTML
<div id="app">
  <div id="list">
    <label for="todo">Todo:</label>
    <input id="todo" type="text" size="40" v-model.trim="todo">
    <button v-on:click="add">追加</button>
    <button v-on:click="remove">削除</button>
    <transition-group name="list" tag="ul">
      <li v-for="item in items" v-bind:key="item.id">
        {{ item.todo + ' (' + item.id + ')'}}
      </li>
    </transition-group>
  </div>
</div>

v-for で分割代入を使えば、8〜10行目は以下のように記述できます。

HTML
<li v-for="{todo, id} in items" v-bind:key="id">
  {{ todo + ' (' + id + ')'}}
</li>
JavaScript
cconst app = Vue.createApp({
  data() {
    return {
      todo: '',
      items: [
        {id:0 , todo: 'sample todo item'}
      ],
      nextId: 1  //key 属性に使用する値(項目を追加するごとに増加)
    };
  },
  methods: {
    add() {
      this.items.unshift({id: this.nextId, todo: this.todo});
      this.nextId ++; //key 属性に使用する値を増加
      this.todo = '';
    },
    remove() {
      this.items = this.items.filter(item => {
        if(item.todo !== this.todo && item.id !== parseInt(this.todo)) {
          return item;
        }
      });
      this.todo = '';
    }
  }
});

app.mount('#app');
v-move クラス

<transition-group> は enter/leave だけでなく、位置の変更(移動)もアニメーションできます。<transition-group> では要素が位置を変更するときに v-move クラスが追加されます

その他のトランジションクラスと同様、v-move クラスの接頭辞は <transition-group> の name 属性と一致します(name 属性を指定していない場合は v-move)。また、move-class 属性で手動でクラスを指定できます。

List Move Transitions

前述の例の場合、追加・削除する項目はアニメーションしますが、既存の項目はカクっとしてしまいます。

既存の項目の移動に対して v-move クラスを使ってアニメーションを適用することで動きがスムーズになります。

前述の例の場合、transition-group の name 属性に list を指定しているので、この場合、.list-move を追加します。

v-move クラス(この場合、.list-move)は、リスト項目が移動対象になった時に付与されるので、このクラスにも transition プロパティを設定することで動きがスムーズになります。

※但し、削除時の要素の移動がアニメーションしないので、削除対象の要素(.list-leave-active)に絶対配置を指定します。

CSS
/* .list-move を追加 */
.list-enter-active, .list-leave-active, .list-move {
  transition: transform 1s, opacity .7s;
}

.list-enter-from, .list-leave-to {
  transform: translateX(50%);
  opacity: 0;
}

/* .list-leave-active に絶対配置を指定  */
.list-leave-active {
  position: absolute;
}

ソート時のアニメーション

v-move クラスを利用することで、リストをソートする際にもアニメーションを適用することができます。

以下は前述の例に、ソートボタンを追加して、項目の内容をソートできるようにした例です。

HTML
<div id="app">
  <div id="list">
    <label for="todo">Todo:</label>
    <input id="todo" type="text" size="40" v-model.trim="todo">
    <button v-on:click="add">追加</button>
    <button v-on:click="remove">削除</button>
    <button v-on:click="sort">ソート</button> <!-- ソートボタンを追加 -->
    <transition-group name="list" tag="ul">
      <li v-for="{todo, id} in items" v-bind:key="id">
        {{ todo + ' (' + id + ')'}}
      </li>
    </transition-group>
  </div>
</div>
JavaScript
const app = Vue.createApp({
  data() {
    return {
      todo: '',
      items: [
        {id:0 , todo: 'sample todo item'}
      ],
      nextId: 1
    };
  },
  methods: {
    add() {
      this.items.unshift({id: this.nextId, todo: this.todo});
      this.nextId ++;
      this.todo = '';
    },
    remove() {
      this.items = this.items.filter(item => {
        if(item.todo !== this.todo && item.id !== parseInt(this.todo)) {
          return item;
        }
      });
      this.todo = '';
    },
    //ソート用のメソッドを追加
    sort() {
      {
      this.items = this.items.sort((a,b) => {
        if(a.todo === b.todo){
          return 0;
        }
        if(typeof a.todo === typeof b.todo){
          return a.todo < b.todo ? -1 : 1;
        }
        return typeof a.todo < typeof b.todo ? -1 : 1;
      })
    }

    }
  }
});

app.mount('#app');

すでに前述の例で v-move クラス(この場合、.list-move)を設定しているので、CSS は同じです。

CSS
/* 前述の例と同じ */
.list-enter-active, .list-leave-active, .list-move {
  transition: transform 1s, opacity .7s;
}

.list-enter-from, .list-leave-to {
  transform: translateX(50%);
  opacity: 0;
}

/* .list-leave-active に絶対配置を指定  */
.list-leave-active {
  position: absolute;
}

エラー処理

Vue ではアプリ(コンポーネント)で発生したエラーを処理するために以下の方法を提供しています。

方法 概要
errorHandler コンポーネントの Render 関数とウォッチャに捕捉されなかったエラーのハンドラを割り当てます(グローバルなエラーハンドラー)。
errorCaptured 子孫コンポーネントからエラーが捕捉されたときに呼び出されるライフサイクルフック

errorHandler

errorHandler は config オブジェクトのプロパティの1つで、errorHandler を設定すると、個々のコンポーネントで発生して処理されなかった例外を最終的に処理することができます。

config オブジェクト

すべてのアプリケーションのインスタンスは、そのアプリケーションの設定を含む config オブジェクトを公開しています。config オブジェクトを使ってアプリケーションをマウントする前に、そのプロパティを変更(設定)することができます。

JavaScript
const app = Vue.createApp({});
//config オブジェクトをンソールへ出力
console.log(app.config);

errorHandler は初期状態では errorHandler で定義されていません。

上記によるコンソールへの出力
compilerOptions: {}
errorHandler: undefined  //未定義
globalProperties: {}
isNativeTag: (tag) => isHTMLTag(tag) || isSVGTag(tag)
optionMergeStrategies: {}
performance: false
warnHandler: undefined
[[Prototype]]: Object

以下は親子関係にあるコンポーネントを定義して、子コンポーネントの mounted() ライフサイクルフックでエラーを発生させて、そのエラーを errorHandler でコンソールに出力する例です。

HTML
<div id="app">
  <parent-div />
</div>

アプリの設定(プロパティ)は「app.config.プロパティ名 = 値」のように設定することができます。

JavaScript
const app = Vue.createApp({})
.component('parent-div', {
  template: `
  <div class="parent">
    <child-div />
  </div>`
})
.component('child-div', {
  mounted() {
    //故意にエラーを発生
    throw new Error('Error thrown by child-iv');
  },
  template: `
  <div class="child">
    ChildDiv
  </div>`
});

//アプリケーションをマウントする前に errorHandler を設定
app.config.errorHandler = (error, vm, info) => {
  console.log(error);
  console.log(vm);
  console.log(info);
  //実際にはエラーを捕捉した際の何らかの処理
}

app.mount('#app');

errorHandler にはハンドラー(関数)を渡します。ハンドラーは以下の引数を受け取ります。

  • error:エラーの情報
  • vm:アプリのインスタンス
  • info:Vue 固有のエラーの情報(例: ライフサイクルフック)
 

上記の場合、設定した errorHandler により、コンソールには例えば以下のように出力されます。

//console.log(error) による出力
Error: Error thrown by child-iv
    at Proxy.mounted (sample.html:96:11)
    at callWithErrorHandling (vue.global.js:1733:24)
    at callWithAsyncErrorHandling (vue.global.js:1742:23)
    at hook.__weh.hook.__weh (vue.global.js:4226:31)
    at flushPostFlushCbs (vue.global.js:1930:49)
    at render (vue.global.js:7682:11)
    at mount (vue.global.js:5926:27)
    at app.mount (vue.global.js:10819:25)
    at sample.html:110:5

//console.log(vm) による出力
Proxy {…}
  [[Handler]]: Objectd
  [[Target]]: Object
  [[IsRevoked]]: false

//console.log(info) による出力
mounted hook
  

errorHandler で捕捉できない例外

Vue のアプリの配下で発生した例外の殆どを errorHandler で捕捉することができますが、setTimeout/setInterval や promise で発生した例外は捕捉できません。そのため、それらの例外を捕捉するには JavaScript の addEventListener() を使います。

setTimeout/setInterval で発生した例外は、error イベントで、promise で発生した例外は unhandledrejection イベントで捕捉します。

const app = Vue.createApp({})
.component('parent-div', {
  template: `
  <div class="parent">
    <child-div />
  </div>`
})
.component('child-div', {
  mounted() {
    setTimeout(() => {
      //setTimeout() でエラーを発生
      throw new Error('Error by setTimeout');
    }, 1000);

    new Promise((resolve, reject) => {
      //Promiset() でエラーを発生
      reject(new Error('Error by Promise'));
    });

  },
  template: `
  <div class="child">
    ChildDiv
  </div>`
});

window.addEventListener('error', (e) => {
  //例外の発生を抑止
  e.preventDefault();
  console.log('Error Listener');
  console.log(e);
});

window.addEventListener('unhandledrejection', (e) => {
  //例外の発生を抑止
  e.preventDefault();
  console.log('unhandledrejection Listener');
  console.log(e);
})

app.mount('#app');

errorCaptured

コンポーネント単位で例外を捕捉する場合は errorCaptured を利用します。

errorCaptured は子孫コンポーネントからエラーが捕捉されたときに呼び出されるライフサイクルフックです。

HTML
<div id="app">
  <parent-div />
</div>

以下は親コンポーネントに errorCaptured を設定する例です。また、errorHandler も設定しています。

const app = Vue.createApp({})
.component('parent-div', {
  template: `
  <div class="parent">
    <child-div />
  </div>`,

  //コンポーネントのエラーを捕捉(ライフサイクルフック)
  errorCaptured(error, instance, info) {
    console.log('errorCaptured hook');
    console.log(error);
    console.log(instance);
    console.log(info);
  }
})
.component('child-div', {
  mounted() {
    throw new Error('Error thrown by child-iv');
  },
  template: `
  <div class="child">
    ChildDiv
  </div>`
});

//errorHandler(グローバルなエラーハンドラ)
app.config.errorHandler = (error, vm, info) => {
  console.log('errorHandler Global');
  console.log(error);
  console.log(vm);
  console.log(info);
}

app.mount('#app');

上記の場合、コンソールには以下のように出力されます。errorCaptured で処理された例外は、グローバルな errorHandler でも処理されます。

errorCaptured hook
Error: Error thrown by child-iv
    at Proxy.mounted (sample.html:104:11)
    at callWithErrorHandling (vue.global.js:1733:24)
    at callWithAsyncErrorHandling (vue.global.js:1742:23)
    at hook.__weh.hook.__weh (vue.global.js:4226:31)
    at flushPostFlushCbs (vue.global.js:1930:49)
    at render (vue.global.js:7682:11)
    at mount (vue.global.js:5926:27)
    at app.mount (vue.global.js:10819:25)
    at sample.html:120:5
Proxy {…}
mounted hook

//errorHandler による出力
errorHandler Global
Error: Error thrown by child-iv
    at Proxy.mounted (sample.html:104:11)
    at callWithErrorHandling (vue.global.js:1733:24)
    at callWithAsyncErrorHandling (vue.global.js:1742:23)
    at hook.__weh.hook.__weh (vue.global.js:4226:31)
    at flushPostFlushCbs (vue.global.js:1930:49)
    at render (vue.global.js:7682:11)
    at mount (vue.global.js:5926:27)
    at app.mount (vue.global.js:10819:25)
    at sample.html:120:5
Proxy {…}
mounted hook

もし、errorCaptured で処理された例外を、errorHandler で処理したくない場合は、errorCaptured で false を return することで上位のコンポーネントへのエラーの伝播を抑止することができます。

errorCaptured(error, instance, info) {
  //エラー処理
  return false;
}

エラー伝播のルール

あるコンポーネントで発生した例外は、上位のコンポーネントへ伝播します。そのため、コンポーネントツリーに複数の errorCaptured が存在する場合は順番に実行され、それらすべては同じエラーで呼び出されます。

もし errorCaptured フック自身がエラーを投げると、このエラーと元のキャプチャされたエラーの両方がグローバルの config.errorHandler に送られます。

errorCaptured フックは、エラーがさらに伝播するのを防ぐために false を返すことができます。

エラー伝播のルール(errorCaptured フック)

Vue でカスタム要素を使う

Vue で JavaScript 標準のカスタム要素(Custom Elements)を使ったり、Vue の提供するメソッドを使ってカスタム要素を定義することなどができます。

Vue と Web コンポーネント

以下は JavaScript 標準の Web コンポーネントのカスタム要素で、<my-hello-greeting> というカスタム要素を作成する例です(関連ページ:Web Components の使い方)。

<my-hello-greeting> は name 属性が指定されていれば、その値を使って「Hello, xxxx」と出力し、name 属性が指定されていなければ「Hello, World」と出力します。また、出力された文字列をクリックすると、文字色を赤に変更します。

JavaScript
//カスタム要素(クラス)の定義
class MyHelloGreeting extends HTMLElement {
  //コンストラクター
  constructor() {
    super();
    //カスタム要素 <my-hello-greeting> に空のシャドウ DOM を取り付ける
    const shadow = this.attachShadow({mode: 'open'});

    //変数 helloTo の初期化
    let helloTo = 'World';
    // name 属性が設定されていれば
    if(this.hasAttribute('name')) {
      // その値を変数 helloTo に代入
      helloTo = this.getAttribute('name');
    }

    // p 要素の生成
    const p = document.createElement('p');
    // p 要素の class 属性を設定
    p.setAttribute('class', 'hello');
    // p 要素のテキストの生成
    p.textContent = `Hello, ${helloTo}`;
    // p 要素にクリックイベントを設定
    p.addEventListener('click', (e) => {
      e.currentTarget.style.setProperty('color', 'red');
    });

    // style 要素(CSS)の生成
    const style = document.createElement('style');
    // CSS を生成
    style.textContent = `
      p {
        font-size: 18px;
        font-weight: bold;
        color: green;
        cursor: pointer;
      }
    `;
    // 生成した要素をシャドウルート(シャドウ DOM)に追加
    shadow.appendChild(style);
    shadow.appendChild(p);
  }
};
//カスタム要素を登録
customElements.define('my-hello-greeting', MyHelloGreeting);

Vue を使わずに、上記のカスタム要素を呼び出すには、以下をドキュメントに記述します。

<my-hello-greeting name="foo"></my-hello-greeting>

Chrome の開発者ツールで確認すると以下のように表示されます。

Vue アプリから呼び出す

前述の JavaScript 標準のカスタム要素の定義と登録を使って、Vue アプリから呼び出すことができます。

但し、Vue はネイティブではない HTML タグを、登録された Vue コンポーネントとして解決しようとするので、コンパイラーオプションを使って特定の要素をカスタム要素として扱うように Vue に伝える必要があります。

具体的には、compilerOptions(ランタイムコンパイラのオプション)に isCustomElement プロパティを設定します。startsWith() は文字列が引数で指定された文字列で始まる場合は true を返す String のメソッドです。

JavaScript
const app = Vue.createApp({});
//コンパイラーオプション(my- で始まるタグはすべてカスタム要素として扱う)
app.config.compilerOptions.isCustomElement = tag => tag.startWith('my-')

class MyHelloGreeting extends HTMLElement {

  //前述のコードと同じ(省略)

};
//カスタム要素を登録
customElements.define('my-hello-greeting', MyHelloGreeting);
HTML
<div id="app">
  <my-hello-greeting name="foo"></my-hello-greeting>
</div>

Vue CLI を利用している場合

Vue CLI などのビルドセットアップによって Vue が使われている場合は、そのオプションはビルド設定経由で渡される必要があります。

Vue CLI 設定の例
// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => ({
        ...options,
        compilerOptions: {
          // my- で始まるタグはすべてカスタム要素として扱う
          isCustomElement: tag => tag.startsWith('my-')
        }
      }))
  }
}

Vue のメソッドによるカスタム要素の定義

Vue の提供する defineCustomElement メソッドを使って Vue コンポーネント API を使ったカスタム要素を作成することができます(Vue コンポーネントと同じ要領でカスタム要素を定義できます)。

defineCustomElement メソッドは HTMLElement を拡張したカスタム要素を返します。

以下は前述の JavaScript 標準のカスタム要素の定義を Vue の defineCustomElement メソッドを使って書き換えた例です(コンパイラーオプションなどは省略)。

JavaScript
//カスタム要素を defineCustomElement メソッドを使って定義
const MyHelloGreeting = Vue.defineCustomElement({
  //data プロパティ
  data() {
    return {
      helloTo : this.name ? this.name : 'World',
      myStyle: {
        fontSize: '18px',
        fontWeight: 'bold',
        color: 'green',
        cursor: 'pointer'
      }
    }
  },
  //props オプション(属性)
  props: {name: String},
  //クリック時のイベントリスナー
  methods: {
    onclick() {
      this.myStyle.color = 'red';
    }
  },
  //テンプレート
  template: `<p class="hello" v-bind:name="helloTo" v-bind:style="myStyle" v-on:click="onclick">
      Hello, {{ helloTo }}
    </p>`
})

//上記で定義したカスタム要素を登録(JavaScript の関数)
customElements.define('my-hello-greeting', MyHelloGreeting);

Chrome の開発者ツールで確認すると以下のように表示されます(HTML は前述の例と同じ)。

x-template

今までの例のテンプレートでは template オプションにバッククォート(`)で文字列を囲むテンプレートリテラルで文字列(HTML)を記述して定義していますが、x-template を使うと、.html ファイルの <script> 要素にテンプレートを記述することができます。

以下は、今までの例同様、template オプションに文字列テンプレートを指定する例です。

HTML
<div id="app">
  <hello-div></hello-div>
</div>
JavaScript
Vue.createApp({})
  .component('hello-div', {
    data() {
      return {
        name: 'Vue'
      };
    },
    //文字列テンプレート(template オプション)
    template: `<div> Hello, {{name}}!</div>`,
}).mount('#app');

以下は上記を x-template を使って書き換えた例です。

x-template のテンプレートの定義では、type 属性に text/x-template を指定した <script> 要素内にテンプレートを記述し、スクリプトから参照できるように id 属性(そのページ内で一意でなければならない)を指定します(type 属性に指定した text/x-template は非標準なのでブラウザは無視します)。

x-template は Vue によってマウントされた要素の外側に記述します。

HTML
<div id="app">
  <hello-div></hello-div>
</div>

<!-- x-template(マウントされた要素の外に記述)-->
<!-- type 属性は "text/x-template" とし、id 属性を指定 -->
<script type="text/x-template" id="my-hello">
  <div> Hello, {{name}}!</div>
</script>

スクリプトでは、template オプションで #id(id 属性のセレクタ)によってテンプレートが記述されている <script> 要素を参照します。

template オプションの文字列が # から始まる場合は、querySelector として扱われ、テンプレート文字列として選択された要素(この場合は script 要素)の innerHTML を使います。

JavaScript
Vue.createApp({})
  .component('hello-div', {
    data() {
      return {
        name: 'Vue'
      };
    },
    //#id(id 属性のセレクタ)でテンプレートを参照
    template: '#my-hello',
}).mount('#app');

x-template を使うと、コードとテンプレートが分離できるのでコーディングとデザインの分業が容易になるかも知れませんが、逆にコンポーネント本体からテンプレートが切り離されたことで、コンポーネントの見通しが悪くなります。

※ Vue3 のドキュメントには x-template についての解説が見つかりませんでした。

Vue2 :X- テンプレート

ネイティブの template 要素を使用

template オプションに # から始まる文字列(要素のID)を指定して、ネイティブの <template> 要素を利用することもできます。

以下は、template オプションに文字列テンプレートを指定する例です。

HTML
<div id="app">
  <hello-div></hello-div>
</div>
JavaScript
Vue.createApp({})
  .component('hello-div', {
    data() {
      return {
        name: 'Vue'
      };
    },
    //文字列テンプレート(バッククォート ` で文字列を囲むテンプレートリテラル)
    template: `<div> Hello, {{name}}!</div>`,
}).mount('#app');

以下は template オプションに # から始まる文字列(要素のID)を指定して、ネイティブの <template> 要素を利用する例です。

ネイティブの template 要素は HTML マークアップテンプレートの格納場所として機能し、HTML 上に見えない要素を作成します。

HTML
<div id="app">
  <hello-div></hello-div>
</div>

<!-- ネイティブの template 要素を使用 -->
<template id="hello">
  <div> Hello, {{name}}!</div>
</template>
JavaScript
Vue.createApp({})
  .component('hello-div', {
    data() {
      return {
        name: 'Vue'
      };
    },
    // # から始まる文字列を指定
    template: '#hello',
    //または 以下のように getElementById や querySelector を使用することもできる
    //template: document.getElementById('hello')
    //template: document.querySelector('#hello')
}).mount('#app');

オプション: レンダリング template

render オプション

テンプレートの定義には template オプションを利用するのが推奨されていますが、複雑な分岐などのプログラム的な処理が必要な場合は render オプションを使ってコードから生成することができます。

render オプションには、render 関数を定義し、その中でレンダリングする要素を表す VNode(仮想ノード)を返します。テキストや VNode の配列を返すこともできます(render() 関数の返り値)。

VNode を生成するには h() 関数を使用します。

Render 関数

以下は props を使って title-text 属性に指定された値を h1 要素で出力するコンポーネントの例です。

HTML
<div id="app">
  <my-title title-text="A Perfect Vue"></my-title>
</div>

以下では、template オプションを利用してテンプレートを定義しています。

JavaScript
const app = Vue.createApp({});

app.component('my-title', {
  // template オプション
  template: `<h1>{{ titleText }}</h1>`,
  props: {
    titleText: {
      type: String,
      required: true
    }
  }
});

app.mount('#app');

上記を render オプションを使って書き換えると以下のように記述できます(この例の場合、通常は template オプションを使えば良く、render オプションを使う必要はありません)。

render() 関数では、h() 関数を使って生成した要素(VNode オブジェクト)を返しています。

JavaScript
const app = Vue.createApp({});

app.component('my-title', {
  // render オプション(render 関数を定義)
  render() {
    // h() 関数を使って VNode オブジェクト(要素)を返す
    return Vue.h(
      'h1', // 要素名
      this.titleText // 子要素
    )
  },
  props: {
    titleText: {
      type: String,
      required: true
    }
  }
});

app.mount('#app');

h() 関数は、createApp() 同様、グローバル API の関数なので、Vue オブジェクトを介してアクセスできます。以下のように分割代入を使えば、Vue. を指定せずに、単に h() や createApp() と記述できます。

JavaScript
const { createApp, h } = Vue;

const app = createApp({});

app.component('my-title', {
  render() {
    return h(
      'h1',
      this.titleText
    )
  },
  props: {
    titleText: {
      type: String,
      required: true
    }
  }
});

app.mount('#app');

前述の例の場合、render オプションを使う意味はありませんが、例えば以下のような v-else-if を多用した template オプションを使ったテンプレートは冗長です。

JavaScript
const { createApp } = Vue;

const app = createApp({});

app.component('anchored-heading', {
  // template オプション
  template: `
    <h1 v-if="level === 1" class="heading">
      <a v-bind:href="url">{{ text }}</a>
    </h1>
    <h2 v-else-if="level === 2" class="heading">
      <a v-bind:href="url">{{ text }}</a>
    </h2>
    <h3 v-else-if="level === 3" class="heading">
      <a v-bind:href="url">{{ text }}</a>
    </h3>
    <h4 v-else-if="level === 4" class="heading">
      <a v-bind:href="url">{{ text }}</a>
    </h4>
    <h5 v-else-if="level === 5" class="heading">
      <a v-bind:href="url">{{ text }}</a>
    </h5>
    <h6 v-else-if="level === 6" class="heading">
      <a v-bind:href="url">{{ text }}</a>
    </h6>
  `,
  props: {
    level: {
      type: Number,
      required: true
    },
    text: {
      type: String,
      required: true
    },
    url: {
      type: String,
      required: true
    }
  }
});

app.mount('#app');

以下は上記を render オプションを使って書き換えた例で、簡潔に記述することができます。

JavaScript
const { createApp, h } = Vue;

const app = createApp({});

app.component('anchored-heading', {
  // render オプション
  render() {
    return h(
      'h' + this.level, // タグ名
      {class: 'heading'}, // 属性
      h('a', {href: this.url}, this.text) // 子要素
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    },
    text: {
      type: String,
      required: true
    },
    url: {
      type: String,
      required: true
    }
  }
});

app.mount('#app');
HTML
<div id="app">
  <anchored-heading v-bind:level="1" text="About Foo" url="#foo" ></anchored-heading>
  <anchored-heading v-bind:level="2" text="About Bar" url="#bar" ></anchored-heading>
  <anchored-heading v-bind:level="3" text="About Baz" url="#baz" ></anchored-heading>
</div>

上記は以下のように出力されます。

<div id="app" data-v-app="">
  <h1 class="heading"><a href="#foo">About Foo</a></h1>
  <h2 class="heading"><a href="#bar">About Bar</a></h2>
  <h3 class="heading"><a href="#baz">About Baz</a></h3>
</div>

render() 関数では JavaScript の if/else や switch、 map() などを必要に応じて組み合わせて使うことで簡潔に記述することができます。

また、v-if などのテンプレートの機能を JavaScript で書き換えることもできます。

render() 関数の返り値

render() 関数は 1 つのルート VNode を返すだけでなく、ラップする要素のないテキストの VNode や配列を使用して複数のルートノードを返すこともできます。

文字列を返すと、ラップする要素のないテキストの VNode が作成されます。

JavaScript
const app = Vue.createApp({});
app.component('hello-text', {
  //文字列を返す
  render() {
    return 'Hello world!'
  },
});
app.mount('#app');
HTML
<div id="app">
  <hello-text></hello-text>
</div>

以下が出力されます。

<div id="app" data-v-app="">Hello world!</div>

配列を使用して複数のルートノードを返すことができます(以下の h() 関数では props を省略)。

JavaScript
const { createApp, h } = Vue;
const app = createApp({});
app.component('hello-divs', {
  render() {
    // 配列を使用して複数のルートノードを返す
    return [
      h('div','hello'),
      h('div','hello'),
      h('div','hello')
    ]
  }
});
app.mount('#app');
HTML
<div id="app">
              <hello-divs></hello-divs>
            </div>

以下が出力されます。

<div id="app" data-v-app="">
  <div>hello</div>
  <div>hello</div>
  <div>hello</div>
</div>

ルートノードでラップしない子の配列を返すと Fragment を作成します。

JavaScript
const { createApp, h } = Vue;
const app = createApp({});
app.component('hello-fragment', {
  render() {
    // ルートノードでラップしない子の配列を返す
    return [
      'Hello',
      h('br'),
      'world!'
    ]
  }
});
app.mount('#app');
HTML
<div id="app">
  <hello-fragment></hello-fragment>
</div>

以下が出力されます。

<div id="app" data-v-app="">
  Hello<br>world!
</div>

h() 関数

h() 関数は VNode(仮想ノード)を作るためのユーティリティ関数で、指定された引数を使って生成した VNode を返します。

h() 関数の h は hyperscript の略で、 HTML (hypertext markup language) を生成するスクリプトというような意味です。

以下が h() 関数の構文です。

h( type [,props] [,children] )

h() 関数は以下の 3 つの引数を受け取ります。

引数 概要
type String | Object | Function HTML タグ名、コンポーネント、非同期コンポーネント、または関数型コンポーネント。 null を返す関数を使うと、コメントがレンダリングされます。必須
props Object テンプレートで使う属性、プロパティ、イベントに対応するオブジェクト。省略可能
children String | Array | Object h() を使って構築された子供の VNode、または文字列をつかった「テキスト VNode」、もしくはスロットを持つオブジェクト。省略可能

※ props がない場合は、children を第 2 引数として渡すことができます。または、空のオブジェクト { } か null を第 2 引数として渡して、 children を第 3 引数にすることもできます。

仮想 DOM ツリー

Vue は、実際の DOM に反映する必要のある変更を追跡するために 仮想 DOM を構築して、ページを最新の状態に保ちます。

通常、h() 関数は render() の中で VNode を返すために使用されます。

render() {
  return h('h1', {}, 'Some title')
}

//上記のように第2引数が空の(props がない)場合は以下のように記述することもできます
render() {
  return h('h1', 'Some title')
}

h() 関数が返す VNode は、実際の DOM 要素ではなく、ページ上にどんな種類のノードをレンダリングするのかを Vue に伝えるための情報をもったプレーンなオブジェクトで、子ノードの記述も含まれています。

このノードの記述を 仮想ノード(Virtual Node)と呼び、通常 VNode と省略します。仮想 DOM(Virtual DOM)は、Vue コンポーネントのツリーから構成される VNode のツリー全体のことです。

以下は h() 関数の第1引数に HTML タグ名(h1)、第2引数にクラス属性のオブジェクト、第3引数にテキストコンテンツを指定して生成した VNode を、render オプションで return してテンプレートとしています。

JavaScript
const app = Vue.createApp({});
app.component('title-h1', {
  render() {
    return Vue.h('h1', { class: 'foo'}, 'Some title');
  }
});
app.mount('#app');
HTML
<div id="app">
  <title-h1></title-h1>
</div>

以下が出力されます。

<div id="app" data-v-app="">
  <h1 class="foo">Some title</h1>
</div>

ネストした要素(VNode)

以下はネストした(入れ子の) VNode の例です。配列で複数の要素を指定しています。

JavaScript
const { createApp, h } = Vue;
const app = createApp({});
app.component('my-div', {
  render() {
    return h('div', { class: 'container'}, [
      h('header', { class: 'foo'},'This is Header'),
      h('main', { class: 'bar'},'This is Main'),
      h('footer', { class: 'baz'},'This is Footer')
    ])
  }
});
app.mount('#app');
HTML
<div id="app">
  <my-div></my-div>
</div>

以下が出力されます。

<div id="app" data-v-app="">
  <div class="container">
    <header class="foo">This is Header</header>
    <main class="bar">This is Main</main>
    <footer class="baz">This is Footer</footer>
  </div>
</div>

複数のルートノード

以下は h() 関数の配列を指定して、複数のルートノードを生成する例です。

JavaScript
const { createApp, h } = Vue;
const app = createApp({});
app.component('my-div', {
  render() {
    return [
      h('div', { class: 'foo'},'This is Foo'),
      h('div', { class: 'bar'},'This is Bar'),
      h('div', { class: 'baz'},'This is Baz')
    ];
  }
});
app.mount('#app');
HTML
<div id="app">
  <my-div></my-div>
</div>

以下が出力されます。

<div id="app" data-v-app="">
  <div class="foo">This is Foo</div>
  <div class="bar">This is Bar</div>
  <div class="baz">This is Baz</div>
</div>

VNode は一意でなければならない

コンポーネント内のすべての VNode は一意でなければならないため、以下のような render() 関数は無効とのことです(以下を実行するとエラーはなく、2つの p 要素が出力されますが)。

JavaScript
const { createApp, h } = Vue;
const app = createApp({});
app.component('hi-paragraphs', {
  render() {
    //VNode を定義
    const p = h('p', 'hi')
    return h('div', [
      // 重複した VNode(上記で定義した VNode)
      p,
      p
    ])
  }
});
app.mount('#app');

もし、同じ要素を何回もコピーする場合は、以下のように h() 関数を使って同じ要素を生成することができます。

以下は length プロパティのみを持つ配列のようなオブジェクトを使って Array.from() で 20 個の同じ p 要素を表す VNode を子要素として返しています。

JavaScript
const { createApp, h } = Vue;
const app = createApp({});
app.component('hi-paragraphs', {
  render() {
    // 配列のようなオブジェクトを使って 20 個の同じ p 要素をレンダリング
    return h(
      'div',
      Array.from({ length: 20 }, () => {
        return h('p', 'hi')
      })
    )
  }
});
app.mount('#app');
HTML
<div id="app">
  <hi-paragraphs></hi-paragraphs>
</div>

以下が出力されます。

<div id="app" data-v-app="">
  <div>
    <p>hi</p>
    <p>hi</p>
    ・・・中略(20 個の同じ p 要素)・・・
    <p>hi</p>
  </div>
</div>

関数型コンポーネント

通常のコンポーネントの他に、シンプルな関数型のコンポーネントを定義することができます。

但し、Vue 3.x では、関数型のコンポーネントを使うメリットがあまりないため、通常のコンポーネントの使用が推奨されているようです。

関数型コンポーネント breaking

関数型コンポーネントは、それ自体にはなんの状態も持たないコンポーネントで、コンポーネントのインスタンスを作成しないでレンダリングされるため、通常のコンポーネントのライフサイクルを無視します。

関数型コンポーネントを生成するには、component メソッドの第2引数にオプションオブジェクトではなく、関数を指定します。

第2引数に指定する関数は以下の引数を受け取り、戻り値として生成された要素(VNode オブジェクト)を返します。

引数
  • props :プロパティ
  • context :コンテキスト

関数型コンポーネントはインスタンスを作成しないので this を参照できないため、 Vue は props を最初の引数として渡します。

第2引数の context はコンポーネントの動作に関するオブジェクトで以下の3つのプロパティが含まれます。

context のプロパティ
  • attrs :属性の情報(インスタンスプロパティの $attrs に相当)
  • emit :イベントの情報(インスタンスプロパティの $emit に相当)
  • slots :スロットの情報(インスタンスプロパティの $slots に相当)

以下は type と text の2つの属性(props)を持つ関数型のコンポーネントの例です。

type 属性の値に「image」が指定されていれば、img 要素で画像を出力し、それ以外の値や type 属性が指定されていない場合は、p 要素で文字列を出力します。

また、text 属性が指定されていれば、その値を出力し、text 属性が指定されていない場合は、「Alert!」と出力します。

JavaScript
const { createApp, h } = Vue;
const app = createApp({});

// 関数型のコンポーネントを登録(定義)
app.component('my-alert',(props, context) => {
  //type 属性の値が image の場合
  if(props.type === 'image'){
    return h('img', {
      src: 'images/alert.jpg',
      alt: 'alert'
    });
  }else{
    //text 属性に値が指定されていればその値を出力
    if(props.text) {
      return h('p', props.text)
    }else{
      //text 属性に値が指定されていなければ「Alert!」と出力
      return h('p', 'Alert!')
    }
  }
});
app.mount('#app');
HTML
<div id="app">
  <my-alert text="Alert Message"></my-alert>
  <my-alert type="image"></my-alert>
  <my-alert></my-alert>
</div>

以下のように出力されます。

<div id="app" data-v-app="">
  <p>Alert Message</p>
  <img src="images/alert.jpg" alt="alert">
  <p>Alert!</p>
</div>