Vue の基本的な使い方 (2) Composition API
Vue 3 の Composition API やカスタムディレクティブ、プラグイン(Element Plus、Vue I18n)の基本的な使い方、単純なプラグインの作成方法などについて。
関連ページ
- Vue の基本的な使い方 (1) Options API
- Vue の基本的な使い方 (3) Vite と SFC(単一ファイルコンポーネント)
- Vue の基本的な使い方 (4) Vue Router ルーティング
- Vue の基本的な使い方 (5) Pinia を使って状態管理
- Vue の基本的な使い方 (6) Vue3 で簡単な To-Do アプリを色々な方法で作成
- Vue の基本的な使い方 (7) Vue Router と Pinia を使った簡単なアプリの作成
作成日:2022年10月16日
Composition API の基本
以下は Composition API の基本的な使い方についての解説(覚書)です。扱っているサンプルはとても単純なもので、Vue は CDN 経由で読み込んでいます。
また、以下では主に https://v3.ja.vuejs.org/ のドキュメントを参考にしています。
現在の Vue の公式ドキュメントは Vue3 に対応していて、左上の「API 選択」ボタンで内容によっては Composition API と Options API を選択できるようになっています。
Composition API は Vue 3 で導入された大規模なコンポーネントを記述するのに適した API です。
従来の Options API では data や methods、computed など定義(オプション)の種類ごとにコードを記述しますが、Composition API ではコンポーネントの定義を setup メソッドを使ってコードを論理的なまとまりで記述することができるようになっています。
Composition API に関するよくある質問 | Composition API FAQ
Vue のドキュメントでは Options API と Composition API で異なる内容が含まる場合、左上の「API 選択」で内容を切り替えられるようになっています(例:リアクティビティーの基礎)。
以下は Options API で記述したボタンをクリックするとカウントを増加させるコンポーネントの例です。
<div id="app"> <button v-on:click="increment">Count Up</button> <p>{{ count }}</p> </div>
Options APIでは、オブジェクトプロパティとして data や methods などの役割(オプション)ごとに記述します。
Vue.createApp({ // data オプション(リアクティブな変数を定義) data() { return { count: 0 } }, // methods オプション(イベントリスナー) methods: { increment() { this.count++ } } }).mount('#app');
以下は上記を Composition API を使って書き換えたものです。
Composition API ではコンポーネントに必要なもの(変数や関数)を setup メソッドで定義し、定義したオブジェクト(変数や関数)を return することでコンポーネントのテンプレート内でオブジェクトのプロパティにアクセスすることができます。
data
Options API ではリアクティブな変数は data オプションに定義しますが、Composition API では setup メソッド内で ref や reactive というメソッドを使用して定義し、最後にまとめて return します。
methods
Options API ではイベントリスナーは methods オプションに定義しますが、Composition API では setup メソッド内で関数として定義し、最後にまとめて return します。
Vue.createApp({ //setup メソッド setup() { //リアクティブな変数(データオブジェクト)を宣言(data オプションに相当) const count = Vue.ref(0); //イベントリスナー( methods オプションに相当) const increment = () => { //ref には value プロパティで値にアクセス count.value ++; }; //上記で定義したオブジェクトを戻り値にして返す(プロパティの短縮構文) return { count, //count: count と同じ increment //increment: increment と同じ } } }).mount('#app');
以下も Options API を使ったコンポーネントの例です。
<div id="app"> <counter-div v-bind:initial-count="0"></counter-div> </div>
この例では、data オプションにプロパティ(props)の値を初期値として使うリアクティブな変数を定義し、methods オプションにイベントリスナーを定義しています。
const app = Vue.createApp({}); app.component('counter-div', { // props オプション props: { initialCount : { type: Number, default: 1 } }, // template オプション template: `<div>Count: {{ count }} <div> <button type="button" v-on:click="countUp">Increase</button> <button type="button" v-on:click="countDown">Decrease</button> </div> </div> `, // data オプション data() { return { //プロパティの値を初期値として使うリアクティブな変数を定義 count: this.initialCount } }, // methods オプション methods: { countUp() { this.count++; }, countDown() { this.count--; } } }); app.mount('#app');
以下は上記を Composition API を使って書き換えた例です(props と template オプションは同じ)。
Composition API ではコンポーネントに必要なものを setup メソッドで定義しますが、プロパティ(props)やテンプレート、イベントなどは setup メソッドの外で定義します。
setup メソッドでは 2 つの引数(props と context)を受け取れるので、引数経由で props などにアクセスすることができます。
そして、setup メソッドで定義したものをまとめて戻り値にして返すことで、それらをコンポーネントの他のオプションで使用することができます。
以下の例の場合、setup メソッドで定義して返した count、countUp、countDown を template オプションで使用(参照)しています。
また、以下の場合、data オプションに相当するリアクティブな変数(count)は ref でリアクティブにしているので、その値には value プロパティ経由でアクセスします。
const app = Vue.createApp({}); app.component('counter-div', { // props オプション(setup の外側で宣言) props: { initialCount : { type: Number, default: 1 } }, // template オプション(setup の外側で宣言) template: `<div>Count: {{ count }} <div> <button type="button" v-on:click="countUp">Increase</button> <button type="button" v-on:click="countDown">Decrease</button> </div> </div> `, // setup メソッド内でコンポーネントに必要なものを定義して返す setup(props, context) { // データオブジェクトを宣言( data オプションに相当) const count = Vue.ref(props.initialCount); // イベントリスナー(関数)を定義( methods オプションに相当) const countUp = () => { // value プロパティ経由で値を操作 count.value ++; }; // イベントリスナー(関数)を定義( methods オプションに相当) const countDown = () => { count.value --; } //上記で定義したものをまとめて戻り値にして返す(テンプレートに公開する) return { count, //データオブジェクト( data オプションに相当) countUp, // イベントリスナー( methods オプションに相当) countDown // イベントリスナー( methods オプションに相当) } } }); app.mount('#app');
setup
setup メソッド(コンポーネントオプション)は、コンポーネントが作成される前に props が解決されると実行され、 Composition API のエントリポイントとして機能します。
setup メソッドは他のコンポーネントオプションとは異なり、コンポーネントのインスタンスを参照しないため、setup の中で this
にアクセスできません。
また、setup は data プロパティ、computed プロパティ、methods が解決される前に呼び出されるため、setup の中では利用できません。
setup メソッドは次のオプションのいずれかが評価される前に実行されます。
- Components
- Props
- Data
- Methods
- Computed Properties
- Lifecycle methods
setup は 2 つのオプションの引数(props と context)を受け取り、これらの引数を介して this を使って通常 アクセスするプロパティにアクセスすることができます。
setup(props, context) { //・・・コンポーネントに必要な変数や関数の定義・・・ return { //定義したものを明示的に返す //ここで返されるものはコンポーネントの他のオプションで使用可能 } }
props
setup 関数の第 1 引数は props(プロパティ)です。props は明示的に宣言されたプロパティのみが含まれます。標準コンポーネントと同様、setup 関数内の props はリアクティブで、新しい props が渡されたら更新されます。
但し、props はリアクティブなので、ES6 の分割代入を使うことができません(分割代入を使うと、props のリアクティブを削除してしまいます)。props を分割代入する必要がある場合は、setup 関数内で toRefs(または toRef)を使います。
context
setup 関数に渡される第 2 引数は context(コンテキスト)です。
context は以下のようなプロパティを持つ JavaScript オブジェクトです。
- attrs :属性の情報(インスタンスプロパティの $attrs に相当)
- emit :イベントの情報(インスタンスプロパティの $emit に相当)
- slots :スロットの情報(インスタンスプロパティの $slots に相当)
- expose :明示的に公開するプロパティを制御する関数
context はリアクティブではないので、分割代入を安全に使用できます。
setup(props, { attrs, slots, emit, expose }) { ... }
attrs と slots はステートフルなオブジェクトで、コンポーネント自身が更新されたとき、常に更新されます。そのため、分割代入の使用を避け、attrs.xxx や slots.xxx のようにプロパティを常に参照する必要があります。
また、props とは異なり、attrs と slots のプロパティは リアクティブではありません。もし、attrs か slots の変更による副作用を適用したい場合は、onBeforeUpdate ライフサイクルフックの中で行います。
コンポーネントプロパティへのアクセス
setup が実行されるとき、以下のプロパティにのみアクセスできるようになります。
- props
- attrs
- slots
- emit
以下のコンポーネントオプションには アクセスできません。
- data
- computed
- methods
- refs (template refs)
戻り値
定義したオブジェクト(変数や関数)を返して、他のコンポーネントのオプションから利用できるようにします(テンプレートに公開します)。
setup がオブジェクトを返す場合、コンポーネントのテンプレート内でオブジェクトのプロパティにアクセスすることができ、 setup に渡された props のプロパティも同じようにアクセスできます。
返される関数は methods と同様の振る舞いをします。
const app = Vue.createApp({}); app.component('counter-div', { // props オプション(setup の外側で宣言) props: { initialCount : { type: Number, default: 1 } }, // template オプション(setup の外側で宣言) // setup で返されたオブジェクトのプロパティ(count、countUp、countDown)にアクセス template: `<div>Count: {{ count }} <div> <button type="button" v-on:click="countUp">Increase</button> <button type="button" v-on:click="countDown">Decrease</button> </div> </div> `, // setup メソッド内でコンポーネントに必要なものを定義して返す setup(props, context) { // 引数の props を使ってプロパティにアクセス(ref メソッドでデータをリアクティブに) const count = Vue.ref(props.initialCount); // 関数を定義( methods オプションに相当) const countUp = () => { count.value ++; }; // 関数を定義( methods オプションに相当) const countDown = () => { count.value --; } //定義したものを返すことで、コンポーネントの他のオプションで使用可能になります return { count, countUp, countDown } } }); app.mount('#app');
28〜32行目は以下のように記述したのと同じことです(プロパティの短縮構文)。
return { count: count, countUp: countUp, countDown: countDown }
emit
以下は Options API で $emit を使用する例です。
子コンポーネントでは、emits オプションに発行するイベント clickCount を宣言し、イベントリスナーの onclick で this.$emit('clickCount')
により clickCount イベントを発生させています。
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');
親コンポーネントでは、カスタムイベントを v-on:click-count で購読し、イベントリスナ onclickcount() で clickCount t 発生時にカウントを1増加しています。
<div id="app"> <p>Count : {{count}}</p> <counter-button v-on:click-count="onclickcount"></counter-button> </div>
以下は上記を Composition API で書き換えた例です(呼び出しの HTML は同じです)。
子コンポーネントでは、setup の第2引数 context のプロパティ emit を使って、イベントリスナーの onclick で context.emit('clickCount')
により clickCount イベントを発生させています。
親コンポーネントのデータ count は ref メソッドで生成したオブジェクトなので、値は value プロパティを使います。
const app = Vue.createApp({ setup() { //データ(ref でリアクティブに) const count = Vue.ref(0); // メソッド(clickCount 発生時にカウントを1増加。※ref の値は value プロパティを操作) const onclickcount = () => count.value++; //変数とメソッドを返す(テンプレートに公開) return { count, onclickcount, } } }); app.component('counter-button', { //emits オプションに発行するイベントを宣言 emits: [ 'clickCount'], template: ` <button type="button" v-on:click="onclick"> count up </button>`, // setup の第2引数 context から emit にアクセス setup(props, context) { const onclick = () => { //クリック時に clickCount イベントを発生 context.emit('clickCount') } //定義したメソッドを返す(テンプレートに公開) return { onclick } } }); app.mount('#app');
また、Options API 同様、以下のようにテンプレートで $emit を使って clickCount イベントを発行することもできます(5行目)。
app.component('counter-button', { //emits オプションに発行するイベントを宣言 emits: [ 'clickCount'], template: ` <button type="button" v-on:click="$emit('clickCount')"> count up </button>` }); app.mount('#app');
以下は Options API で $emit の第二引数を使ってリスナーに値を渡す例です。
この例では amount というプロパティ(クリック時に増減する値)を追加して、その値を $emit の第二引数に指定して親コンポーネントのコールバック関数に渡しています。
const app = Vue.createApp({ data() { return { count: 0 }; }, methods: { //$emit の第二引数を、引数に受け取る(引数名は任意の文字列) onclickcount(value) { this.count += value; }, } }); app.component('counter-button', { //ボタンをクリックした際に増減する値を指定するプロパティ props: { amount: { type: Number, //数値型 required: true //必須 } }, //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="100"></counter-button> </div>
以下は上記を Composition API で書き換えた例です(呼び出しの HTML は同じです)。
emit の第二引数に指定した値は、親コンポーネントのコールバック関数の引数に渡されます。また、setup 内では props は、第1引数の props のプロパティ(props.amount)としてアクセスします。
const app = Vue.createApp({ setup() { //データ(ref でリアクティブに) const count = Vue.ref(0); // メソッド(渡された値を引数に取る) const onclickcount = (value) => count.value += value; //変数とメソッドを返す return { count, onclickcount, } } }); app.component('counter-button', { //ボタンをクリックした際に増減する値を指定するプロパティ props: { amount: { type: Number, //数値型 required: true //必須 } }, //emits オプションに発行するイベントを宣言 emits: [ 'clickCount'], template: ` <button type="button" v-on:click="onclick"> {{ amount }} </button>`, //この例では、第2引数で分割代入を使って emit を取得 setup(props, { emit }) { const onclick = () => { //第二引数に親コンポーネントのコールバック関数に渡す値(データ)を指定 emit('clickCount', props.amount) } //定義したメソッドを返す return { onclick } } }); app.mount('#app');
親コンポーネントでカスタムイベントを購読する際に、イベントハンドラを直接記述する場合は、Options API 同様、$event
で clickCount イベントの値(emit の第二引数)にアクセスすることができます。
<div id="app"> <p>Count : {{count}}</p> <counter-button v-on:click-count="count += $event" v-bind:amount="10"></counter-button> </div>
上記の場合、親コンポーネントではイベントリスナは不要なので以下のようになります(子コンポーネントは同じなので省略)。
const app = Vue.createApp({ setup() { //データ(ref でリアクティブに) const count = Vue.ref(0); //データを返す return { count, } } });
expose
expose は公開するプロパティを明示的に制限するために使用することができる関数です。また、setup で Render 関数を返すような場合に、必要なプロパティを公開するために使用することができます。
Vue 3.2 より前では、Options API でメソッドやデータなどで宣言されたものはすべて、テンプレートがアクセスできるように公開されていました。Composition API についても同様で、setup メソッドから返されるものはすべて、親からテンプレート参照でアクセスできます。
例えば、以下の子コンポーネント my-counter の全てのメソッドには、親コンポーネントからテンプレート参照を使ってアクセスすることができます。
const app = Vue.createApp({}); app.component('my-counter', { template: ` <p>Counter: {{ count }}</p> <button @click="increment">Increment</button> <button @click="decrement">Decrement</button> <button @click="reset">Reset</button> `, setup() { //変数 const count = Vue.ref(0); // メソッド const increment = () => count.value++; const decrement = () => count.value--; const reset = () => count.value = 0; //変数とメソッドを返す(テンプレートに公開) return { count, increment, decrement, reset, } } }); app.mount('#app');
<div id="app"> <my-counter></my-counter> </div>
<div id="app" data-v-app=""> <p>Counter: 0</p> <button>Increment</button> <button>Decrement</button> <button>Reset</button> </div>
以下は、親コンポーネントでテンプレート参照を使って子コンポーネントのメソッドにアクセスして、親コンポーネントでも子コンポーネントのメソッドを実装したボタンを追加する例です。
const app = Vue.createApp({ setup() { //子コンポーネント側のコンポーネント情報を受け取る ref を宣言 const counter = Vue.ref(null); //または Vue.ref() // 子コンポーネント側の increment メソッドを発火させるメソッド const increaseCount = () => { // 公開されているメソッドを呼び出す counter.value.increment(); }; // decrement メソッドを発火させるメソッド const decreaseCount = () => { counter.value.decrement(); }; // reset メソッドを発火させるメソッド const resetCount = () => { counter.value.reset(); }; // テンプレートに公開 return { counter, increaseCount, decreaseCount, resetCount }; }, }); //子コンポーネントのコードは前述と同じ app.component('my-counter', { template: ` <p>Counter: {{ count }}</p> <button @click="increment">Increment</button> <button @click="decrement">Decrement</button> <button @click="reset">Reset</button> `, setup () { const count = Vue.ref(0); const increment = () => count.value++; const decrement = () => count.value--; const reset = () => count.value = 0; return { count, increment, decrement, reset, } } }); app.mount('#app');
<div id="app"> <my-counter ref="counter"></my-counter> <div> <!-- 親のボタン --> <button @click="increaseCount">Increment from parent</button> <button @click="decreaseCount">Decrement from parent</button> <button @click="resetCount">Reset from parent</button> </div> </div>
この場合、親で追加したボタンからもカウントを操作することができます。
<div id="app" data-v-app=""> <p>Counter: 0</p> <button>Increment</button> <button>Decrement</button> <button>Reset</button> <div><!-- 親のボタン --> <button>Increment from parent</button> <button>Decrement from parent</button> <button>Reset from parent</button> </div> </div>
expose を使うと、親コンポーネントからテンプレート参照でコンポーネントインスタンスにアクセスする際に、公開するプロパティを明示的に制限することができます。
例えば、以下のようにすると、親からは reset にしかアクセスできなくなります。そのため、親で追加した「Increment from parent」のボタンをクリックすると、「Uncaught TypeError: counter.value.increment is not a function」のようなエラーになります。
app.component('my-counter', { template: ` <p>Counter: {{ count }}</p> <button @click="increment">Increment</button> <button @click="decrement">Decrement</button> <button @click="reset">Reset</button> `, //第2引数の context で expose にアクセス setup (props, context) { const count = Vue.ref(0); const increment = () => count.value++; const decrement = () => count.value--; const reset = () => count.value = 0; //公開するプロパティを reset のみに制限する context.expose({ reset }); return { count, increment, decrement, reset, } } });
以下は increment も親に公開する例です。
また、以下では setup の第2引数に分割代入で expose を取得しています。
app.component('my-counter', { template: ` <p>Counter: {{ count }}</p> <button @click="increment">Increment</button> <button @click="decrement">Decrement</button> <button @click="reset">Reset</button> `, //第2引数に分割代入を使って expose にアクセス setup (props, { expose }) { const count = Vue.ref(0); const increment = () => count.value++; const decrement = () => count.value--; const reset = () => count.value = 0; //公開するプロパティを reset と increment に制限する expose({ reset, increment }); return { count, increment, decrement, reset, } } }); app.mount('#app');
expose の引数を空にすれば、全てを親に公開しないようにできます。
setup(props, { expose }) { // 全てを親に公開しない(インスタンスを隠蔽) expose(); ・・・ }
Render 関数での使用
setup は同じスコープで宣言されたリアクティブなステートを直接利用することができる Render 関数を返すこともできます(render オプション)。
以下はテンプレートを template オプションで定義しています。
const app = Vue.createApp({}); app.component('count-button', { //template オプションを利用 template: ` <button type="button" v-on:click="increment"> Count: {{ count }} </button>` , setup() { // データオブジェクトを宣言 const count = Vue.ref(0); // イベントリスナー(関数)を定義 const increment = () => ++count.value; // テンプレートに公開 return { count, increment }; } }); app.mount('#app');
<div id="app"> <count-button></count-button> </div>
以下は上記を template オプションを使わずに Render 関数を返すように書き換えた例です。
const app = Vue.createApp({}); app.component('count-button', { setup() { const count = Vue.ref(0); const increment = () => ++count.value; // Render 関数を返す return () => Vue.h( 'button', { type: "button", onClick: increment }, 'Count: ' + count.value ); } }); app.mount('#app');
h() 関数を直接返すとエラーになります。「h() 関数を返す関数」を返します。
以下のように別途 Render 関数を定義して記述することもできますが、通常は上記のように記述します。
const app = Vue.createApp({}); app.component('count-button', { setup() { const count = Vue.ref(0); const increment = () => ++count.value; // Render 関数 const myRender = function() { return Vue.h( 'button', //HTML タグ名 { type: "button", // type 属性 onClick: increment // v-on:click }, 'Count: ' + count.value // 子ノード ); } // Render 関数を返す return myRender ; } }); app.mount('#app');
Render 関数では h() 関数を使ってテンプレートになる VNode を返しています。
h() 関数の第2引数には、テンプレートで使う属性、プロパティ、イベントに対応するオブジェクトを指定します(v-on:click の場合は、プロパティ名は onClick になります)。テンプレートの機能をプレーンな JavaScript で置き換える
いずれの場合も、出力は以下のようになります。
<div id="app" data-v-app=""> <button type="button">Count: 0</button> </div>
Render 関数を返す場合は、他のプロパティを返すことができなくなります。プロパティを公開して、外部からアクセスする必要がある場合は、expose を呼び出して、外部コンポーネントのインスタンスで利用可能なプロパティを定義したオブジェクトを渡します。
以下は前述の例の count-button コンポーネントの increment メソッドを expose で公開して、親コンポーネントでテンプレート参照を使ってアクセスするように書き換えたものです。
<div id="app"> <!-- ref の childRef を指定 --> <count-button ref="childRef" v-on:click="handleClick"></count-button> </div>
const app = Vue.createApp({ setup() { //子コンポーネント側のコンポーネント情報を受け取る ref を宣言 const childRef = Vue.ref(null); //または Vue.ref() // 子コンポーネント側のメソッドを発火させるメソッド const handleClick = () => { // 公開されているメソッドを呼び出す childRef.value.increment(); }; // テンプレートに公開 return { childRef, handleClick, }; }, }); app.component('count-button', { setup(props, { expose }) { const count = Vue.ref(0); const increment = () => ++count.value; // increment メソッドを公開 expose({ increment }); return () => Vue.h( 'button', { type: "button", }, 'Count: ' + count.value ); } }); app.mount('#app');
テンプレート機能(v-if や v-for など)を同等の render 関数で実装する例は以下に掲載されています。
イベントやキー修飾子は、withModifiers ヘルパーを使用することもできます。
slots
以下は Options API で $slots を使用する例です。
<div id="app"> <base-layout> <template v-slot:header> <h1>Vue.js Sample</h1> </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> <p>Copyright ©xxxx. All rights reserved.</p> </template> </base-layout> </div>
render 関数でコンポーネントを記述する際に、this.$slots を使ってそれぞれの名前付きスロットのコンテンツにアクセスしています。
h() 関数の引数の子要素(children)の指定では、this.$slots.header().length が true であれば (0でなければ)、スロットのコンテンツ this.$slots.header() とし、そうでなければデータオブジェクトのフォールバックコンテンツを指定しています。
const { createApp, h } = Vue const app = createApp({}) app.component('base-layout', { data() { return { //スロットのフォールバックコンテンツ headerContent : h('h1', 'Default Header Content'), mainContent : h('p', 'Default Main Content'), footerContent : h('p', 'Default Footer Content') }; }, //h() で VNode を生成して返す render() { return h('div', {class:'container' }, [ h('header', //this.$slots.header() でスロットのコンテンツにアクセス this.$slots.header().length ? this.$slots.header() : this.headerContent ), h('main', this.$slots.default().length ? this.$slots.default() : this.mainContent ), h('footer', this.$slots.footer().length ? this.$slots.footer() : this.footerContent ) ]) } }); app.mount('#app');
以下は上記を Composition API で書き換えた例です。スロットの情報 slots は setup() の第2引数のプロパティ context.slotsとして受け取れますが、以下では分割代入で直接変数 slots に受け取っています。
const { createApp, h } = Vue const app = createApp({}) app.component('base-layout', { // 分割代入で第2引数に slots を受け取る setup(props, { slots }) { //スロットのフォールバックコンテンツの定義 const headerContent = h('h1', 'Default Header Content'); const mainContent = h('p', 'Default Main Content'); const footerContent = h('p', 'Default Footer Content'); // Render 関数を返す return () => h('div', {class:'container' }, [ h('header', slots.header().length ? slots.header() : headerContent ), h('main', slots.default().length ? slots.default() : mainContent ), h('footer', slots.footer().length ? slots.footer() : footerContent ) ]) } }); app.mount('#app');
ref メソッド
ref メソッドを使うと、あらゆる変数をリアクティブにすることができ、ref メソッドを使ってリアクティブな(値の変更を検知できる)変数を定義することができます。
ref メソッドは引数を受け取って、それを value
プロパティを持つミュータブルな ref オブジェクトでラップして返します。つまり、ref メソッドは値へのリアクティブな参照(refrence)を作成します。
const { ref } = Vue; const counter = ref(0); // counter は value プロパティを持つオブジェクト
ref で定義したリアクティブな変数の値にアクセスするには .value
を使用します。
const counter = Vue.ref(0); console.log(counter) // { value: 0 } オブジェクト(実際には以下のように出力されます) //RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 0, _value: 0} //value プロパティを使って変数の値にアクセス console.log(counter.value) // 0 ///value プロパティを使って変数の値を操作(増加) counter.value++ console.log(counter.value) // 1
以下では、プロパティ(props)の値を ref でリアクティブ(ref オブジェクト)にしています。
setup 関数内の props はリアクティブですが、setup 関数内で const count = props.initialCount とすると、コンポーネントがマウントされた後には更新されない値となってしまうので、 ref でリアクティブにします(setup は、コンポーネントが作成される前に props が解決されると実行されます)。
const app = Vue.createApp({}); app.component('counter-div', { props: { initialCount : { type: Number, default: 1 } }, // テンプレートでは count はアンラップされるので count で値にアクセス template: `<div>Count: {{ count }} <div> <button type="button" v-on:click="countUp">Increase</button> <button type="button" v-on:click="countDown">Decrease</button> </div> </div> `, setup(props, context) { // 引数の props を使ってプロパティにアクセスし、その値を ref メソッドでリアクティブに const count = Vue.ref(props.initialCount); const countUp = () => { // value プロパティを使って変数の値を操作 count.value ++; }; const countDown = () => { count.value --; } return { count, countUp, countDown } } }); app.mount('#app');
ref のアンラップ
ref メソッドでリアクティブ化された変数(Ref 変数)は、 setup() によって返されるオブジェクトのプロパティとして返されていてテンプレート内でアクセスされる場合、自動的に内部の値にアンラップ(ref でラップされた値を取り出す)されます。
このため、上記の場合、テンプレートでは count.value ではなく、count で値にアクセスします。
ref の値としてオブジェクトが代入された場合
ref の値としてオブジェクトが代入された場合、内部では reactive メソッドによりそのオブジェクトは深いリアクティブになります。
reactive メソッド
reactive メソッドを使って、オブジェクトをリアクティブにすることができます。
reactive メソッドは、プリミティブ以外の値を受け取り、リアクティブなオブジェクトに変換します(オブジェクトのリアクティブなコピーを返します)。
リアクティブの変換は、全てのネストされたプロパティに対して影響を及ぼします。深い変換を避け、ルートレベルのリアクティビティーのみを保持するためには、代わりに shallowReactive() 使用します。
//通常のオブジェクトを受け取り、リアクティブオブジェクトに変換(リアクティブなコピーをす) const obj = Vue.reactive({ count: 1, }) console.log(obj) // Proxy {count: 1} obj.count++ console.log(obj.count) // 2
reactive メソッドは、引数としてオブジェクトを受け取り、そのメンバーをまとめてリアクティブに変換します(オブジェクトに複数のプロパティを指定できます)。
以下は前述の例の ref を reactive を使って書き換えたものです。
ref の場合は、value プロパティで値にアクセスしますが、reactive の場合は、定義したオブジェクトのプロパティで値にアクセスします。
const app = Vue.createApp({}); app.component('counter-div', { props: { initialCount : { type: Number, default: 1 } }, // 定義したオブジェクト(state)のプロパティ(count)でアクセス template: `<div>Count: {{ state.count }} <div> <button type="button" v-on:click="countUp">Increase</button> <button type="button" v-on:click="countDown">Decrease</button> </div> </div> `, setup(props, context) { //reactive メソッドにオブジェクトを渡してリアクティブ化 const state = Vue.reactive({ count: props.initialCount, }); const countUp = () => { //オブジェクト(state)のプロパティ(count)でアクセス state.count ++; }; const countDown = () => { state.count --; } return { state, countUp, countDown } } });
分割代入などによるリアクティブの消失
reactive メソッドで生成したオブジェクトに分割代入やスプレッド構文を適用して得た変数はリアクティブではなくなります。
const obj = Vue.reactive({ count: 0, }); // 分割代入により、count はリアクティブではなくなります const { count } = obj;
例えば、前述の例の setup で以下のように分割代入を使ってしまうと、変数 count はリアクティブではなくなり、初期値は表示されますが、ボタンをクリックしても変更が反映されません(この例の場合、プロパティが1つなので、分割代入を使う意味はありませんが)。
setup(props, context) { //reactive メソッドにオブジェクトを渡してリアクティブ化 const state = Vue.reactive({ count: props.initialCount, }); // count はリアクティブではない(分割代入で state.count を変数 count に代入) let { count } = state; //また、以下のようにプロパティを直接参照して代入してもリアクティブではなくなります //let count = state.count; //上記の分割代入と同じこと const countUp = () => { //分割代入された変数 count でアクセス count ++; }; const countDown = () => { count --; } return { state, countUp, countDown } }
以下はスプレッド構文を使った例で、同様に変数 count はリアクティブではなくなります。
app.component('counter-div', { props: { initialCount : { type: Number, default: 1 } }, //スプレッド構文で展開された count でアクセス(count はリアクティブではない) template: `<div>Count: {{ count }} <div> <button type="button" v-on:click="countUp">Increase</button> <button type="button" v-on:click="countDown">Decrease</button> </div> </div> `, setup(props, context) { //reactive メソッドにオブジェクトを渡してリアクティブ化 const state = Vue.reactive({ count: props.initialCount, }); const countUp = () => { state.count ++; }; const countDown = () => { state.count --; } return { ...state, //スプレッド構文でプロパティを返す countUp, countDown } } });
reactive メソッドで生成したオブジェクトに分割代入やスプレッド構文を使う場合は toRefs メソッドを使用します。
toRefs メソッド
toRefs メソッドは reactive で生成したオブジェクトの各プロパティを(元のオブジェクトの対応するプロパティを指す)ref に変換します。これらの ref は元となるオブジェクトへのリアクティブな接続を保持します。
const obj = Vue.reactive({ count: 0, }); // obj のプロパティ count は ref に変換され、リアクティビティが保たれます const { count } = Vue.toRefs(obj);
前述の分割代入を使った例は、toRefs メソッドを使って以下のように書き換えれば、それぞれのプロパティが ref になっているので、リアクティビティが保たれ機能します。
変換されたプロパティは、ref なので .value で値にアクセスします。
app.component('counter-div', { props: { initialCount : { type: Number, default: 1 } }, template: `<div>Count: {{ count }} <div> <button type="button" v-on:click="countUp">Increase</button> <button type="button" v-on:click="countDown">Decrease</button> </div> </div> `, setup(props, context) { //reactive メソッドにオブジェクトを渡してリアクティブ化 const state = Vue.reactive({ count: props.initialCount, }); // toRefs でオブジェクトの各プロパティを ref に変換し、分割代入で変数 count に代入 let { count } = Vue.toRefs(state); const countUp = () => { // count は ref なので .value で値にアクセス count.value ++; }; const countDown = () => { count.value --; } return { count, //count を返す countUp, countDown } } });
前述のスプレッド構文を使った例も、toRefs メソッドを使ってリアクティビティを保つことができます。
app.component('counter-div', { props: { initialCount : { type: Number, default: 1 } }, //スプレッド構文で展開された count でアクセス template: `<div>Count: {{ count }} <div> <button type="button" v-on:click="countUp">Increase</button> <button type="button" v-on:click="countDown">Decrease</button> </div> </div> `, setup(props, context) { //reactive メソッドにオブジェクトを渡してリアクティブ化 const state = Vue.reactive({ count: props.initialCount, }); const countUp = () => { state.count ++; }; const countDown = () => { state.count --; } return { //toRefs でオブジェクトの各プロパティを ref に変換してスプレッド構文を適用 ...Vue.toRefs(state), countUp, countDown } } });
toRef メソッド
toRef メソッドを使うと、リアクティブなオブジェクトの特定のプロパティを ref として取り出すことができます。
const obj = Vue.reactive({ count: 0, }); // obj のプロパティ count を ref に変換 const count = Vue.toRef(obj, 'count');
例えば、以下のように、リアクティブなオブジェクトのプロパティを直接参照して変数に代入するとリアクティブではなくなります。
const state = Vue.reactive({ count: props.initialCount, }); // count はリアクティブではなくなります let count = state.count;
toRef メソッドを使えば、リアクティブなオブジェクトのプロパティを ref として取り出して、リアクティブを保ったまま変数に代入できます。
setup(props, context) { //reactive メソッドにオブジェクトを渡してリアクティブ化 const state = Vue.reactive({ count: props.initialCount, }); //プロパティ count を ref として取り出す let count = Vue.toRef(state, 'count'); const countUp = () => { // count は ref なので .value で値にアクセス count.value ++; }; const countDown = () => { count.value --; } return { count, //count を返す countUp, countDown } }
toRefs と toRef
前述の例の場合、以下はほぼ同じことになります。
const state = Vue.reactive({ count: props.initialCount, }); //プロパティ count を ref として取り出す let count = Vue.toRef(state, 'count'); //toRefs でオブジェクトの各プロパティを ref に変換し、分割代入で変数 count に代入 let { count } = Vue.toRefs(state);
但し、元のオブジェクトに対象のプロパティが存在しない場合は、結果が異なります。
toRef メソッドでは、値が空(undefined)の ref が生成されますが、toRefs メソッドでは undefined になります。
const obj = Vue.reactive({ count: 0, }); // obj の存在しないプロパティ foo を ref に変換(空の ref が生成される) const foo = Vue.toRef(obj, 'foo'); console.log(foo); //ObjectRefImpl {_object: Proxy, _key: 'foo', _defaultValue: undefined, __v_isRef: true}
const obj = Vue.reactive({ count: 0, }); // obj の存在しないプロパティ foo を ref に変換(undefined になる) let { foo } = Vue.toRefs(obj); console.log(foo); //undefined
このため、オブジェクトが対象のプロパティを持たない可能性がある場合は、toRef を使います。
computed メソッド
Options API では、算出プロパティを定義するために computed オプションを使用しますが、Composition API では computed メソッドを使用します。
computed メソッドは、getter 関数を受け取り、getter からの戻り値に対してイミュータブルでリアクティブな ref オブジェクトを返します。computed メソッドの第一引数に指定した関数が getter として動作します。
以下は computed メソッドを使用して、データオブジェクト(count)の値を2倍にする例です。
setup メソッド内で computed メソッドを使用して算出プロパティを定義します。以下では computed メソッドの引数に count の値を2倍にして返す関数を指定しています。
そして、定義した算出プロパティを setup メソッドから return します。
テンプレート内では、setup メソッドから return した算出プロパティを利用できます。
const app = Vue.createApp({}); app.component('counter-div', { props: { initialCount : { type: Number, default: 1 } }, // テンプレートでは算出プロパティにアクセスできます template: `<div>Count: {{ count }} <br> Count の2倍 : {{ doubleCount }} <div> <button type="button" v-on:click="countUp">Increase</button> <button type="button" v-on:click="countDown">Decrease</button> </div> </div> `, setup(props, context) { //プロパティの値を ref メソッドでリアクティブに const count = Vue.ref(props.initialCount); const countUp = () => { count.value ++; }; const countDown = () => { count.value --; } //computed メソッドを使用して算出プロパティを定義 const doubleCount = Vue.computed(() => { return count.value * 2; }); //算出プロパティの値を変更しようとするとエラーになる(setter を用意すればOK) //doubleCount.value = 10; //Write operation failed: computed value is readonly return { count, countUp, countDown, doubleCount //算出プロパティ } } }); app.mount('#app');
<div id="app"> <counter-div :initial-count="0"></counter-div> </div>
getter と setter
Options API の算出プロパティはデフォルトでは getter 関数のみですが、必要に応じて setter 関数を設定することもできます(算出 Setter 関数)。
Composition API の算出プロパティでも、setter 関数がサポートされています(Writable Computed)。
setter が必要な場合は、get と set をプロパティに持つオブジェクトを computed() の第一引数に指定します。
以下は前述の例の算出プロパティに setter を指定した例です。computed の値は ref 同様 value プロパティ経由でアクセスします。
setup(props, context) { //プロパティの値を ref メソッドでリアクティブに const count = Vue.ref(props.initialCount); const countUp = () => { count.value ++; }; const countDown = () => { count.value --; } //computed メソッドを使用して算出プロパティを定義 const doubleCount = Vue.computed({ //getter を定義 get: () => count.value * 2, //setter を定義 set: val => { count.value = val / 2; }, }); //算出プロパティの値(value プロパティ経由でアクセス)を変更することができる doubleCount.value = 10; console.log(count.value); //5 return { count, countUp, countDown, doubleCount //算出プロパティ } }
以下は、Options API のコンポーネント内で算出プロパティを使って v-model を実装する例です。
コンポーネント内で v-model を実装する方法の1つは、以下のように算出プロパティ(computed)の機能を使ってゲッターとセッターを定義します。
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: { // getter get() { return this.modelValue }, // setter set(value) { this.$emit('update:modelValue', value) } } } }); app.mount('#app');
<div id="app"> <custom-input v-model="myText"></custom-input> <p>{{ myText }}</p> </div>
以下は上記を setup メソッド で Composition API の算出プロパティを使って書き換えた例です。
const app = Vue.createApp({ setup() { const myText = Vue.ref(''); return { myText } } }); app.component('custom-input', { // modelValue プロパティ props: ['modelValue'], // emits オプション emits: ['update:modelValue'], // テンプレートで算出プロパティを v-model としてバインド template: `<input v-model="myValue">`, //setup メソッド setup(props, context) { // 算出プロパティに getter と setter を定義 const myValue = Vue.computed({ get() { return props.modelValue }, set(value) { if(props.modelValue !== value){ context.emit('update:modelValue', value) } } }) return { myValue } } }); app.mount('#app');
算出プロパティのデバッグ
computed メソッドの第2引数には、デバッグのためのオブジェクトを渡すことができます(3.2+)。
デバッグのためのオブジェクトは、以下のオプション(コールバック)を指定できます。
- onTrack :リアクティブプロパティが依存関係として参照される時に呼び出される
- onTrigger :リアクティブプロパティが変更(更新)される時に呼び出される
両方のコールバックは依存情報を持つデバッグイベント(e)を引数に受け取り、その引数を介してデバッグに関する情報を取得できます。
const app = Vue.createApp({}); app.component('counter-div', { props: { initialCount : { type: Number, default: 1 } }, template: `<div>Count: {{ count }} <br> Count の2倍 : {{ doubleCount }} <div> <button type="button" v-on:click="countUp">Increase</button> <button type="button" v-on:click="countDown">Decrease</button> </div> </div> `, setup(props, context) { const count = Vue.ref(props.initialCount); const countUp = () => { count.value ++; }; const countDown = () => { count.value --; } //computed メソッドの第2引数にデバッグのためのオブジェクトを指定 const doubleCount = Vue.computed(() => { return count.value * 2; }, { onTrack(e) { // count.value を依存関係として参照する場合にトリガーする console.log(`${e.type} : ${e.key}`); console.log(e); }, onTrigger(e) { // count.value を変更される場合にトリガーする console.log(`${e.type} : ${e.key}`); console.log(e); } }); return { count, countUp, countDown, doubleCount } } }); app.mount('#app');
<div id="app"> <counter-div :initial-count="0"></counter-div> </div>
//onTrack(e) :レンダリングの際に出力される get : value //`${e.type} : ${e.key}` {effect: ReactiveEffect, target: RefImpl, type: 'get', key: 'value'} //onTrigger(e) :ボタンをクリックする際に出力される set : value //`${e.type} : ${e.key}` {effect: ReactiveEffect, target: RefImpl, type: 'set', key: 'value', newValue: 1}
watchEffect メソッド
例えば、以下の場合、コンソールには最初に 0 と出力されるだけで、setTimeout() により1秒後に値を増加しても何も出力されません。
//リアクティブな値を定義 const count = Vue.ref(0); console.log(count.value); //即座に 0 が出力されるだけ setTimeout(() => { //1秒後に count を1増加(何も起きない) count.value++ }, 1000);
watchEffect メソッドは、渡されるコールバック関数に含まれるリアクティブ値を検出してコールバック関数を即時実行し、リアクティブ値が変化するとコールバック関数を再実行します。
以下の場合、watchEffect メソッドのコールバック関数にリアクティブな値(count.value)が含まれているので、setTimeout() により1秒後に値が増加すると、コンソールに 1 と出力されます。
//リアクティブな値を定義 const count = Vue.ref(0); Vue.watchEffect(() => { //コールバック関数に含まれるリアクティブ値を検出 console.log(count.value); //即座に 0 が出力され、1秒後に 1 が出力される }); setTimeout(() => { //リアクティブ値が変化すると watchEffect のコールバック関数が再実行される count.value++ }, 1000);
以下はボタンをクリックすると initial-count 属性で渡されたカウントの値を増加するコンポーネントの例です。
<div id="app"> <counter-div :initial-count="0"></counter-div> </div>
const app = Vue.createApp({}); app.component('counter-div', { props: ['initialCount'], template: `<div>Count: {{ count }} <div> <button type="button" v-on:click="countUp">Increase</button> </div> </div> `, setup(props, context) { //プロパティの値を ref でリアクティブに const count = Vue.ref(props.initialCount); const countUp = () => { count.value ++; }; return { count, countUp } } }); app.mount('#app');
上記の例の場合、initialCount の値は initial-count 属性で渡されるコンポーネントの初期化時に反映されるでけで、後からの変更を検出できません。
以下はルートコンポーネントにカウントを乱数で初期化するボタンを追加した例ですが、初期化するボタンをクリックして init を実行しても、initialCount の値は更新されません。
<div id="app"> <counter-div :initial-count="initialCount"></counter-div> <button type="button" v-on:click="init">乱数で初期化</button> </div>
const app = Vue.createApp({ setup() { //リアクティブな変数(カウントの初期値)を定義(data オプションに相当) const initialCount = Vue.ref(0); //イベントリスナー( methods オプションに相当) const init = () => { //initialCount を乱数でリセット(ref の値には value プロパティ経由でアクセス) initialCount.value = Math.floor(Math.random() * 10); //console.log(initialCount.value) //コンソールには出力される }; return { initialCount, init } } }); app.component('counter-div', { props: ['initialCount'], template: `<div>Count: {{ count }} <div> <button type="button" v-on:click="countUp">Increase</button> </div> </div> `, setup(props, context) { //プロパティの値を ref でリアクティブに const count = Vue.ref(props.initialCount); const countUp = () => { count.value ++; }; return { count, countUp } } }); app.mount('#app');
watchEffect を使って変更を検出
以下は前述の例を、子コンポーネントで watchEffect を使って initialCount の変更を検出するように書き換えたものです。この場合、ルートコンポーネントの初期化するボタンをクリックするとカウントの値を初期化できます(HTML は同じなので省略)。
const app = Vue.createApp({ setup() { //リアクティブな変数(カウントの初期値)を定義(data オプションに相当) const initialCount = Vue.ref(0); //イベントリスナー( methods オプションに相当) const init = () => { //initialCount を乱数でリセット(ref の値には value プロパティ経由でアクセス) initialCount.value = Math.floor(Math.random() * 10); //console.log(initialCount.value) }; return { initialCount, init } } }); app.component('counter-div', { props: ['initialCount'], template: `<div>Count: {{ count }} <div> <button type="button" v-on:click="countUp">Increase</button> </div> </div> `, setup(props, context) { //プロパティの値を ref でリアクティブに const count = Vue.ref(props.initialCount); //カウントの初期値(リアクティブ値)を監視して変更があればコールバック関数を実行 Vue.watchEffect(() => { //props.initialCount が変更されたら count の値を更新 count.value = props.initialCount; }) const countUp = () => { count.value ++; }; return { count, countUp } } }); app.mount('#app');
監視の停止
watchEffect をコンポーネントの setup() 関数または ライフサイクルフック の中で呼び出すと、ウォッチャはコンポーネントのライフサイクルにリンクされ、コンポーネントのアンマウント時に自動的に監視が停止します。
また、明示的にウォッチャによる監視を停止するための stop ハンドル(関数)が返されます。
const stop = watchEffect(() => { /* ... */ }) // 監視を停止 stop()
watchEffect のオプション
watchEffect メソッドの第2引数にオプションを指定することができます。
watchEffect( () => { /* ... */ }, { flush: 'post' //コールバックの実行タイミングのオプションを指定 } )
コールバックの実行タイミングを制御するには、flush オプションに以下を指定することができます。
値 | 概要 |
---|---|
pre | レンダリング(描画)前に実行(デフォルト)。テンプレートの実行前にコールバックが他の値を更新することができます。 |
post | レンダリング後に実行(更新後の文書ツリーにアクセスする場合や $refs 経由で子コンポーネントにアクセスする場合に利用) |
sync | 値が変更されたら即座に実行(作用をいつも同期的に発火することを強制するため非効率的)。 |
Vue 3.2 以降では、flush オプションの post と sync に対応するエイリアスのメソッド、watchPostEffect と watchSyncEffect が用意されています。
デバッグ用の onTrack および onTrigger オプションが必要ない場合は、それらのメソッドを利用することでコードの意図をより明確にすることもできます。
watch メソッド
watch メソッドは Options API の $watch メソッド(及び watch オプション)と同じものです。
watch メソッドを使うと、特定のデータソースを監視して変化したときにコールバック関数を実行できます。デフォルトでは監視しているデータソースが変更された場合に限り、コールバック関数が実行されます。
watchEffect と比較すると、watch では以下のことが可能です。
- 作用の効率的な実行
- ウォッチャの再実行条件を、より具体的に指定できる
- 監視している状態の、以前の値と現在の値の両方にアクセスできる
単一のデータソースの監視
watch メソッドの第1引数に指定する監視対象のデータソースは以下のいずれかになります。
- 値を返す gettter 関数
- ref
- リアクティブなオブジェクト
- または上記の配列
const state = Vue.reactive({ count: 0 }); //値を返す gettter 関数を渡す Vue.watch( () => state.count, // gettter 関数 (newValue, oldValue) => { /* ...コールバック... */ } ) const count = Vue.ref(0); //直接 ref を渡す Vue.watch( count, // ref (newValue, oldValue) => { /* ...コールバック... */ })
以下のようなリアクティブのオブジェクトのプロパティを監視できません。
const obj = reactive({ count: 0 }) // これは、watch() に数値(オブジェクトのプロパティ)を渡しているので動作しません。 Vue.watch( obj.count, //オブジェクトのプロパティを監視できない (count) => { console.log(`count is: ${count}`) } )
代わりに、getter 関数を使います。
Vue.watch( () => obj.count, // gettter 関数 (count) => { console.log(`count is: ${count}`) } )
以下は、コンソールに即座に「count is: 0」と出力し、1秒後に「count is: 1」と出力する例です。
//リアクティブなオブジェクトを定義 const obj = Vue.reactive({ count: 0 }) console.log(`count is: ${obj.count}`) //count is: 0 Vue.watch( () => obj.count, // gettter 関数でオブジェクトのプロパティを監視 (count) => { //第2引数を出力 console.log(`count is: ${count}`) //1秒後に count is: 1 } ) setTimeout(() => { obj.count++ }, 1000);
以下でも同じ結果になります。
const obj = Vue.reactive({ count: 0 }) console.log(`count is: ${obj.count}`) //count is: 0 Vue.watch( obj, // リアクティブなオブジェクトを監視 (obj) => { //第2引数のプロパティを出力 console.log(`count is: ${obj.count}`) //1秒後に count is: 1 } ) setTimeout(() => { obj.count++ }, 1000);
第2引数はソースが変更した時に呼ばれるコールバックで 3 つの引数を受け取ります。
- 新しい値
- 古い値
- 副作用のクリーンアップコールバックを登録するための関数
クリーンアップコールバックは、次に作用が再実行される直前に呼び出され、保留中の非同期リクエストのような無効化された副作用をクリーンアップするために使用することができます。
複数のデータソースの監視
配列を使って複数のソースを同時に監視することもできます。
const firstName = Vue.ref(''); const lastName = Vue.ref(''); Vue.watch([firstName, lastName], (newValues, prevValues) => { console.log(newValues, prevValues); }) firstName.value = 'John'; // logs: ["John", ""] ["", ""] setTimeout(() => { lastName.value = 'Smith'; // logs: ["John", "Smith"] ["John", ""] }, 100);
ただし、同じ関数内で監視しているソースの両方を同時に変更している場合には、ウォッチャは一度だけ実行されます。
以下は前述の watchEffect メソッドの例を watch メソッドで書き換えたものです。
const app = Vue.createApp({ setup() { const initialCount = Vue.ref(0); const init = () => { initialCount.value = Math.floor(Math.random() * 10); }; return { initialCount, init } } }); app.component('counter-div', { props: ['initialCount'], template: `<div>Count: {{ count }} <div> <button type="button" v-on:click="countUp">Increase</button> </div> </div> `, setup(props, context) { const count = Vue.ref(props.initialCount); /* //watchEffect メソッドの場合 Vue.watchEffect(() => { //props.initialCount が変更されたら count の値を更新 count.value = props.initialCount; }) */ //上記を watch メソッドで書き換え Vue.watch( () => props.initialCount, //props.initialCount を監視 (newValue, prevValue) => { //props.initialCount が変更されたら count の値を newValue で更新 count.value = newValue; } ) const countUp = () => { count.value ++; }; return { count, countUp } } }); app.mount('#app');
ネストしたオブジェクトや配列の監視
ネストしたオブジェクトや配列のプロパティの変更をチェックするには、deep オプションを true にする必要があります。
const state = Vue.reactive({ id: 1, attributes: { name: '' } }); Vue.watch( () => state, (newState, prevState) => { //実行されない console.log('not deep', newState.id, prevState.id); console.log('not deep', newState.attributes.name, prevState.attributes.name); } ) Vue.watch( () => state, (newState, prevState) => { console.log('deep', newState.id, prevState.id); console.log('deep', newState.attributes.name, prevState.attributes.name); }, { deep: true } //第3引数にオプション deep: true を指定 ) state.id = 7; // ログの出力: "deep" "7" "7" state.attributes.name = 'Alex' // ログの出力: "deep" "Alex" "Alex"
但し、リアクティブなオブジェクトや配列を監視すると、そのオブジェクトの状態の現在値と前回値の両方について参照が常に返されます。
以下はオブジェクトのプロパティを指定して(プロパティを返す関数を渡して)監視する例です。
Vue.watch( () => state.id, (newState, prevState) => { console.log('newState: ' + newState, 'prevState: ' + prevState); } ) Vue.watch( () => state.attributes.name, (newState, prevState) => { console.log('newState: ' + newState, 'prevState: ' + prevState); } ) state.id = 7; //ログの出力: newState: 7 prevState: 1 state.attributes.name = 'Alex'; //ログの出力: newState: Alex prevState:
配列を使って複数のソースを同時に監視する例です。
Vue.watch( () => [state.id, state.attributes.name], (newState, prevState) => { console.log(newState, prevState); } ) state.id = 7; state.attributes.name = 'Alex'; //ログの出力: [7, 'Alex'] [1, '']
watch メソッドのオプション
watch メソッドの第3引数に以下のようなオプションを指定できます。
オプション | 説明 |
---|---|
deep | ネストされたオブジェクトを監視するかどうか(デフォルトは false) |
immediate | 起動時に即座に実行するかどうか(デフォルトは false) |
flush | 処理(コールバック)の実行タイミングを制御(以下を指定)
|
Vue.watch( () => state, (newState, prevState) => { /* ...コールバック... */ }, //第3引数にオプションを指定 { deep: true, immediate: true, flush: 'post' } )
ライフサイクルフック
Composition API でも各ライフサイクルに該当する関数(ライフサイクルフック)が用意されていて、setup メソッドで使用することができます。
Composition API のライフサイクルフック名は、Options API でのフックの前に on を付けます。
Options API | Composition API (setup) |
---|---|
beforeCreate | setup() を使用(※) |
created | setup() を使用(※) |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
activated | onActivated |
deactivated | onDeactivated |
※ Composition API では、setup メソッドに定義した処理が beforeCreate と created に該当するため、これらに該当するフックは用意されておらず、setup を利用します。
言い換えると、setup メソッドは beforeCreate と created のライフサイクルで実行されます。
以下は Composition API のライフサイクルフックのそれぞれのタイミングでログを出力する例です。
<div id="app"> <button v-on:click="onclick">Click</button> <p>{{ count }}</p> </div>
const app = Vue.createApp({ setup() { console.log('setup'); Vue.onBeforeMount(() => { console.log('onBeforeMount hook'); }); Vue.onMounted(() => { console.log('onMounted hook'); }); Vue.onBeforeUpdate(() => { console.log('onBeforeUpdate hook'); }); Vue.onUpdated(() => { console.log('onUpdated hook: count = ' + count.value); }); Vue.onBeforeUnmount(() => { console.log('onBeforeUnmount hook'); }); Vue.onUnmounted(() => { console.log('onUnmounted hook'); }); const count = Vue.ref(0); const onclick = () => { count.value ++; }; return { count, onclick } } }); app.mount('#app'); setTimeout(() => { //5秒後にアンマウント app.unmount(); },5000);
上記の場合、最初に以下がほぼ同時にコンソールに出力されます。
setup onBeforeMount hook onMounted hook
そして5秒後に app.unmount() により以下が出力されます。
onBeforeUnmount hook onUnmounted hook
5秒以内(アンマウントする前)にボタンをクリックすると上記の前に以下が出力されます(以下はボタンを2回クリックした例です)。
onBeforeUpdate hook onUpdated hook: count = 1 onBeforeUpdate hook onUpdated hook: count = 2
テンプレート参照(ref 属性)
ref 属性を使ってコンポーネントや HTML 要素を参照することができます。
Options API では、ref 属性を付与した要素を this.$refs で参照することができます。以下の例ではテンプレートの div 要素の ref 属性に foo という名前を指定し、その要素を $refs.foo で参照しています。
const app = Vue.createApp({}); app.component('foo-div', { //ref 属性に foo を指定 template: `<div ref="foo">This is Foo.</div>`, mounted() { //this.$refs.foo で参照 console.log(this.$refs.foo); //<div>This is Foo.</div> が出力される console.log(this.$refs.foo.innerHTML); //This is Foo. console.log(this.$refs.foo.localName); //div (タグ名) } }); app.mount('#app');
<div id="app"> <foo-div></foo-div> </div>
Composition API では、$refs の代わりに ref メソッドを使って要素の参照を保持する ref($refs に該当)を宣言します(名前はテンプレートの ref 属性の値に一致させる必要があります)。
テンプレート内の要素やコンポーネントインスタンスの参照を取得するには、参照する要素に ref 属性で名前を指定します(参照先に ref 属性を指定します)。
そして setup() 内で ref() メソッドを使って ref 属性に指定した名前と同じ名前の要素の参照を保持する ref オブジェクトを定義します(以下9行目)。そして定義した ref オブジェクトを return してテンプレートに公開します。
これにより、ref 属性で指定した名前と、ref() メソッドで定義した ref オブジェクトが紐付き、変数名.value で参照できます(ref オブジェクトの値には value プロパティでアクセスします)。
const app = Vue.createApp({}); app.component('foo-div', { //ref 属性 foo を設定 template: `<div ref="foo">This is Foo.</div>`, setup() { //ref() を使って ref 属性に指定した名前と同じ名前の変数を定義($refs.foo に該当) const foo = Vue.ref(null); //または Vue.ref() //onMounted ライフサイクルでアクセス Vue.onMounted(() => { // .value プロパティでアクセス(foo は ref オブジェクト) console.log(foo.value); //<div>This is Foo.</div> が出力される console.log(foo.value.innerHTML); //This is Foo. console.log(foo.value.localName); //div }); //ref メソッドで定義した変数(ref オブジェクト)を返す return { foo } } }); app.mount('#app');
テンプレート参照に値が代入される(バインドされた ref にアクセスできる)のは初回レンダリング後になります(onMounted でアクセスできるようになります)。
また、ref メソッドは value プロパティを持つ ref オブジェクトを返すので、.value でアクセスします。
以下は親のルートコンポーネントでテンプレートに ref 属性を指定して、子コンポーネントのインスタンスやテンプレートの要素を参照する例です。
<div id="app"> <!-- それぞれに ref 属性を指定 --> <p ref="hello">hello</p> <bar-div ref="bar" class="child"></bar-div> </div>
const app = Vue.createApp({ setup() { //要素の参照を保持する ref を宣言(名前はテンプレートの ref 属性の値に一致させる) const hello = Vue.ref(); //hello は ref オブジェクト const bar = Vue.ref(); //bar は ref オブジェクト //onMounted ライフサイクルフックで参照先の情報を出力 Vue.onMounted(() => { //ref オブジェクトの value プロパティにアクセス console.log(hello.value); //<p>hello</p> console.log(hello.value.innerHTML); //hello console.log(bar.value); //Proxy {…} 子コンポーネントのインスタンス console.log(bar.value.message); //This is Bar. console.log(bar.value.onclick); //() => { alert('Bar'); } メソッド console.log(bar.value.$attrs); //Proxy {class: 'child', __vInternal: 1} console.log(bar.value.$attrs.class); //child }); // ref をテンプレートに公開 return { hello, bar, }; }, }); //子コンポーネント app.component('bar-div', { template: `<div class="bar" v-on:click="onclick">{{ message }}</div>`, setup() { //データ const message = 'This is Bar.'; //メソッド const onclick = () => { alert('Bar'); } //上記データとメソッドを公開 return { message, onclick } } }); app.mount('#app');
子コンポーネントのメソッドを親コンポーネント内で呼び出す
setup 関数に渡される第2引数 context の expose プロパティを使ってコンポーネントのプロパティを明示的に公開することができます。
setup メソッドから返されるものはすべて、親から直接アクセスできますが、expose プロパティで指定することで、公開するプロパティを制限することができます。
expose で公開されたプロパティは、親コンポーネントでテンプレート参照を使って利用できます。
<div id="app"> <!-- 子コンポーネントの ref 属性に childRef を付与 --> <child-div ref="childRef" message="Foo!"></child-div> <button type="button" v-on:click="handleClick">childFunc を呼び出す</button> </div>
子コンポーネント側のコンポーネント情報を受け取る ref(この例では childRef)を ref メソッドで初期化して宣言します。
テンプレートの ref 属性に宣言した変数と同じ名前を指定します。
expose で公開されたプロパティ は childRef.value.xxxx のような形で利用することができます。
//親コンポーネント const app = Vue.createApp({ setup() { //ref 属性に指定した値と同じ名前の変数を宣言して ref メソッドで初期化 const childRef = Vue.ref(null); //または Vue.ref() // 子コンポーネントのメソッドを発火させるメソッドを定義 const handleClick = () => { // 子コンポーネントで公開されているメソッドを呼び出す childRef.value.childFunc(); //以下のメソッドは公開されていないので使用するとエラーになる //childRef.value.childOnlyFunc(); }; // テンプレートに公開 return { childRef, handleClick, }; }, }); //子コンポーネント app.component('child-div', { // props オプション props: { message : { type: String, default: 'World!' } }, template: `<div>Hello {{ message }}</div>`, setup(props, context) { // 公開するメソッドの定義 const childFunc = () => { console.log("childFunc: props の値は " + props.message); }; // 非公開のメソッドの定義 const childOnlyFunc = () => { console.log("非公開のメソッド "); }; // context.expose で childFunc メソッドを外部コンポーネントに公開 context.expose({ childFunc }); return { childFunc, childOnlyFunc } } });
子コンポーネントの setup 関数は第2引数を分割代入を使って以下のように記述することもできます。
setup(props, { expose }) { const childFunc = () => { console.log("childFunc: props の値は " + props.message); }; const childOnlyFunc = () => { console.log("非公開のメソッド "); }; // expose で childFunc メソッドを外部コンポーネントに公開 expose({ childFunc }); return { childFunc, childOnlyFunc } }
以下のように出力され、ボタンをクリックすると「childFunc: props の値は Foo!」と出力されます。
<div id="app" data-v-app=""> <div>Hello Foo!</div> <button type="button">childFunc を呼び出す</button> </div>
provide / inject メソッド
Provide / Inject の仕組みも、Composition API の setup メソッドでは、provide / inject メソッドを使って実装することができます。
以下は Provide / Inject の仕組み(Options API)を使った例で、parent-div で provide オプションを使って title というリアクティブな値を提供し、child-div で inject オプションを使って title を取り込む例です。
Root └─ parent-div └─ foo-div └─ child-div
この例の場合、parent-div で提供する値はテキストボックスに入力された値で変更できるように、computed メソッドでリアクティブにしています。
const app = Vue.createApp({}); app.component('parent-div', { template: ` <div id="parent"> <input v-model="title"> <foo-div /> </div>`, data() { return { title: 'Vue3' } }, //値を提供(provide オプション) provide() { return { //computed メソッド でリアクティブに title: Vue.computed(() => this.title) } } }).component('foo-div', { template: ` <div id="foo"> <child-div /> </div>` }).component('child-div', { template: ` <div id="child"> child: {{ title.value }} </div>`, //値を注入(inject オプション) inject: ['title'], }); app.mount('#app');
<div id="app"> <parent-div /> </div>
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.) 」のような警告が出ます(次のマイナーリリースでは ref が自動的にアンラップされるので .value は不要になるとのこと)。
以下は、上記を Composition API の provide / inject メソッドを使って書き換えた例です。
script setup を使用しない場合、provide() と inject() は setup() 内で同期的に呼び出す必要があります。
const app = Vue.createApp({}); app.component('parent-div', { template: ` <div id="parent"> <input v-model="title"> <foo-div /> </div>`, setup() { //提供する値をリアクティブに const title = Vue.ref('Vue3'); //provide メソッドで値を登録 Vue.provide('title', title); return { title }; }, }).component('foo-div', { template: ` <div id="foo"> <child-div /> </div>` }).component('child-div', { template: ` <div id="child"> child: {{ title }} </div>`, setup() { //inject メソッドで登録された値を取得 const title = Vue.inject('title'); return { title }; } }); app.mount('#app');
provide メソッドは 2 つの引数(名前、値)によってプロパティを定義できます。
- 名前:文字列または Symbol。子孫のコンポーネントが注入する際に使用する名前(キー)。インジェクションキーと呼ばれます。
- 値:提供される値。この値は refs のようなリアクティブな状態を含む、任意の型にすることができます
// 静的な値を提供 Vue.provide('foo', 'bar') // リアクティブな値を提供 const count = Vue.ref(0) Vue.provide('count', count)
inject メソッドは 2 つの引数(注入されるプロパティ名、デフォルト値)をとります。第2引数のデフォルト値は省略可能です。
上記の例(30行目)では、inject メソッドの第2引数を省略していますが、もし該当する provide がない場合のデフォルト値を指定することもできます。
const title = Vue.inject('title', '既定値');
リアクティブ
提供された値と注入された値をリアクティブにするには、値を提供する際に ref または reactive を使います。
以下は、前述の例の provide で提供する値をオブジェクトにして、reactive メソッドを使ってリアクティブにするように書き換えたものです。
const app = Vue.createApp({}); app.component('parent-div', { template: ` <div id="parent"> <input v-model="obj.title"> <foo-div /> </div>`, setup() { //reactive メソッドを使ってリアクティブに const obj = Vue.reactive({ title: 'Object Title' }); //オブジェクトで提供 Vue.provide('obj', obj); return { obj }; }, }).component('foo-div', { template: ` <div id="foo"> <child-div /> </div>` }).component('child-div', { template: ` <div id="child"> child: {{ obj.title }} </div>`, setup() { const obj = Vue.inject('obj'); return { obj }; } }); app.mount('#app');
リアクティブプロパティの変更
リアクティブな provide / inject の値を使う場合、リアクティブなプロパティに対しての変更は可能な限り提供する側(provide 側)で行うこと推奨されます。
もし、inject 側で値を操作したい場合は、provide 側で更新メソッドを定義して、inject 側で更新メソッドも inject します。
const app = Vue.createApp({}); app.component('parent-div', { template: ` <div id="parent"> <input v-model="title"> <foo-div /> </div>`, setup() { const title = Vue.ref('Vue3'); //更新メソッドを提供 const updateTitle = () => { title.value = 'New Title' }; Vue.provide('title', title); //provide メソッドで更新メソッドを登録 Vue.provide('updateTitle', updateTitle); return { title }; }, }).component('foo-div', { template: ` <div id="foo"> <child-div /> </div>` }).component('child-div', { template: ` <div id="child"> <p>child: {{ title }}</p> <button v-on:click="updateTitle">タイトル更新</button> </div>`, setup() { const title = Vue.inject('title'); //更新メソッドを注入 const updateTitle = Vue.inject('updateTitle'); return { title, updateTitle }; } }); app.mount('#app');
readonly 渡したデータが変更されないように
provide で渡したデータが、注入されたコンポーネント内で変更されないようにしたい場合は、提供するプロパティに readonly メソッドを適用して値を読み取り専用にすることができます。
以下は 子コンポーネントで provide された値を変更するクリックイベントを設定していますが、provide でデータを渡す際に、readonly で値を読み取り専用にしているので、クリックしても値は変更されません。
クリックすると、[Vue warn] Set operation on key "value" failed: target is readonly.のような警告がコンソールに出力されます。
本来、inject 側で provide された値を変更すべきではありません
const app = Vue.createApp({}); app.component('parent-div', { template: ` <div id="parent"> <foo-div /> </div>`, setup() { const title = Vue.ref('Vue3'); //readonly メソッドで値を読み取り専用に Vue.provide('title', Vue.readonly(title)); return { title }; }, }).component('foo-div', { template: ` <div id="foo"> <child-div /> </div>` }).component('child-div', { template: ` <div id="child"> <p>child: {{ title }}</p> <button v-on:click="title='child で変更'">タイトル変更</button> </div>`, setup() { const title = Vue.inject('title'); return { title }; } }); app.mount('#app');
Composables コンポーザブル
Options API では computed や data などを記述する場所が決まっているため、コンポーネントからロジックを分離することが困難でしたが、Composition API では、computed メソッドや ref メソッドを使うことで標準的な JavaScript として記述できるため、ロジックを切り出すことが容易になりました。
以下は Composition API で記述したカウンターの例です(すでに何度か同じコードを使用しています)。
<div id="app"> <counter-div v-bind:initial-count="0"></counter-div> </div>
const app = Vue.createApp({}); app.component('counter-div', { // props オプション props: { initialCount : { type: Number, default: 1 } }, // template オプション template: `<div>Count: {{ count }} <div> <button type="button" v-on:click="countUp">Increase</button> <button type="button" v-on:click="countDown">Decrease</button> </div> </div> `, // setup メソッド内でコンポーネントに必要なものを定義 setup(props, context) { // データオブジェクトの宣言 const count = Vue.ref(props.initialCount); // イベントリスナー(メソッド)の定義 const countUp = () => { count.value ++; }; // イベントリスナー(メソッド)の定義 const countDown = () => { count.value --; } // 定義したものをまとめて戻り値にして返す return { count, countUp, countDown } } }); app.mount('#app');
以下は上記のコンポーネントからカウンターののロジックを関数 useCounter として切り出した例です。
コンポーザブル関数
ロジックを分離し(カプセル化し)、再利用する目的で切り出した関数を Composable Function (コンポーザブル関数、またはコンポジション関数)と呼びます(Composables)。
//切り出したカウンターのためのロジック(コンポーザブル関数) const useCounter = (initialCount) => { //カウンターで使用する変数(データオブジェクト) const count = Vue.ref(initialCount); //カウンターで使用するメソッド const countUp = () => { count.value ++; }; //カウンターで使用するメソッド const countDown = () => { count.value --; } //カウンターで使用するオブジェクト(変数とメソッド)を返す return { count, countUp, countDown } } //コンポーザブル関数を利用するコンポーネント const app = Vue.createApp({}); app.component('counter-div', { // props オプション (同じ) props: { initialCount : { type: Number, default: 1 } }, // template オプション (同じ) template: `<div>Count: {{ count }} <div> <button type="button" v-on:click="countUp">Increase</button> <button type="button" v-on:click="countDown">Decrease</button> </div> </div> `, // setup メソッド setup(props, context) { //コンポーザブル関数(useCounter)を呼び出し、その結果を変数に代入(分割代入) const {count, countUp, countDown } = useCounter(props.initialCount); //この例では単にコンポーザブル関数の結果を setup の戻り値に return { count, countUp, countDown } } }); app.mount('#app');
この例の場合、コンポーザブル関数(useCounter)からの戻り値を setup メソッドの戻り値にしているだけなので、上記の setup メソッド(40〜49行目)はスプレッド構文を使って以下のように記述しても同じです。
setup(props, context) { return { ...useCounter(props.initialCount) } }
コンポーザブル関数は一般的に以下のようなルールで定義します。
- 戻り値は、setup メソッドで利用する変数や関数からなるオブジェクト
- 関数名は通常(慣例的に) use から始める(useXxxx)
カスタムディレクティブ
Vue 本体で提供されているディレクティブに加えて、独自のカスタムディレクティブ (custom directives) を登録することができます。
Vue ではコードの再利用の基本はコンポーネントとコンポーザブルですが、単純な要素に対する低レベルの DOM アクセスを含むロジックの再利用はカスタムディレクティブを利用することができます。
コンポーネント内では単純な DOM 要素(文書ツリー)の操作のコードを記述するのは避けるべきで、単純な DOM 要素へのアクセスが必要な場合はカスタムディレクティブを使用します。
言い換えると、カスタムディレクティブは、DOM を直接操作することで目的の機能を実現できる場合にのみ使用します。
以下はページを読み込むと、v-focus というカスタムディレクティブを指定した要素にフォーカスが当たります。
<div id="app"> <input v-focus /> </div>
const app = Vue.createApp({}); // v-focus というグローバルカスタムディレクティブを登録 app.directive('focus', { // バインドされた要素が DOM にマウントされるタイミングで mounted(el) { // その要素にフォーカスを当てる el.focus(); } }); app.mount('#app');
directive メソッド
カスタムディレクティブは directive メソッドを使って定義することができます。
directive(name, definition)
- name:ディレクティブの名前(接頭辞の v- は付けない)
- definition:ディレクティブの定義オブジェクト(実行タイミングと関数からなるオブジェクト)
ディレクティブの定義オブジェクトは、「どのタイミングでどのような処理をするか」を関数(フック関数)で指定し、複数指定することができます。
利用できるタイミングは以下になり、それぞれのタイミングで実行する関数をフック関数と呼びます。
フック関数 | 概要(タイミング) |
---|---|
created | バインドされた要素の属性やイベントリスナが適用される前。v-on イベントリスナの前に呼ばれなければならないイベントリスナをつける必要がある場合に利用できます。 |
beforeMount | ディレクティブが最初に要素にバインドされたとき、親コンポーネントがマウントされる前 |
mounted | バインドされた要素の親コンポーネントとそのすべての子がマウントされた時。ディレクティブの挙動の初期化などに利用できます。 |
beforeUpdate | バインドされた要素を含むコンポーネントの VNode が更新される前(コンポーネント配下の要素が更新される前) |
updated | バインドされた要素を含むコンポーネントの VNode とその子コンポーネントの VNode が更新された後(コンポーネント配下の要素が更新された後) |
beforeUnmount | バインドされた要素の親コンポーネントがアンマウントされる前 |
unmounted | ディレクティブが要素からバインドを解除された時、また親コンポーネントがアンマウントされた時に 1 度だけ呼ばれます |
フック関数に渡される引数
フック関数が受け取る引数は以下になります(いずれの引数も省略可能)。
引数 | 概要 |
---|---|
el | ディレクティブが適用される(バインドされる)要素。 |
binding | このオブジェクトは、以下のプロパティを持ちます。
|
vnode | el で受け取った DOM 要素の仮想ノード(VNode) |
prevNode | 変更前の仮想ノード(VNode)。beforeUpdate および updated フックでのみ利用可能 |
※ 引数の el 以外のすべての引数は読み取り専用であり、フック関数の中で変更してはいけません。
ディレクティブに渡す値
ディレクティブに渡す値(属性値)は任意の型の値を渡すことができ data プロパティなどとバインドすることができます。そしてフック関数の中で binding.value でアクセスできます。
以下で定義したディレクティブ v-text-color は、その要素の文字色(el.style.color)を、ディレクティブに渡した値(binding.value)に設定します。
const app = Vue.createApp({ data() { return { myColor: 'green' } } }).directive('text-color', { mounted(el, binding) { // binding.value はディレクティブに渡した値 el.style.color = binding.value } }); app.mount('#app');
const app = Vue.createApp({ setup(){ const myColor = Vue.ref('green'); return { myColor }; } }).directive('text-color', { mounted(el, binding) { el.style.color = binding.value } }); app.mount('#app');
テンプレートの1つ目の div 要素のディレクティブ v-text-color には data プロパティの myColor が指定されているのでその値の緑色に、2つ目の div は文字列 'red' が渡されているので赤になります。
<div id="app"> <div v-text-color="myColor">Hello, it's green.</div><!-- 文字は緑色 --> <div v-text-color="'red'">Hello, it's red.</div><!-- 文字は赤色 --> </div>
<div id="app" data-v-app=""> <div style="color: green;">Hello, it's green.</div> <div style="color: red;">Hello, it's red.</div> </div>
オブジェクトリテラル
ディレクティブに複数の値が必要な場合、JavaScript のオブジェクトリテラルを渡すこともできます。属性値には任意の型の値(あらゆる妥当な JavaScript 式)を渡すことができます。
以下は属性値(ディレクティブの値)にオブジェクトを指定する例です。
<div id="app"> <div v-demo="{ color: 'white', bg: 'red', text: 'hello!' }"></div> </div>
const app = Vue.createApp({}) .directive('demo', { mounted(el, binding) { el.style.color = binding.value.color; el.style.backgroundColor = binding.value.bg; el.textContent = binding.value.text; } }); app.mount('#app');
以下も属性値にオブジェクトを指定する例です。この例の場合、属性値が指定されていない場合の既定値を設定しています。
const app = Vue.createApp({}) .directive('my-style', { mounted(el, binding) { //属性値が指定されている場合 if(binding.value) { //オブジェクトの color プロパティが指定されていればその値を、そうでなければ green el.style.color = binding.value.color? binding.value.color : 'green'; el.style.fontSize = binding.value.fontSize ? binding.value.fontSize : '20px'; }else{ //属性値が指定されていない場合 el.style.color = 'green'; el.style.fontSize = '20px'; } } }); app.mount('#app');
<div id="app"> <div v-my-style="{color:'red', fontSize:'40px'}">Hello</div><!-- 赤で40px--> <div v-my-style="{fontSize:'50px'}">Hello</div><!-- 緑で50px--> <div v-my-style="{color:'blue'}">Hello</div><!-- 青で20px--> <div v-my-style>Hello</div><!-- 緑で20px--> </div>
ローカルディレクティブ
コンポーネントの directives オプションを使うと、特定のインスタンス配下でのみ有効なディレクティブを定義することができます。
const app = Vue.createApp({}); app.component('focused-input', { template: `<input v-focus />`, // directives オプション(ローカルディレクティブの定義) directives: { //ディレクティブの名前 focus: { //ディレクティブの定義オブジェクト( mounted(el) { el.focus() } } } }); app.mount('#app');
<div id="app"> <focused-input /> </div>
ディレクティブに引数を渡す
ディレクティブ名の後にコロン(:)区切りで、ディレクティブの引数を指定することができます。
<elem v-my-directive:引数="ディレクティブの値">
フック関数の中では引数を binding.arg で参照することができ、引数の値は文字列として取得できます。
以下で定義したディレクティブ v-color は、引数に「background-color」が指定されていれば背景色を、引数が指定されていなければ文字色を、ディレクティブに渡した値の色にします。
この例では引数に静的な値(文字列)を指定していますが、動的な値を指定することもできます。
const app = Vue.createApp({ data() { return { myColor: 'green', } } }).directive('color', { mounted(el, binding) { // binding.arg はディレクティブに渡した引数 const prop = binding.arg || 'color'; el.style[prop]=binding.value; } }); app.mount('#app');
1つ目の div 要素の文字色は引数に background-color が指定されているので背景色が、2つ目の div は引数が指定されていないので文字色が myColor の値(緑)になります。
<div id="app"> <div v-color:background-color="myColor">Background is green.</div><!--背景色が緑--> <div v-color="myColor">Text is green.</div><!-- 文字色が緑 --> </div>
以下は switch 文を使って引数に指定された値に応じて要素に色を設定する例です。
引数の値として background、border、text 以外が指定された場合は、文字色を設定するようにしています。
const app = Vue.createApp({ data() { return { myColor: 'green', } } }).directive('color', { mounted(el, binding) { //引数の値に応じて色を付ける switch(binding.arg) { case 'background': el.style.backgroundColor = binding.value; break; case 'border': el.style.borderStyle = 'solid'; el.style.borderWidth = '1px'; el.style.borderColor = binding.value; break; case 'text': el.style.color = binding.value; break; default: //引数が省略されたり、値が無効な場合 el.style.color = binding.value; } } }); app.mount('#app');
<div id="app"> <div v-color:border="myColor">Hello!</div><!--緑のボーダー--> <div v-color:background="'yellow'">Hello!</div><!--黄色の背景色 --> </div>
動的なディレクティブ引数
v-bind や v-on 同様、[ ] で囲むことで JavaScript 式をディレクティブの引数に使うことができます。
例えば、v-mydirective:[argument]="value" において、argument はコンポーネントインスタンスの data プロパティに基づいて更新されます。
以下は v-pin ディレクティブの動的な引数 [direction] の値により、要素をピン留め(固定表示)する基準を変更できるようにした例です。
動的な引数 [direction] には、要素をピン留めする基準の値として、data プロパティの direction に right や bottom、left、top を指定します(デフォルトは top)。
v-pin ディレクティブの値には、基準からの位置を data プロパティの pinPadding に数値で指定します。
<div id="app"> <p v-pin:[direction]="pinPadding"> {{ pinPadding }}px from the {{ direction || 'top' }} </p> </div>
この例の場合、data プロパティの direction に right を指定しているので、右端から pinPadding で指定した 200px の位置に固定(fixed)表示されます。
data プロパティの direction に bottom などを指定することで固定する基準を変更することができます。
ディレクティブに引数が指定されていないか、data プロパティの direction の値が空文字の場合は、top が適用されます(data プロパティに direction が定義されていない場合は警告が出力されます)。
const app = Vue.createApp({ data() { return { direction: 'right', //ディレクティブの引数 pinPadding: 200 //ディレクティブの値 } } }).directive('pin', { mounted(el, binding) { el.style.position = 'fixed' // binding.arg はディレクティブに渡した引数 const s = binding.arg || 'top' el.style[s] = binding.value + 'px' } }); app.mount('#app');
属性値の変化を検出
以下は前述の v-pin ディレクティブの値(pinPadding )を input 要素のスライダーの値で、引数の値(direction)をセレクトボックスの値で変更するようにした例ですが、実際には期待通りに動作しません。
<div id="app"> <input type="range" min="0" max="500" v-model="pinPadding"> <select v-model="direction"> <option value="top">top</option> <option value="right">right</option> <option value="bottom">bottom</option> <option value="left">left</option> </select> <p v-pin:[direction]="pinPadding"> {{ pinPadding }}px from the {{ direction || 'top' }} </p> </div>
const app = Vue.createApp({ data() { return { direction: 'right', pinPadding: 200 } } }).directive('pin', { //マウントされた時に一度だけ呼び出される mounted(el, binding) { el.style.position = 'fixed' const s = binding.arg || 'top' el.style[s] = binding.value + 'px' } }); app.mount('#app');
上記のままでは、スライダーやセレクトボックスで変更した値は v-model によってテンプレート構文(Mustache 構文)の {{ xxxx }} の部分には反映されますが、ディレクティブには反映されません(初期状態のまま)。
これは、フック関数 mounted は、バインドされた要素のコンポーネントがマウントされた時に一度だけ呼び出され、更新時には呼び出されないためです。
update フック関数を追加
コンポーネントの更新を検知するには、update フック関数を追加で定義する必要があります。
17〜20行目は、top や right などの CSS での位置の指定を一旦クリアするための記述です(これらの CSS プロパティは複数指定できてしまうため)。
const app = Vue.createApp({ data() { return { direction: 'right', pinPadding: 200 } } }).directive('pin', { mounted(el, binding) { el.style.position = 'fixed'; const s = binding.arg || 'top'; el.style[s] = binding.value + 'px'; }, //update フック関数を定義(更新を検知) updated(el, binding) { //位置の指定を初期化 el.style.top = null; el.style.right = null; el.style.bottom = null; el.style.left = null; const s = binding.arg; el.style[s] = binding.value + 'px'; } }); app.mount('#app');
mounted/updated をまとめて定義
以下はカスタムディレクティブ v-text-color の値をセレクトボックスで変更できるようにした例です。
<div id="app"> <select v-model="myColor"> <option value="green">Green</option> <option value="blue">Blue</option> <option value="red">Red</option> <option value="orange">Orange</option> </select> <div v-text-color="myColor">Hello!</div> </div>
マウントされた時(mounted)と更新された時(updated)のタイミングで同じ処理を行っています。
const app = Vue.createApp({ data() { return { myColor: 'green' } } }).directive('text-color', { //mounted フック関数(マウントされた際に1回だけ実行) mounted(el, binding) { el.style.color = binding.value; }, //updated フック関数(更新時に毎回実行) updated(el, binding) { el.style.color = binding.value; } }); app.mount('#app');
マウントされた時と更新された時に同じ処理を行い、他のフックは必要ない場合は、以下のようにディレクティブにコールバックを渡すことで簡潔に記述することができます。
directive メソッドの第2引数にオブジェクトを渡す代わりに、関数リテラルを渡すことで mounted と updated をまとめて定義することができます(関数による省略記法)。
const app = Vue.createApp({ data() { return { myColor: 'green' } } //関数による省略記法(第2引数に関数リテラルを渡す) }).directive('text-color', (el, binding) => { //mounted と updated で実行される処理 el.style.color = binding.value; }); app.mount('#app');
値が変化したかを判定
以下は前述の例にテキストボックスを追加し、表示する文字(message)を変更できるようにした例です。
<div id="app"> <select v-model="myColor"> <option value="green">Green</option> <option value="blue">Blue</option> <option value="red">Red</option> <option value="orange">Orange</option> </select> <input type="text" v-model="message"> <div v-text-color="myColor">{{ message }}</div> </div>
updated フック関数はディレクティブの値(binding.value)が変化したかどうかに関わらず、親コンポーネントに変化があった場合に毎回呼び出されます。
この例で追加したテキストボックスは、v-text-color ディレクティブを利用していませんが、テキストボックスに文字を入力したり変更すると updated フック関数が毎回呼び出され、コンソールに binding.value が出力されるのが確認できます。
const app = Vue.createApp({ data() { return { myColor: 'green', message: 'Hello!' } } //関数による省略記法(mounted/updated をまとめて定義) }).directive('text-color', (el, binding) => { el.style.color = binding.value; console.log(binding.value); //呼び出されるたびにコンソールへ出力 }); app.mount('#app');
binding.oldValue で確認
ディレクティブの値に変化がないのに、フック関数が繰り返し呼び出されるのは無駄なので、ディレクティブの値(binding.value)が変化していないかを binding.oldValue(変更前の値)と比較して、変化している場合にのみ処理を実行するようにします。
const app = Vue.createApp({ data() { return { myColor: 'green', message: 'Hello!' } } }).directive('text-color', (el, binding) => { //ディレクティブの値に変化がなければ終了 if(binding.value === binding.oldValue) { return; } el.style.color = binding.value; console.log(binding.value); }); app.mount('#app');
修飾子付きのディレクティブ
ディレクティブ名の後にドット(.)区切りで、ディレクティブの修飾子を指定することができます。
フック関数の中では修飾子を binding.modifiers で参照することができ、「binding.modifiers.名前」で真偽値(その修飾子が指定されていれば true、指定されていなければ false)が返ってきます。
以下は指定した値の文字色を適用する v-text-color というディレクティブに、italic、bold、underline という修飾子を追加する例です。
const app = Vue.createApp({ data() { return { myColor: 'green' } } }).directive('text-color', { mounted(el, binding) { el.style.color = binding.value; //修飾子 italic が指定されていれば if(binding.modifiers.italic) { el.style.fontStyle = 'italic'; } //修飾子 bold が指定されていれば if(binding.modifiers.bold) { el.style.fontWeight = 'bold'; } //修飾子 underline が指定されていれば if(binding.modifiers.underline) { el.style.textDecoration = 'underline'; } } }); app.mount('#app');
修飾子は複数同時に指定することができます。
<div id="app"> <div v-text-color.italic="myColor">Hello!</div> <!-- 緑色で斜体 --> <div v-text-color.bold="myColor">Hello!</div> <!-- 緑色で太字 --> <div v-text-color.italic.bold="myColor">Hello!</div> <!-- 緑色で斜体で太字 --> <div v-text-color.underline.italic="myColor">Hello!</div> <!-- 緑色で下線付き斜体 --> </div>
ディレクティブでイベント処理
ディレクティブでイベントリスナーを設定することもできます。
フック関数の中で、ディレクティブがバインド(適用)される要素を表す引数 el に addEventListener を使ってイベントリスナーを登録することができます。
以下の v-mo-color ディレクティブはマウスオーバーすると背景色を付け、マウスアウトすると背景色を元に戻します。初期化のタイミングで呼び出される mounted フック関数でディレクティブが適用される要素にイベントリスナーを登録しています。
const app = Vue.createApp({ data() { return { myColor: 'lightgreen', } } }).directive('mo-color', { mounted(el, binding) { //ディレクティブが適用される要素にイベントリスナーを登録 el.addEventListener('mouseenter', (e) => { el.style.backgroundColor = binding.value; //または e.currentTarget.style.backgroundColor = binding.value; }); el.addEventListener('mouseleave', (e) => { el.style.backgroundColor = null; //または e.currentTarget.style.backgroundColor = null; }); } }); app.mount('#app');
<div id="app"> <div v-mo-color="myColor">Hello!</div> </div>
以下は、ディレクティブを指定した要素を固定表示し、現在のスクロール量を表示する例です。親要素にある程度の高さがないとスクロールしないので、親要素に高さを指定しています。
<div style="margin-top:50px; height: 2000px;"> <div id="app"> <div v-log-scroll>window.scrollY</div> </div> </div>
const app = Vue.createApp({}) .directive('log-scroll', { mounted(el) { window.addEventListener('scroll', () => { el.style.position = 'fixed'; el.textContent = 'window.scrollY : ' + window.scrollY; console.log(window.scrollY); }) } }); app.mount('#app');
引数や属性値を利用
以下は指定された引数により異なるイベントを登録し、属性値(ディレクティブの値)により実行する処理を変える例です。
ディレクティブ名の後にコロン(:)区切りで指定された引数の値(文字列)は binding.arg で取得でき、属性値に指定されているメソッドは binding.value()で実行できます。
const app = Vue.createApp({ methods: { //属性値に指定するメソッド alertHello() { alert('hello'); }, logHello() { console.log('hello'); } } }).directive('hello', { mounted(el, binding) { //引数に mouseenter が指定されていれば mouseenter イベントを登録 if (binding.arg === 'mouseenter') { el.addEventListener("mouseenter", () => { //属性値に指定されているメソッドを実行 binding.value(); }); //引数に dblclick が指定されていれば dblclick イベントを登録 }else if (binding.arg === 'dblclick') { el.addEventListener("dblclick", () => { binding.value(); }); //引数が指定されていない(上記のいずれでもない)場合は click イベントを登録 }else{ el.addEventListener("click", () => { binding.value(); }); } } }); app.mount('#app');
<div id="app"> <div v-hello:mouseenter="logHello">Mouse Enter</div><!--マウスオーバーでログ出力--> <div v-hello:dblclick="alertHello">Double Click</div><!--ダブルクリックでアラート--> <div v-hello="alertHello">Click</div><!--クリックでアラート--> </div>
修飾子を利用
以下は前述の例に、修飾子のオプションを追加したものです。ディレクティブに修飾子 bye が指定されていれば、アラートやログで出力する文字を変えています。
const app = Vue.createApp({ methods: { alertHello(bye) { //引数が true であれば if(bye) { alert('hello and goodbye'); }else{ alert('hello'); } }, logHello(bye) { if(bye) { console.log('hello and goodbye'); }else{ console.log('hello'); } } } }).directive('hello', { mounted(el, binding) { if (binding.arg === 'mouseenter') { el.addEventListener("mouseenter", () => { //修飾子 bye が指定されていれば引数に true を指定 binding.modifiers.bye ? binding.value(true) : binding.value(); }); }else if (binding.arg === 'dblclick') { el.addEventListener("dblclick", () => { binding.modifiers.bye ? binding.value(true) : binding.value(); }); }else{ el.addEventListener("click", () => { binding.modifiers.bye ? binding.value(true) : binding.value(); }); } } }); app.mount('#app');
<div id="app"> <!--ダブルクリックで「hello」とアラート表示--> <div v-hello:dblclick="alertHello">Double Click</div> <!--クリックで「hello and goodbye」とコンソールに出力--> <div v-hello.bye="logHello">Click</div> </div>
ミックスイン
ミックスイン (mixin) はコンポーネントオプションを再利用するための仕組み(機能)です。
mixin で定義した共通なオプション(data、methods、computed、life cycle)を component 内で使用できるようになります。
但し、以下のような 欠点があるため、Vue3 からはミックスインの使用は非推奨とされていて、 Composition API(Composables)が推奨されています。
- プロパティのソースが不明確
- 名前空間の競合
- 暗黙的なクロスミックスイン通信
以下はコンポーネントの data オプション(データオブジェクト)の内容と「hello from mixin!」という文字列をコンソールに出力するミックスインです。
ミックスインは「オプション名: 値」形式で記述されたオブジェクトリテラルで、任意のコンポーネントオプションを含むことができます。
data、methods、computed、life cycle などコンポーネントで利用できる全てのオプションを利用でき、メソッドやライフサイクルフックでコンポーネントのインスタンスにアクセスすることができます。
以下の例では、created ライフサイクルフックでコンポーネントのデータオブジェクト($data インスタンスプロパティ)を参照しています。
コンポーネントにミックスインを組み込むには mixins オプションを利用します。mixins オプションでは、複数のミックスインを組み込めるので配列として指定します。
//ミックスインオブジェクトを定義 const myMixin = { created() { this.hello(); //このミックスインのメソッド hello() を実行 console.log(this.$data); //コンポーネントの data オプションの内容を出力 }, methods: { hello() { console.log('hello from mixin!'); } } } //ミックスインを使用するコンポーネントを定義 const app = Vue.createApp({}) .component('my-compo', { data() { return { current: new Date().toLocaleString() }; }, template: `<div>{{ current }}</div>`, //mixins オプションでミックスインを組み込む mixins: [myMixin] }); app.mount('#app');
<div id="app"> <my-compo></my-compo> </div>
以下がコンソールに出力されます。
hello from mixin! Proxy {current: '2022/9/22 19:52:34'}
ブラウザには以下が出力されます。
<div id="app" data-v-app=""> <div>2022/9/22 19:52:34</div> </div>
オプションのマージ
ミックスインとコンポーネントで同じオプションが定義されている(重複したオプションを含む)場合、以下のようなルールでマージされます。
data オプション
各ミックスインはそれぞれの data オプションを持つことができます。それぞれのオプション(関数)が呼び出され、返されたオブジェクトがマージされます。コンフリクトした場合(同名のプロパティがある場合)には、コンポーネント自身の data のプロパティが優先されます。
//ミックスイン const myMixin = { //data オプション(オブジェクトを返す関数) data() { return { message: 'hello', foo: 'Foo' } } } //ミックスインを使用するコンポーネント const app = Vue.createApp({ mixins: [myMixin], //data オプション(オブジェクトを返す関数) data() { return { //data オプション(オブジェクトを返す関数) message: 'goodbye', bar: 'Bar' } }, created() { //オブジェクトがマージされ、同名のプロパティはコンポーネントのプロパティが優先される console.log(this.$data) // => { message: "goodbye", foo: "Foo", bar: "Bar" } } }) app.mount('#app');
オブジェクトの値を期待するオプション
methods や computed、components、directives などのオブジェクトの値を期待するオプションは、data オプション同様、同じオブジェクトにマージされ、これらのオブジェクトでキーのコンフリクトがある場合は、コンポーネントオプションが優先されます。
//ミックスイン const myMixin = { methods: { foo() { console.log('foo') }, conflicting() { console.log('from mixin') } } } //ミックスインを使用するコンポーネント const app = Vue.createApp({ mixins: [myMixin], methods: { bar() { console.log('bar') }, //同名のメソッド conflicting() { console.log('from component') } } }) //コンポーネントのインスタンスを変数 vm に代入 const vm = app.mount('#app'); vm.foo() // => "foo" vm.bar() // => "bar" //同名のメソッド(コンポーネントオプションが優先される) vm.conflicting() // => "from component"
ライフサイクルフック
同じ(名前の)ライフサイクルフックはそれら全てが呼び出されるよう配列にマージされます。ミックスインのフックはコンポーネント自身のフックの前に呼び出されます。
//ミックスイン const myMixin = { created() { console.log('mixin hook called') } } //ミックスインを使用するコンポーネント const app = Vue.createApp({ mixins: [myMixin], //同名のライフサイクルフック created() { console.log('component hook called') } }) app.mount('#app'); //以下が出力される //mixin hook called //component hook called
カスタムオプション
カスタムオプションは単純に上書きされます。カスタムオプションはインスタンスプロパティの $options でアクセスできます。
//ミックスイン const myMixin = { //カスタムオプション myOption: 'Mixin' } //ミックスインを使用するコンポーネント const app = Vue.createApp({ mixins: [myMixin], //同名のカスタムオプション myOption: 'Component' }) const vm = app.mount('#app'); console.log(vm.$options.myOption); //Component
カスタムロジックを使用してカスタムオプションをマージ
カスタムオプションはデフォルトでは単純に上書きされますが、カスタムロジックを使用してカスタムオプションを独自のルールでマージすることもできます。
カスタムオプションが重複した場合のルールは optionMergeStrategies を使います。使い方は、「app.config.optionMergeStrategies.カスタムオプション名」にルールを表す関数を定義します。
app.config.optionMergeStrategies.customOption = (toVal, fromVal) => { // マージされた値を返す }
ルールを表す関数は、親インスタンスと子インスタンスで定義されたオプションの値をそれぞれ第 1 引数と第 2 引数として受け取り、メージの結果を戻り値として返します。
- toVal:マージ先の値
- fromVal:マージ元の値
以下はルールを表す関数の引数に何が入っているかを確認する例です。
//ミックスイン const myMixin = { //カスタムオプション myOption: 'Mixin' }; //ミックスインを使用するコンポーネント const app = Vue.createApp({}) .component('my-compo', { //重複したカスタムオプション myOption: 'Component', template: `<div>{{ $options.myOption }} </div>`, mixins: [myMixin], created() { console.log('created : ' + this.$options.myOption); // => created : Component } }); //マージルールを定義(マウントする前) app.config.optionMergeStrategies.myOption = (toVal, fromVal) => { console.log('toVal(マージ先の値): ' + toVal); console.log('fromVal(マージ元の値): ' + fromVal); return fromVal || toVal; } app.mount('#app');
上記を実行すると、以下のように最初にミックスインから、次にコンポーネントから出力された toVal と fromVal がコンソールに表示されます(関数がそれぞれで実行される)。
24行目の return では fromVal が存在する場合には常にそれが返されるため、最終的には this.$options.myOption に Component がセットされます。
toVal(マージ先の値): undefined fromVal(マージ元の値): Mixin toVal(マージ先の値): Mixin fromVal(マージ元の値): Component created : Component //created() フック
常に子インスタンスの値を返すようにするには、マージルールを以下のようにします。
app.config.optionMergeStrategies.myOption = (toVal, fromVal) => { //toVal が存在する場合には常に toVal を返す return toVal || fromVal }
以下は、カスタムオプションとして配列を用意し、競合した場合は配列を連結する例です。
この例の場合、引数 toVal と fromVal が undefined の場合の既定値として空の配列を指定しています。
//ミックスイン const myMixin = { //カスタムオプション myOption: ['foo', 'bar'] }; //ミックスインを使用するコンポーネント const app = Vue.createApp({}) .component('my-compo', { //重複したカスタムオプション myOption: ['baz'], template: `<div>{{ $options.myOption }} </div>`, mixins: [myMixin], created() { console.log(this.$options.myOption); // =>(3) ['baz', 'foo', 'bar'] } }); //マージルールを定義(引数 toVal と fromVal が undefined の場合の既定値を指定) app.config.optionMergeStrategies.myOption = (toVal=[], fromVal=[]) => { return fromVal.concat(toVal) } app.mount('#app');
<div id="app"> <my-compo></my-compo> </div>
以下のように出力されます。
<div id="app" data-v-app=""> <div>["baz","foo","bar"]</div> </div>
もし、引数 toVal と fromVal に既定値として空の配列を指定しない場合は、以下のような出力になります。
<div id="app" data-v-app=""> <div>["baz","foo","bar", null]</div> </div>
グローバルミックスイン
mixin メソッドを使って無条件に全てのコンポーネントに適用されるグローバルミックスインを定義することもできます。
コンポーネント側では、mixins オプションを指定する必要はありません。
const app = Vue.createApp({ myOption: 'hello!' }); // myOption オプションが存在すれば、その値をコンソールに出力するグローバルミックスイン app.mixin({ created() { const myOption = this.$options.myOption if (myOption) { console.log(myOption); //hello! } } }); app.mount('#app');
グローバルにミックスインを適用すると、その後にアプリ内で作成される全ての Vue コンポーネントインスタンスに影響するので注意が必要です。
const app = Vue.createApp({ myOption: 'hello!' }) // `myOption` カスタムオプションにハンドラを注入する app.mixin({ created() { const myOption = this.$options.myOption if (myOption) { console.log(myOption) } } }); // myOption を子コンポーネントにも追加 app.component('my-compo', { myOption: 'hello from child component!', template: `<div>{{ this.$options.myOption }}</div>` }) app.mount('#app'); //それぞれのコンポーネントでログが出力される //hello! //hello from child component!
以下は、コンポーネントの data オブジェクトに title プロパティが存在すれば、<title> 要素の値(document.title)に設定する例です。
const app = Vue.createApp({}) .mixin({ created() { //data オブジェクトに title プロパティが存在すれば if(this.$data.title) { //data の title プロパティを document.title(title 要素の値)に設定 document.title = this.$data.title; // または document.querySelector('title').textContent = this.$data.title; } } }); app.component('my-compo', { template: `<div>My Compo</div>`, data() { return { //title プロパティが存在するので、この値が title 要素に反映される title: 'Global Mixin!' } } }) app.mount('#app');
<head> 内の title 要素は以下のように出力されます。
<title>Global Mixin!</title>
プラグイン
プラグインを利用すると目的に特化した機能を Vue に追加することができます。
Vue で利用できるプラグインは以下のページで探すことができます(プラグイン以外にもフレームワークやライブラリなど、Vue 関連のリストが多数掲載されています)。
github.com/vuejs/awesome-vue | awesome-vue.js.org
プラグインをインストールして利用するには、使用するプラグインを読み込んで use() メソッドを使って第1引数にプラグイン、第2引数にプラグインのオプション(プラグインごとに異なる)を指定します。
例えば、myPlugin というプラグインをインストールするには以下のようになります。
const app = Vue.createApp({ /* ... */ }) app.use(myPlugin, { /* 省略可能なオプション */ });
import を使用している場合は、以下のようになります。
import { createApp } from 'vue' import MyPlugin from './plugins/MyPlugin' const app = createApp({ /* ... */ }) app.use(myPlugin, { /* 省略可能なオプション */ });
以下はサードパーティで提供されているプラグイン(ライブラリ)を利用する例です。
Element Plus
Element Plus は Vue 3 ベースのコンポーネントライブラリで、様々な UI コンポーネントが含まれています。以下は Element Plus のページへのリンクです。
- github.com/element-plus/element-plus
- Element Plus Home
- Element Plus Guide(Installation)
- Element Plus Component
以下で使用している Element Plus のバージョンは 2.2.18 です。
以下は Component のボタンのページの例です。Component にはフォームやメニュー、カルーセルなどたくさんの UI コンポーネントがあります。
Installation
パッケージマネージャ(NPM, Yarn, pnpm)を使ってインストールする方法が推奨されていますが、CDN 経由で読み込むこともできます。
# NPM $ npm install element-plus --save # Yarn $ yarn add element-plus # pnpm $ pnpm install element-plus
以下は unpkg を利用して CDN で読み込む例です。JavaScript は body の閉じタグの前で読み込むこともできます。
<head> <!-- Import style --> <link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" /> <!-- Import Vue 3 --> <script src="//unpkg.com/vue@3"></script> <!-- Import component library --> <script src="//unpkg.com/element-plus"></script> </head>
※ CDN を使用してインポートする場合は、Element Plus がアップグレードされたときに互換性のないアップデートの影響を受けないように、リンクアドレスでバージョンをロックすることが推奨されています。
バージョンをロックするには以下のように @ の後にバージョンを指定します。
<head> <!-- バージョン 2.2.18 --> <link rel="stylesheet" href="//unpkg.com/element-plus@2.2.18/dist/index.css" /> <!-- Import Vue 3 --> <script src="//unpkg.com/vue@3"></script> <!-- バージョン 2.2.18 --> <script src="//unpkg.com/element-plus@2.2.18"></script> </head>
ボタンの実装例
以下は CDN で Element Plus を読み込み、ボタン(Button コンポーネント)を実装する例です。
Element Plus のインストールは use() メソッドの第1引数にグローバル変数 ElementPlus を指定します。ボタンは <el-button> 要素で呼び出せます。
<el-button> 要素に指定できる属性は Button Attributes で確認できます。以下は type 属性に primary を指定しているので青系の色のボタンが表示されます。
<!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"> <!-- import CSS --> <link rel="stylesheet" href="//unpkg.com/element-plus@2.2.18/dist/index.css" /> <title>Vue Sample</title> </head> <body> <div id="app"> <!-- ボタン --> <el-button type="primary">{{ message }}</el-button> </div> <!-- import Vue JavaScript --> <script src="//unpkg.com/vue@3/dist/vue.global.js"></script> <!-- import Element Plus JavaScript --> <script src="//unpkg.com/element-plus@2.2.18"></script> <script> const app = Vue.createApp({ setup() { const message = "Hello Element Plus"; return { message } } }); // Element Plus をインストール(グローバル変数 ElementPlus を指定) app.use(ElementPlus); app.mount("#app"); </script> </body> </html>
パッケージマネージャを利用している場合の Element Plus のインストールは以下のようになります。
import { createApp } from 'vue'; import ElementPlus from 'element-plus'; import 'element-plus/dist/index.css'; import App from './App.vue'; const app = createApp(App); app.use(ElementPlus); app.mount('#app');
Element Plus の全てを読み込む Full Import ではなく、必要なものだけを利用することもできるようです。詳細は On-demand Import を参照。
Global configuration
use() メソッドを使って Element Plus を登録する際に、form コンポーネントの size と popup コンポーネントの zIndex を設定することができます。zIndex のデフォルト値は 2000 です。
以下のように use() メソッドの第2引数のオプションにグローバル設定オブジェクト(global config object)を渡します。
const app = createApp(App); app.use(ElementPlus, { size: 'small', //form コンポーネントのサイズを small に zIndex: 3000 //popup コンポーネントの zIndex を 3000 に });
Internationalization
Element Plus のコンポーネントはデフォルトで英語を使用していますが、他の言語を使用したい場合は use() メソッドの第2引数のオプション(グローバル設定オブジェクト)に指定することができます。
※DatePicker や TimePicker などのコンポーネントで日本語のロケールを使用する場合に設定します。ボタンやカルーセルなどのコンポーネントを利用する場合は不要です。
import { createApp } from 'vue'; import ElementPlus from 'element-plus'; //日本語リソースをインポート import Ja from 'element-plus/dist/locale/ja.mjs' import 'element-plus/dist/index.css'; import App from './App.vue'; const app = createApp(App); app.use(ElementPlus, { locale: Ja, //ロケールを日本語に }); app.mount('#app');
CDN の場合
CDN で利用している場合は、以下のようにします。
<script src="//unpkg.com/vue@3/dist/vue.global.js"></script> <script src="//unpkg.com/element-plus@2.2.18"></script> <!-- 日本語リソースをインポート --> <script src="//unpkg.com/element-plus@2.2.18/dist/locale/ja"></script> <script> const app = Vue.createApp({}); app.use(ElementPlus, { //ElementPlusLocale の後に Ja(言語コード)を続ける locale: ElementPlusLocaleJa, }) app.mount("#app"); </script>
カルーセルの実装例
以下は画像を表示するカルーセル(Carousel)の実装例です。
コンポーネントの呼び出しでは <el-carousel> 要素でカルーセル全体を囲み、カルーセルの各コンテンツを <el-carousel-item> 要素に記述します。
この例の <el-carousel> 要素には以下のような属性を指定しています(ドキュメントの Carousel Attributes でどのような属性を指定できるかを確認できます)。
- trigger:インジケータをクリックしたら画像が変わるように click を指定(デフォルトは hover)
- height:カルーセルの高さ
- interval:コンテンツが切り替わる間隔(ミリ秒)
コンテンツは1つずつ記述しても構いませんが、以下では v-for でデータ(images)から展開しています。
また、以下の例ではキャプション用の class="caption" を指定した div 要素と、絶対配置するキャプションの基準となる class="item-inner" を指定した div 要素を追加しています。
キャプションは v-text を使用して出力しています。
<div id="app"> <!-- カルーセル --> <el-carousel trigger="click" height="600px" v-bind:interval="4000"> <el-carousel-item v-for="{src, alt, caption} in images" :key="src"> <div class="item-inner"><!-- キャプションを表示する基準とする div 要素を追加 --> <img v-bind:src="src" v-bind:alt="alt"> <div v-text="caption" class="caption"></div><!-- キャプション --> </div> </el-carousel-item> </el-carousel> </div>
setup() では v-for で使用するコンテンツのデータ(images)をオブジェクトの配列で定義しています。
const app = Vue.createApp({ setup() { const images = [ {src: 'images/01.jpg', alt: 'alt text1', caption: 'caption1'}, {src: 'images/02.jpg', alt: 'alt text2', caption: 'caption2'}, {src: 'images/03.jpg', alt: 'alt text3', caption: 'caption3'}, {src: 'images/04.jpg', alt: 'alt text4', caption: 'caption4'}, ] return { images } } }); //Element Plus を利用 app.use(ElementPlus); app.mount("#app");
以下のような構造のカルーセルが出力されるので必要に応じてスタイルを設定します(ボタンやインジケータは省略しています)。
<div class="el-carousel el-carousel--horizontal"> <div class="el-carousel__container" style="height: 600px;"> <div class="el-carousel__item" style="transform: translateX(900px) scale(1);"> <div class="item-inner"><img src="images/01.jpg" alt="alt text1"> <div class="caption">caption1</div> </div> </div> <div class="el-carousel__item" style="transform: translateX(1800px) scale(1);"> <div class="item-inner"><img src="images/02.jpg" alt="alt text2"> <div class="caption">caption2</div> </div> </div> ・・・省略・・・ </div> </div>
.el-carousel { max-width: 900px; margin: 0 auto; } /* キャプションを表示する基準となる要素 */ .el-carousel__item div.item-inner { position: relative; } /* 画像をレスポンシブに */ .el-carousel__item img { width: 100%; } /* キャプション */ .el-carousel__item .caption { position: absolute; bottom: 5%; left: 50%; margin-right: -50%; transform: translate(-50%, 0); color: #FFF; font-size: 16px; background-color: rgba(0,127,127,0.5); padding: 3px 8px; }
<!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"> <!-- Element の CSS の読み込み --> <link rel="stylesheet" href="//unpkg.com/element-plus@2.2.18/dist/index.css" /> <style> .el-carousel { max-width: 900px; margin: 0 auto; } /* キャプションを表示する基準となる要素 */ .el-carousel__item div.item-inner { position: relative; } /* 画像をレスポンシブに */ .el-carousel__item img { width: 100%; } /* キャプション */ .el-carousel__item .caption { position: absolute; bottom: 5%; left: 50%; margin-right: -50%; transform: translate(-50%, 0); color: #FFF; font-size: 16px; background-color: rgba(0,127,127,0.5); padding: 3px 8px; } </style> <title>Element Plus Carousel Sample</title> </head> <body> <div id="app"> <!-- カルーセル --> <el-carousel trigger="click" height="600px" v-bind:interval="4000"> <el-carousel-item v-for="{src, alt, caption} in images" :key="src"> <div class="item-inner"><!-- キャプションを表示する基準とする div 要素を追加 --> <img v-bind:src="src" v-bind:alt="alt"> <div v-text="caption" class="caption"></div><!-- キャプション --> </div> </el-carousel-item> </el-carousel> </div> <!-- Vue の読み込み --> <script src="//unpkg.com/vue@3/dist/vue.global.js"></script> <!-- Element の読み込み --> <script src="//unpkg.com/element-plus@2.2.18"></script> <script> const app = Vue.createApp({ setup() { const images = [ {src: 'images/01.jpg', alt: 'alt text1', caption: 'caption1'}, {src: 'images/02.jpg', alt: 'alt text2', caption: 'caption2'}, {src: 'images/03.jpg', alt: 'alt text3', caption: 'caption3'}, {src: 'images/04.jpg', alt: 'alt text4', caption: 'caption4'}, ] return { images } } }); //Element Plus を利用 app.use(ElementPlus); app.mount("#app"); </script> </body> </html>
Vue I18n
Vue I18n は Vue アプリを国際化する(複数言語に対応させる)ためのライブラリで、I18n とは「Internationalization」の意味です(I と n の間に18文字あることから)。
ドキュメントは以下で確認できます。
- Vue I18n( Guide | API )
- vue-i18n-next
以下で使用している Vue I18n のバージョンは 9.2.2 です。
以下は CDN を利用した日本語と英語のページに対応する基本的なサンプルです(Getting started)。
<!-- Vue の読み込み --> <script src="//unpkg.com/vue@3/dist/vue.global.js"></script> <!-- Vue I18n(バージョン 9 の最新版)の読み込み --> <script src="https://unpkg.com/vue-i18n@9"></script>
-
en や ja などの言語名(ロケール)を最上位プロパティとする階層構造のオブジェクト(階層の深さは自由)で、「キー名:値」の形式で翻訳情報を定義し、変数名は messages としておきます。
-
CDN 経由で利用する場合は、グローバルの VueI18n を使って createI18n() メソッドにオプションを指定して i18n のインスタンスを生成します。
-
Vue のインスタンスを生成します。
-
生成した i18n インスタンスを Vue の use() メソッドに渡して Vue I18n をインストールして有効化します。
// 1. 翻訳情報(translated locale messages)を用意 const messages = { en: { message: { hello: 'hello world' } }, ja: { message: { hello: 'こんにちは、世界' } } } // 2. VueI18n.createI18n() にオプションを指定して i18n インスタンスを生成 const i18n = VueI18n.createI18n({ locale: 'ja', // アプリで使用するロケールを指定 fallbackLocale: 'en', // locale で指定した言語の情報(翻訳)がない場合のフォールバックを指定 messages, // 翻訳情報を指定(messages: messages の省略形) // その他のオプション }); // 3. Vue のインスタンスを生成 const app = Vue.createApp({ // 必要なオプションを設定 }); // 4. 生成した i18n インスタンスを use() に渡して Vue I18n をインストール(有効化) app.use(i18n); // 5. アプリをマウント app.mount("#app");
Vue I18n をインストールして有効化すると、コンポーネントで $t() メソッドを使うことができます。
<div id="app"> <p>{{ $t("message.hello") }}</p> <!-- $t() メソッドで現在のロケール(locale)の翻訳を取得 --> </div>
この例では locale の値が ja なので、$t("message.hello") は message.hello の値として「こんにちは、世界」を取得します(locale の値を en にすると「hello world」を取得します)。
<div id="app" data-v-app=""> <p>こんにちは、世界</p> </div>
大まかな仕組みとしては、messages という翻訳情報のオブジェクトにそれぞれの言語ごとのメッセージを定義しておき、テンプレートに {{ $t("message.hello") }} と記述すると、locale で指定した言語の定義したメッセージを表示できます。
フォールバック fallbackLocale
locale で指定した優先言語に翻訳がない場合のフォールバックとして、fallbackLocale オプションに使用する言語を指定することができます。
暗黙的なフォールバック
ロケールに地域などが指定されている場合(en-US など)、暗黙的なフォールバックが自動的に有効になります。
例えば、de-DE-bavarian の場合、以下のようにフォールバックされます。
- de-DE-bavarian
- de-DE
- de
自動フォールバックを抑制するには、末尾に ! を追加します (例: de-DE!)。
翻訳情報の分離
翻訳情報の messages は外部ファイルとして読み込むこともできるので、他のコードと分離することもできます。以下は翻訳情報を messages.js というファイルに保存して import で読み込む例です。
export const messages = { en: { message: { hello: 'hello world' } }, ja: { message: { hello: 'こんにちは、世界' } } }
<script type="module"> //翻訳情報 messages をインポート import { messages } from './messages.js'; const i18n = VueI18n.createI18n({ locale: 'ja', fallbackLocale: 'en', messages // messages: messages の省略形 }); const app = Vue.createApp({}); app.use(i18n); app.mount("#app"); </script>
Installation
CDN を利用する場合、以下のような方法で Vue I18n を読み込むことができます。
<!-- バージョン 9 の最新版を読み込む場合 --> <script src="https://unpkg.com/vue-i18n@9"></script> <!-- 特定のバージョン(以下は 9.2.2)を読み込む場合 --> <script src="https://unpkg.com/vue-i18n@9.2.2/dist/vue-i18n.global.js"></script> <!-- バージョン 9.2.2 の prod.js(本番環境用)を読み込む場合 --> <script src="https://unpkg.com/vue-i18n@9.2.2/dist/vue-i18n.global.prod.js"></script>
# NPM $ npm install vue-i18n@9 # Yarn $ yarn add vue-i18n@9
Composition API モード
Vue の Composition API を使用する場合は、VueI18n.createI18n() に指定するオプションで legacy オプションを false に設定する必要があります。
// 2. VueI18n.createI18n() にオプションを指定して i18n インスタンスを生成 const i18n = VueI18n.createI18n({ legacy: false, // Composition API の場合は legacy を false にしなければならない locale: 'ja', fallbackLocale: 'en', messages, });
legacy: false を設定することで、Vue I18n が API モードを Legacy API モードから Composition API モードに切り替えるので、コンポーネントの setup で useI18n() メソッドを使用することができます。
useI18n() メソッドは Composer インスタンスを返します。
Composer インスタンスは、VueI18n インスタンスと同様に、t() メソッドなどの translation API と、locale や fallbackLocale などのプロパティを提供します。
useI18n() メソッドは setup() メソッドの先頭で呼び出す必要があります。
// 3. Vue のインスタンスを生成 const app = Vue.createApp({ // setup() で useI18n() を呼び出して Composer インスタンスの t メソッドを取得して返す setup() { const { t } = VueI18n.useI18n(); //先頭で useI18n() メソッドを呼び出す return { t }; } });
const messages = { en: { message: { hello: 'hello world' } }, ja: { message: { hello: 'こんにちは、世界' } } } const i18n = VueI18n.createI18n({ legacy: false, // legacy を false に locale: 'ja', fallbackLocale: 'en', messages, }); const app = Vue.createApp({ setup() { // useI18n() を呼び出して t メソッドを取得して返す const { t } = VueI18n.useI18n(); return { t }; } }); app.use(i18n); app.mount("#app");
setup() でレンダーコンテキストとして t を返すことにより、コンポーネントのテンプレートで t() メソッドを使用できます。
<div id="app"> <p>{{ t("message.hello") }}</p> </div>
<div id="app" data-v-app=""> <p>こんにちは、世界</p> </div>
以下は setup() の中で Composer インスタンスのプロパティを使用する例です。locale などの値は value プロパティでアクセスしていますが、テンプレートでは自動的にアンラップされるので .value は不要です。
必要であれば、t メソッドを使って翻訳情報にアクセスすることができます。
const app = Vue.createApp({ setup() { //Composer インスタンスのプロパティを取得 const { t, locale, fallbackLocale, messages } = VueI18n.useI18n(); // Composer インスタンス console.log(VueI18n.useI18n()); // 出力は以下 // {id: 1, locale: ComputedRefImpl, fallbackLocale: ComputedRefImpl, …} // locale の value プロパティ console.log(locale.value); // ja console.log(fallbackLocale.value); // en console.log(messages.value.en.message.hello); // hello world // t メソッドで翻訳情報にアクセス console.log(t('message.hello')); //こんにちは、世界(locale が ja の場合) return { t }; } });
ロケールの切り替え
今までの例ではロケールをアプリ側で固定していますが、ブラウザの言語設定に応じて動的に切り替えたり、ボタンやセレクトボックスで切り替えることもできます。
ブラウザの言語設定に応じて切り替え
以下はブラウザの言語設定に応じて、ロケールを動的に切り替える例です。
VueI18n.createI18n() の locale の指定を以下のようにブラウザの言語設定を取得するようにします。
navigator.languages はユーザーの言語を表す DOMString の配列を返し、配列の中では最も優先される言語が最初に来るように並べられているので、先頭の要素を取得するようにしています。
また、ブラウザによっては navigator.language が存在しないので、navigator.userLanguage や navigator.browserLanguage を参照します。
const i18n = VueI18n.createI18n({ legacy: false, // Composition API の場合は legacy を false に //ブラウザの言語設定を参照して取得 locale: (navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage || navigator.browserLanguage, fallbackLocale: 'en', messages, });
const messages = { en: { message: { hello: 'hello world' } }, ja: { message: { hello: 'こんにちは、世界' } } } const i18n = VueI18n.createI18n({ legacy: false, // Composition API //ユーザー(ブラウザ)の言語を取得 locale: (navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage || navigator.browserLanguage, fallbackLocale: 'en', messages, }); const app = Vue.createApp({ // setup() で useI18n() を呼び出して t メソッドを取得して返す setup() { const { t } = VueI18n.useI18n(); return { t }; } }); app.use(i18n); app.mount("#app");
セレクトボックスで動的に切り替え
以下はセレクトボックスで言語を選択して動的に切り替える例です。
vue-i18n-next/examples/composition/started.html
<div id="app"> <form> <label for="locale-select">{{ t('message.language') }}</label> <select id="locale-select" v-model="locale"> <option value="en">en</option> <option value="ja">ja</option> </select> </form> <p>{{ t("message.hello") }}</p> </div>
const { createApp } = Vue; const { createI18n, useI18n } = VueI18n; const i18n = createI18n({ legacy: false, // Composition API locale: 'ja', messages: { en: { message: { language: 'Language', //セレクトボックスのラベル hello: 'hello world!' } }, ja: { message: { language: '言語', //セレクトボックスのラベル hello: 'こんにちは、世界!' } } } }); const app = createApp({ setup() { // useI18n() から t と locale を取得(先頭で useI18n() メソッドを呼び出す) const { t, locale } = useI18n(); // t と locale を返して、テンプレートで使用できるように return { t, locale }; } }); app.use(i18n); app.mount('#app');
翻訳情報の構文
Vue I18n は以下のような構文(Message Format Syntax)を使用して、UI に表示されるメッセージをローカライズできます。
Named interpolation(名前付き補間)
翻訳文字列に {xxxx}
の形式で変数を埋め込むことができます。{ }
をプレースホルダーと呼びます。
以下は message.hello キーの値に name という変数を埋め込んでいます。変数は複数埋め込むこともできます。Named interpolation
const messages = { en: { message: { hello: 'Hello {name}' //名前付き補間 } }, ja: { message: { hello: 'こんにちは、{name}' //名前付き補間 } } } const i18n = VueI18n.createI18n({ legacy: false, locale: 'ja', fallbackLocale: 'en', messages }); const app = Vue.createApp({ setup() { const { t } = VueI18n.useI18n(); return { t }; } }); app.use(i18n); app.mount("#app");
変数に値を指定するには、t メソッド(Legacy API の場合は $t メソッド)の呼び出しで以下のように指定します。
<div id="app"> <p>{{ t('message.hello', {name: '太郎さん'}) }}</p> </div>
<div id="app" data-v-app=""> <p>こんにちは、太郎さん</p> </div>
上記の場合、ロケールを en に切り替えると出力は「Hello 太郎さん」となってしまいます。
以下のようにすれば、ロケールが en の場合は「Hello Taro-san」、ja の場合は「こんにちは、太郎さん」と出力されます。
const messages = { en: { message: { hello: 'Hello {name}', taro: 'Taro-san' } }, ja: { message: { hello: 'こんにちは、{name}', taro: '太郎さん' } } }
<div id="app"> <p>{{ t('message.hello', {name: t('message.taro')}) }}</p> </div>
以下のように computed メソッドを使って t メソッドの値を返すことで同じことができます。
const messages = { en: { message: { hello: 'Hello {name}', taro: 'Taro-san' } }, ja: { message: { hello: 'こんにちは、{name}', taro: '太郎さん' } } } const i18n = VueI18n.createI18n({ legacy: false, locale: 'ja', fallbackLocale: 'en', messages }); const app = Vue.createApp({ setup() { const { t } = VueI18n.useI18n(); // computed メソッドを使って t('message.taro') を返す const taro = Vue.computed(() => t('message.taro')); return { t, taro }; } }); app.use(i18n); app.mount("#app");
<div id="app"> <p>{{ t('message.hello', { name: taro }) }}</p> </div>
List interpolation(リスト補間)
プレースホルダーには、{0}
のように配列のインデックスを指定することもできます。List interpolation
const messages = { en: { message: { hello: 'Hello {0}' //リスト補間 } }, ja: { message: { hello: 'こんにちは、{1}' //リスト補間 } } } const i18n = VueI18n.createI18n({ legacy: false, locale: 'ja', fallbackLocale: 'en', messages }); const app = Vue.createApp({ setup() { const { t } = VueI18n.useI18n(); return { t }; } }); app.use(i18n); app.mount("#app");
テンプレートで以下のように記述すると、ロケールが en の場合は「Hello Taro-san」、ja の場合は「こんにちは、太郎さん」と出力されます
<div id="app"> <p>{{ t('message.hello', ['Taro-san', '太郎さん']) }}</p> </div>
Literal interpolation(リテラル補間)
例えば、以下の文字は特殊文字なので、翻訳文字列ではそのまま使うことができません(Special Characters)。
{
, }
, @
, $
, |
プレースホルダーの中で文字列をシングルクォートで囲むことで、その文字(または文字列)を特殊文字ではなく単なる文字として認識させることができます。Literal interpolation
以下の場合、@ はメッセージ参照の特殊文字ではなく、単なる @ という文字として認識されます。
const messages = { en: { address: "{account}{'@'}{domain}" // @ をリテラル補間 }
<div id="app"> <p>email:{{ t('message.address', { account:'foo', domain:'example.com' }) }}</p> </div>
<div id="app" data-v-app=""> <p>email:foo@example.com</p> </div>
Linked messages(翻訳文字列の参照)
@:
の後にキー名を指定して、別の翻訳文字列を参照して利用することができます。Linked messages
const messages = { en: { message: { hello: 'Hello', taro: 'Taro-san', greeting: '@:message.hello @:message.taro' //翻訳文字列の参照 } }, ja: { message: { hello: 'こんにちは', taro: '太郎さん', greeting: '@:message.hello @:message.taro' //翻訳文字列の参照 } } } const i18n = VueI18n.createI18n({ legacy: false, locale: 'ja', fallbackLocale: 'en', messages }); const app = Vue.createApp({ setup() { const { t } = VueI18n.useI18n(); return { t }; } }); app.use(i18n); app.mount("#app");
テンプレートで以下のように記述すると、ロケールが en の場合は「Hello Taro-san」、ja の場合は「こんにちは 太郎さん」と出力されます
<div id="app"> <p>{{ t('message.greeting') }}</p> </div>
日時のフォーマット
Vue I18n では日時のフォーマットを定義してロケールにあわせてローカライズすることができます。
以下のような名前付きの日時形式 (short、long など) を定義することができます。Datetime Formatting
const datetimeFormats = { 'en': { short: { year: 'numeric', month: 'short', day: 'numeric' }, long: { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short', hour: 'numeric', minute: 'numeric' } }, 'ja': { short: { year: 'numeric', month: 'short', day: 'numeric' }, long: { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short', hour: 'numeric', minute: 'numeric', hour12: true } } }
関連(参考):MDN / Intl.DateTimeFormat
フォーマットは以下のような形式で定義します。フォーマット名はフォーマットを適用するためのキーとなります。任意の名前を指定できますが、通常は short や long を使います。
const 変数名 = { 言語名: { フォーマット名: { オプション名: 値 ・・・ }, ・・・ }, ・・・ }
フォーマットの有効化
フォーマットを定義したら、createI18n() の datetimeFormats オプションに渡します。
const i18n = VueI18n.createI18n({ legacy: false, locale: 'ja', fallbackLocale: 'en', messages, datetimeFormats //datetimeFormats: datetimeFormats の省略形 });
整形
$d メソッドを使って日付を指定の形式で整形することができます。
$d メソッドの第2引数には、定義した日時のフォーマットのキー(short や long などのフォーマット名)を指定できます。オプションで第3引数にロケールなどを指定することができます(その他いろいろな引数の指定方法があります)。
<div id="app"> <p>{{ $d(new Date(), 'short') }}</p> <p>{{ $d(new Date(), 'long') }}</p> <p>{{ $d(new Date(), 'short', 'en') }}</p> <p>{{ $d(new Date(), 'long', 'en') }}</p> </div>
locale が ja の場合、例えば以下のように出力されます。
<div id="app" data-v-app=""> <p>2022年10月23日</p> <p>2022年10月23日(日) 午後4:30</p> <p>Oct 23, 2022</p> <p>Sun, Oct 23, 2022, 4:30 PM</p> </div>
$d メソッドは Legacy API モードと Composition API モードのどちらでも利用できます。Composition API モードでは、useI18n() から d メソッドを取得して同様に使用することができます。
<div id="app"> <p>{{ d(new Date(), 'short') }}</p> <p>{{ d(new Date(), 'long') }}</p> <p>{{ d(new Date(), 'short', 'en') }}</p> <p>{{ d(new Date(), 'long', 'en') }}</p> </div>
const datetimeFormats = { 'en': { short: { year: 'numeric', month: 'short', day: 'numeric' }, long: { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short', hour: 'numeric', minute: 'numeric' } }, 'ja': { short: { year: 'numeric', month: 'short', day: 'numeric' }, long: { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short', hour: 'numeric', minute: 'numeric', hour12: true } } } const messages = { en: { message: { hello: 'Hello' } }, ja: { message: { hello: 'こんにちは' } } } const i18n = VueI18n.createI18n({ legacy: false, locale: 'ja', fallbackLocale: 'en', messages, datetimeFormats }); const app = Vue.createApp({ setup() { // d メソッドを取得して返す const { t, d } = VueI18n.useI18n(); return { t, d }; } }); app.use(i18n); app.mount("#app");
数値のフォーマット
Vue I18n では日時のフォーマット同様、数値のフォーマットを定義してロケールにあわせてローカライズすることができます。Number Formatting
以下のような名前付きの数値形式 (currency や decimal など) を定義することができます。
const numberFormats = { 'en': { currency: { style: 'currency', currency: 'USD', notation: 'standard' }, decimal: { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 }, percent: { style: 'percent', useGrouping: false } }, 'ja': { currency: { style: 'currency', currency: 'JPY', useGrouping: true, currencyDisplay: 'symbol' }, decimal: { style: 'decimal', minimumSignificantDigits: 3, maximumSignificantDigits: 5 }, percent: { style: 'percent', useGrouping: false } } }
関連(参考):MDN / Intl.NumberFormat
フォーマットは日時と同様、以下のような形式で定義します。フォーマット名は任意の名前を指定できますが、通常は currency や decimal、percent などを使います。
const 変数名 = { 言語名: { フォーマット名: { オプション名: 値 ・・・ }, ・・・ }, ・・・ }
フォーマットの有効化
フォーマットを定義したら、createI18n() の numberFormats オプションに渡します。
const i18n = VueI18n.createI18n({ legacy: false, locale: 'ja', fallbackLocale: 'en', messages, numberFormats //numberFormats: numberFormats の省略形 });
整形
$n メソッドを使って数値を指定の形式で整形することができます。
$n メソッドの第2引数には、定義した数値のフォーマットのキー(currency や percent、decimal などのフォーマット名)を指定できます。オプションで第3引数にロケールを指定することができます($d メソッド同様、その他にもいろいろな引数の指定方法があります)。
<div id="app"> <p>{{ $n(10000, 'currency') }}</p> <p>{{ $n(10000, 'currency', 'ja') }}</p> <p>{{ $n(10000, 'currency', 'ja', { useGrouping: false }) }}</p> <p>{{ $n(987654321, 'currency', { notation: 'compact' }) }}</p> <p>{{ $n(0.99123, 'percent') }}</p> <p>{{ $n(0.99123, 'percent', { minimumFractionDigits: 2 }) }}</p> <p>{{ $n(12.11612345, 'decimal') }}</p> <p>{{ $n(12145281111, 'decimal', 'ja') }}</p> </div>
locale が ja の場合、例えば以下のように出力されます。
<div id="app" data-v-app=""> <p>¥10,000</p> <p>¥10,000</p> <p>¥10000</p> <p>¥9.9億</p> <p>99%</p> <p>99.12%</p> <p>12.116</p> <p>12,145,000,000</p> </div>
$n メソッドは $d メソッド同様、Legacy API モードと Composition API モードのどちらでも利用できます。Composition API モードでは、useI18n() から n メソッドを取得して同様に使用できます。
<div id="app"> <p>{{ n(10000, 'currency') }}</p> <p>{{ n(10000, 'currency', 'ja') }}</p> <p>{{ n(10000, 'currency', 'ja', { useGrouping: false }) }}</p> <p>{{ n(987654321, 'currency', { notation: 'compact' }) }}</p> <p>{{ n(0.99123, 'percent') }}</p> <p>{{ n(0.99123, 'percent', { minimumFractionDigits: 2 }) }}</p> <p>{{ n(12.11612345, 'decimal') }}</p> <p>{{ n(12145281111, 'decimal', 'ja') }}</p> </div>
const i18n = VueI18n.createI18n({ legacy: false, //Composition API locale: 'ja', fallbackLocale: 'en', messages, numberFormats }); const app = Vue.createApp({ setup() { // n メソッドを取得して返す const { t, n } = VueI18n.useI18n(); return { t, n }; } }); app.use(i18n); app.mount("#app");
プラグインの作成
プラグインは自分で作成することもできます。
プラグインは install() メソッドを持つオブジェクトか、またはインストール関数として動作する関数なので、コンソールにメッセージを出力するだけの単純なプラグインは以下のように定義することができます。
const MyPlugin = { install() { console.log('Hello from MyPlugin!'); }, };
const MyPlugin = () => { console.log('Hello from MyPlugin!'); };
プラグインを利用するには、createApp() で Vue のアプリケーションインスタンスを生成後、use() メソッドでアプリケーションにプラグインを追加します。
以下を記述すると、コンソールに「Hello from MyPlugin!」と出力されます。
const MyPlugin = { install() { console.log('Hello from MyPlugin!'); }, }; //アプリケーションを生成 const app = Vue.createApp({}); //use() メソッドでアプリケーションにプラグインを追加 app.use(MyPlugin);
定義したプラグインを、例えば plugins フォルダに MyPlugin.js というファイル名で保存した場合は、以下のように読み込みます。
<!-- Vue の読み込み --> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <!-- MyPlugin.js の読み込み --> <script src="plugins/MyPlugin.js"></script> <script> const app = Vue.createApp({}); //プラグイン MyPlugin をアプリケーションに追加 app.use(MyPlugin); </script>
以下は前述のプラグインをモジュールとして定義してデフォルトエクスポートする例です。
export default { install() { console.log('Hello from MyPlugin!'); }, }
利用する側のファイルでは定義したプラグインをインポートします。
以下ではインポートマップを使って Vue を読み込んでいますが、Chrome 以外のブラウザでは対応していないので、ES Module Shims を最初に読み込んでいます。
<script async src="https://ga.jspm.io/npm:es-module-shims@1.6.2/dist/es-module-shims.js"></script> <script type="importmap"> { "imports": { "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js" } } </script> <script type="module"> import { createApp } from 'vue'; import MyPlugin from './plugins/MyPlugin.js'; const app = createApp({}); app.use(MyPlugin); </script>
パラメータ
プラグインは以下の2つのパラメータ(引数)を受け取ることができます。
- app:createApp() により生成されるアプリケーションのインスタンス
- options:app.use() に渡される追加のオプション
以下はプラグインの install() に渡されるパラメータをコンソールに出力して確認する例です。
const MyPlugin = { install(app, options) { // パラメータの値を確認 console.log(app); //以下の画像を参照 console.log(app.version); //3.2.41(Vue のバージョン) console.log(options); //{name: 'name option'} console.log(options.name); //name option }, }; const app = Vue.createApp({}); // use() にオプションを指定 app.use(MyPlugin, {name: 'name option'});
以下が出力例です。app には Vue アプリケーションのインスタンス、options には use() の第2引数に指定したオブジェクトが確認できます。
プラグインを関数として定義する場合も同じです。
//プラグインを関数として定義 const MyPlugin = (app, options) => { console.log(app); //上記の画像を参照 console.log(app.version); //3.2.41(Vue のバージョン) console.log(options); //{name: 'name option'} console.log(options.name); //name option }; const app = Vue.createApp({}); app.use(MyPlugin, {name: 'name option'});
サンプル
プラグインの install() メソッドの中では、app.component() や app.directive() を使って、1つもしくは複数のグローバルなコンポーネントやカスタムディレクティブを登録したり、app.config.globalProperties にグローバルなインスタンスプロパティやメソッドを追加するなどができます。
以下はカスタムディレクティブの項で作成した v-text-color ディレクティブを定義する単純な自作のプラグイン MyTextColorPlugin の例です。
// プラグイン MyTextColorPlugin の定義 const MyTextColorPlugin = { install(app) { //アプリケーションにグローバルなカスタムディレクティブを定義(登録) app.directive('text-color', (el, binding) => { //カスタムディレクティブを指定した要素のテキストをその値の色に el.style.color = binding.value; }); }, }; // アプリケーションの生成 const app = Vue.createApp({ setup() { //テキストの色の初期値(データ) const myColor = Vue.ref('green'); return { myColor } } }); // プラグイン MyTextColorPlugin をアプリケーションに追加 app.use(MyTextColorPlugin); // アプリケーションをマウント app.mount('#app');
以下ではルートコンポーネントのテンプレートで、要素にプラグインのカスタムディレクティブを指定して、テキストの色を変更できるようにしています。
<div id="app"> <div v-text-color="'purple'">It's Purple.</div> <select v-model="myColor"> <option value="green">Green</option> <option value="blue">Blue</option> <option value="red">Red</option> <option value="orange">Orange</option> </select> <h3 v-text-color="myColor">Color Title</h3> </div>
最初の div 要素はカスタムディレクティブ v-text-color に指定した値('purple')により紫色に、h3 要素の色はセレクトボックスで選択された色になります(myColor の初期値は緑)。
<div id="app" data-v-app=""> <div style="color: purple;">It's Purple.</div> <select> <option value="green">Green</option> <option value="blue">Blue</option> <option value="red">Red</option> <option value="orange">Orange</option> </select> <h3 style="color: green;">Color Title</h3> </div>
以下は前述のプラグインにコンポーネントを追加した例です。
install() メソッドの第2パラメータ(options)に textColor プロパティを持つオブジェクトを期待しています。options に textColor プロパティが指定されていれば、その値を文字色の初期値として使用します。
プラグインを use() メソッドを使ってインストールする際に、オプションに textColor プロパティを持つオブジェクトを指定しています。オプションが省略されている場合は、文字色の初期値として green を使用します。
// プラグイン MyTextColorPlugin の定義 const MyTextColorPlugin = { install(app, options) { app.directive('text-color', (el, binding) => { el.style.color = binding.value; }) .component('color-select-box', { template: ` <select v-model="textColor"> <option value="green">Green</option> <option value="blue">Blue</option> <option value="red">Red</option> <option value="orange">Orange</option> </select> <h3 v-text-color="textColor">Color Title</h3> `, setup() { //options に textColor プロパティが指定されていれば、その値を文字色に const textColor = options && options.textColor ? Vue.ref(options.textColor) : Vue.ref('green'); return { textColor } } }); }, }; // アプリケーションの生成 const app = Vue.createApp({}); // オプションにオブジェクトを指定してプラグインをアプリケーションに追加 app.use(MyTextColorPlugin, { textColor: 'red' }); app.mount('#app');
テンプレートでプラグインで登録したコンポーネント color-select-box を呼び出します。
<div id="app"> <color-select-box></color-select-box> </div>
以下が出力され、h3 要素の文字色がセレクトボックスで選択した色になります。
<div id="app" data-v-app=""> <select> <option value="green">Green</option> <option value="blue">Blue</option> <option value="red">Red</option> <option value="orange">Orange</option> </select> <h3 style="color: red;">Color Title</h3> </div>