前回の記事では子コンポーネントにはpropsを経由して、親側のアプリケーションやコンポーネント(以下親コンポーネントと記載)のデータを渡せることを紹介しました。
Vue.jsではpropsを利用して親コンポーネントと子コンポーネントの間でのデータのやり取りを行いますが、propsでは親から子にデータを渡すことはできても、子から親にデータを渡すことはできません。
また、propsで受け取ったデータを子コンポーネント内で変更することもできません。(これは、子コンポーネント側でデータが変更できてしまうと、複数のコンポーネントが配置された複雑な構造を持つ場合に、どこでデータが変更されたかを親コンポーネント側で把握しにくくなるためです。)
そこでVue.jsでは、子コンポーネントから親コンポーネントにデータを渡す手段としてイベントを使用します。
今回は、子コンポーネントから親コンポーネントに対してのカスタムイベントを発生させる$emitというインスタンスメソッドについて紹介します。
$emitメソッド
Vue.jsでは、子コンポーネントで何らかの処理を行った時に、子コンポーネントを使用している親コンポーネントに対してイベントを発生させることで、何らかの変更が起こったことを通知します。
親コンポーネントではイベントを購読して処理の発生を検知します。
子コンポーネントでイベントを発生させるためには、インスタンスメソッドの$emitメソッドを使用します。
$emitメソッドは、子コンポーネントで何らかの処理が行われた時に呼び出します。
例えば、子コンポーネントに配置されているテキストボックスの値が変更になった時や、ボタンがクリックされた時などです。
$emitメソッドを呼び出すとイベントが発生し、親コンポーネントへ変更の通知を行うことができます。
$emitメソッドでは、親コンポーネント側でデータを変更するのに必要なデータ(任意のオブジェクト)をイベントの引数として渡すことができます。
親コンポーネント側ではイベントの発生を検知し、イベントの引数でデータを受け取ることで、親コンポーネント内のデータを変更することができるようになります。
$emitメソッドの書式
$emitメソッドのもっとも簡素な呼び出しは以下のようになります。
1 |
this.$emit(イベント名); |
$emitメソッドはインスタンスメソッドなので、dataプロパティオプションにアクセスする時などと同様にthis修飾子が必要になります。
$emitメソッドの引数にはイベント名を指定します。
例えば「button-click」という名前のイベントであれば次のように記述します。
1 |
this.$emit('button-click'); |
上記の書式では、親コンポーネントにはデータを渡さずイベントの発行の行っています。
親コンポーネントにイベントの引数としてデータを渡したい場合は、次のようにします。
1 |
this.$emit(イベント名, データ); |
$emitメソッドの第1引数にイベント名を指定し、第2引数にデータを指定します。
例えば、親コンポーネントに「1」を渡す場合は、以下のように記述することができます。(ここではイベント名を「text-click」とします。)
1 |
this.$emit('text-click', 1); |
親コンポーネントに渡す複数のデータを渡したい場合は、第3引数以降に続けて記述します。
1 |
this.$emit(イベント名, データ1, データ2, データ3); |
例えば、親コンポーネントに「1」と「’abc’」の2つの値を渡す場合は、以下のように記述することができます。(ここではイベント名を「link-click」とします。)
1 |
this.$emit('link-click', 1, 'abc'); |
イベントの引数には値だけではなく任意のオブジェクトが指定できますので、次のように記述することもできます。(ここではイベント名を「text-changed」とします。)
1 2 3 4 5 |
const args = { id: 1 name: 'abc' } this.$emit('text-changed', args); |
親コンポーネントに渡すデータが複数ある場合は、オブジェクトにまとめて渡す方がイベントのシグネチャがシンプルになります。
親でのイベントの購読(監視)
子コンポーネントで発行するイベントは、HTML要素のイベントと同様にv-onディレクティブを使用して購読することができます。
v-onディレクティブでイベントにメソッドをバインドすることで、子コンポーネントで何かしらの処理が発生して$emitメソッドが呼び出されると、親側でバインドしているイベントメソッドが呼び出されます。
先に示した「button-click」イベントをバインドする場合は次のようになります。
1 |
v-on:button-click="onButtonClick" |
button-clickイベントにバインドしている「onButtonClick」はmethodsオプションに定義したメソッドになります。
1 2 3 4 5 |
methods: { onButtonClick: function() { // ... 処理 ... } } |
子コンポーネントで発行したイベントに引数(データ)がある場合も同様になります。
先に示した「text-click」イベントをバインドする場合は次のようになります。
1 |
v-on:text-click="onTextClick" |
1 2 3 4 5 |
methods: { onTextClick: function(e) { // ... 処理 ... } } |
子コンポーネント側で$emitメソッドの引数に指定したデータは、親コンポーネント側でイベントにバインドするメソッドの引数で受け取ることができます。
上記の例では「function(e)」の「e」で受け取ります。
$emitメソッドの第2引数に指定したデータ(親コンポーネントに渡すデータ)を親コンポーネントのHTMLテンプレートで参照する場合は「$event」という変数で受け取ることができます。
1 |
v-on:text-click="counter += $event" |
「$event」はイベントの第1引数を受け取ることができる特別な変数です。
上記の例ではdataオプションに定義したcounterというプロパティにイベントで受け取った引数の値を加算しています。
カスタムイベントでは複数のデータを引数に渡すことができます。
複数のデータ(引数)がある場合は、methodsオプションに定義するメソッドの関数の引数を複数指定します。
先に示した「link-click」イベントをバインドする場合だと、以下のようになります。
1 |
v-on:link-click="onLinkClick" |
1 2 3 4 5 |
methods: { onLinkClick: function(e, f) { // ... 処理 ... } } |
$emitメソッドを使用したカスタムイベントの名前
$emitメソッドで指定するカスタムイベントの名前は、コンポーネント名やプロパティ名とは違い、大文字と小文字は自動的に変換されません。
$emitメソッドで指定したイベントを親コンポーネントでバインドする際のイベントリスナは、まったく同じ名前にする必要があります。
例えばキャメルケース(camelCase)でイベント名を付けた場合は、イベントリスナ名をケバブケース(kebab-case)にしてもイベントは発生しません。
1 |
this.$emit('customEvent') |
1 |
<custom-component v-on:custom-event="method"></custom-component> |
上記の例では、キャメルケースのイベント名「customEvent」を「custom-event」というイベントリスナ名でバインドしていますが、これは正しく動作しません。
イベントの名前については、ケバブケース(kebab-case)で記述することが推奨されています。
Vue.jsn公式ドキュメントには次のように記載されています。
コンポーネントやプロパティとは違い、イベント名の大文字と小文字は自動的に変換されません。その代わり発火されるイベント名とイベントリスナ名は全く同じにする必要があります。例えばキャメルケース(camelCase)のイベント名でイベントを発火した場合
1 this.$emit('myEvent')ケバブケース(kebab-case)でリスナ名を作っても何も起こりません:
12 <!-- 動作しません --><my-component v-on:my-event="doSomething"></my-component>コンポーネントやプロパティとは違い、イベント名は JavaScript 内で変数やプロパティ名として扱われることはないので、キャメルケース(camelCase)やパスカルケース(PascalCase)を使う理由はありません。さらに DOM テンプレート内の v-on イベントリスナは自動的に小文字に変換されます (HTML が大文字と小文字を判別しないため)。このため v-on:myEvent は v-on:myevent になり myEvent にリスナが反応することができなくなります。
こういった理由から、いつもケバブケース(kebab-case)を使うことをお薦めします。
$emitメソッドの引数に指定するイベント名が複数の単語から構成される場合は、ケバブケース(kebab-case)に統一するようにしてください。
まとめ
子コンポーネントは$emitメソッドの第1引数にイベント名を指定してイベントを発火させる。
$emitの第2引数以降には、親コンポーネントに渡したいデータを指定できる。
親コンポーネントでカスタム要素として子コンポーネントを配置する際は、監視するイベント名が指定できる。
親コンポーネントはmethodsオプションに定義するメソッドの関数の引数で子コンポーネントから渡されたデータを参照できる。
HTMLテンプレートからデータを参照する場合は$event変数を利用する。
最後にjsFiddleに貼り付けてコンポーネントの動作を確認できるコードのサンプルを掲載しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
<script src="https://unpkg.com/vue@next"></script> <div id="app"> <my-component v-bind:prop-name="propsData" v-on:button-click="buttonClick" v-on:link-click="linkClick" v-on:number-changed="counter += $event"></my-component> <div>button-click: {{text1}}</div> <div>link-click: {{text2}}</div> <div>number-changed: {{counter}}</div> </div> <script type="text/x-template" id="component-template"> <div> <button v-on:click="onButtonClick">ボタン</button> </div> <div> <a v-on:click="onLinkClick">リンク</a> </div> <div> <input type="number" v-model.number="num" v-on:change="onTextChanged" /> </div> </script> <script> // コンポーネント const myComponent = { props: { propName: { type: String, default: '', required: true, validator: function(value) { return value.length <= 5; } } }, template: '#component-template', data: function() { return { num: 0 } }, methods: { onButtonClick: function() { const args = { id: 1, name: 'abc', value: 10.54 } this.$emit('button-click', args); }, onLinkClick: function() { this.$emit('link-click', 1, 2); }, onTextChanged: function($event) { this.$emit('number-changed', Number($event.target.value)); } } } // アプリケーション Vue.createApp({ components: { 'my-component': myComponent }, data: function() { return { text1: '', text2: '', counter: 0 } }, methods: { buttonClick: function(event) { this.text1 = event.id + ', ' + event.name + ', ' + event.value; }, linkClick: function(e, f) { this.text2 = e + ', ' + f; } } }).mount('#app') </script> |
上記のコードをコピーしてjsFiddleのHTMLパネルに貼り付けて「Run」をクリックすることで実行結果が確認できます。
jsFiddleの使い方等については、以下の記事をご覧ください。