コンポーネントで双方向データバインディングを実装
propsとemitを組み合わせることで、コンポーネントに対して双方向データバインディングを設定できます。図3のサンプルでは、子コンポーネント内のテキストボックスと、子コンポーネント外のテキストボックスの内容が同期して変更されます。
![図3 双方向データバインディングのサンプル(p003-binding)](http://cz-cdn.shoeisha.jp/static/images/article/16252/16252_03.png)
親コンポーネントには、PhoneFormコンポーネントをリスト6の通り記述します。
<PhoneForm v-model="paramName" v-model:vendor="paramVendor" v-model:price="paramPrice"></PhoneForm>
(1)で、PhoneFormコンポーネントの引数にv-modelディレクティブで変数をバインディングしています。ディレクティブには「v-model」のほか、「v-model:vendor」「v-model:price」の通りプロパティ名を指定することもできます。
対応する子コンポーネントはリスト7の通りです。
<template> <!-- inputタグだけ抽出して記述 ...(1)--> <input type="text" :value="modelValue" @input="onUpdateModelValue"> <input type="text" :value="vendor" @input="onUpdateVendor"> <input type="number" :value="price" @input="onUpdatePrice"> </template> <script lang="ts"> import { defineComponent } from 'vue' export default defineComponent({ props: { // ...(2) modelValue: String, // v-modelで指定した名前 vendor: String, // v-model:vendorで指定したメーカー price: Number // v-model:priceで指定した価格 }, emits: [ // ...(3) 'update:modelValue', 'update:vendor', 'update:price' ], setup(props, context) { const onUpdateModelValue = ((e: Event) => { // ...(4) if (e.target instanceof HTMLInputElement) { context.emit('update:modelValue', e.target.value) } }) (以下略:vendor、priceのイベントハンドラー) } })
v-modelディレクティブで指定されるプロパティは(2)のpropsに記述します。「v-model:vendor」や「v-model:price」のように、コロンの後ろに名前が指定されたディレクティブに対応するプロパティはその名前になります(「vendor」、「price」)。名前が指定されない「v-model」ディレクティブに対応するプロパティは「modelValue」になる決まりです。同様にemits(3)に指定するイベント名も、「v-model:vendor」「v-model:price」に対応するイベント名はそれぞれ「update:vendor」「update:price」、「v-model」に対応するイベント名は「update:modelValue」となります。
コンポーネントのinputタグ(1)では、(2)のプロパティを:value(v-bind:valueの省略記法)ディレクティブに指定してデータを反映するとともに、@input(v-on:inputの省略記法)ディレクティブに(4)のイベントハンドラーを指定して、文字入力時に(3)のイベントをemitすることで、双方向データバインディングの動作が実現します。
<script setup>で記述を簡略化
これまでの記述は、<script setup>を利用してさらに簡略化できます。以下ではポイントだけを説明します。全てのコードはp002a-emit-setupサンプルを参照してください。
参照するコンポーネントの指定
<script setup>を利用しない場合、参照するコンポーネントをdefineComponentのcomponentsに設定していました。<script setup>を利用する場合、コンポーネントをインポートするだけで、そのコンポーネントが利用できるようになります。
<script setup lang="ts"> // importするだけでコンポーネントが利用できる import PhoneWithBuy from "./components/PhoneWithBuy.vue" (略) </script>
プロパティ、emitの指定
<script setup>を利用しない場合、プロパティやemitは、defineComponentのprops、emitで指定していました。<script setup>を利用する場合、プロパティはリスト9(1)のdefinePropsメソッド、emitは(2)のdefineEmitsメソッドで取得して、(3)の通り利用できます。
const props = defineProps({ // ...(1) phoneData: PhoneData // PhoneData型プロパティ }) const emit = defineEmits([ // ...(2) 'onBuy' // emitする可能性があるイベントを指定 ]) const onClickBuyButton = (() => { emit('onBuy', props.phoneData) // ...(3) })
コンポーネント内部のHTML要素を指定するスロット
最後に、コンポーネントタグの内側に記述された内容を画面に反映する「スロット」を、タイトルと内容の文言からなるエラー表示を行う図4のサンプルで説明します。
![図4 スロットの利用例(p004-slot)](http://cz-cdn.shoeisha.jp/static/images/article/16252/16252_04.png)
図4の内容を表示するCustomErrorコンポーネントは、リスト10の通りです。スロットには名前なしのもの(1)と名前付きのもの(2)があります。
<template> <div class="error"> <div class="title"> <slot></slot> <!-- 名前なしスロット ...(1)--> </div> <div class="description"> <slot name="desc"></slot> <!-- 名前付きスロット ...(2)--> </div> </div> </template>
コンポーネントを利用する側の実装は、リスト11の通りです。<CustomError>の内側で、名前がついていない(1)がリスト10(1)の名前なしスロットに、v-slotで名前(desc)がついている(2)の内容がリスト10(2)の名前付きスロットに設定されます。
<template> <CustomError> 名前なしスロットに渡されます。 <!--(1)--> <template v-slot:desc> <!--(2)--> 名前付きスロットに渡されます。 </template> </CustomError> </template>
まとめ
本記事では、Vue.jsでWebページを構成するコンポーネントの記述・利用法を説明しました。Webページを小さいコンポーネントに分割して、プロパティでコンポーネントにデータを設定したり、emitでコンポーネントからイベントを発行したりできます。
次回は、複数のページを切り替え表示するVue.jsのルーティングについて説明します。