基礎からメモ: Vue.js CH5-p161 子から親へのコンポーネント間通信

カスタムイベントと$emit

コンポーネントの状態に対応して親コンポーネントに何らかのアクションを起こさせるとか、子コンポーネントが持つデータを親に渡すなどの場合、カスタムイベントとインスタンスメソッドの$emitを使う。

カスタムイベントとは、v-on:clickのようなフックをするためのイベントタイプを自作できる仕組み。$emitコンポーネントに紐付いたその自作イベントを発火させるトリガー。

まず子コンポーネントを使う親側のテンプレートに、v-onディレクティブで子のカスタムイベントをハンドルしておく。具体的には次のように、そのカスタムイベントが発火したときに起動させたい親側のメソッドを紐付けておく。

<comp-child v-on:childs-event="parentsMethod"></comp-child>

コンポーネントの中に、$emitを使って、その定義したカスタムイベントを発火させる仕組みを作る。

Vue.component('comp-child', {
  template: '<button v-on:click="handleClick">Fire!</button>',
  methods: {
    handleClick: function() {
      this.$emit('childs-event')
    }
  }
});

そして親側に子のイベントをキャッチし、v-onで紐付けられて起動されるハンドラを登録しておく。

let app = new Vue({
  el: '#app',
  methods: {
    parentsMethod: function() {
      alert("イベントをキャッチ! " + this.message + '!!');
    }
  },
  data: {
    message: 'Message Data of Parent'
  }
});

コンポーネントのテンプレートで定義されたボタンをクリックすると、結果的に親側で登録されたハンドラが呼び出され、この場合はアラートが表示される。

親のデータを子から操作する

前はエラーが出たところで終わっていた、親データを子から操作する仕組みをこの$emitで実装してみる。

HTMLはリストの要素を表示させる。

<div id="app">
<ul>
  <comp-child v-for="item in list"
    v-bind:key="item.id"
    v-bind="item"
    v-on:attack="handleAttack">
  </comp-child>
</ul>
</div>

親側でイベントから要素のIDを受取り、該当するIDの要素の hp を10ずつ減らすハンドラ関数を定義しておく。

let app = new Vue({
  el: '#app',
  methods: {
    handleAttack: function(id) {
      let item = this.list.find(function(el) {
        return el.id === id;
      })
      if (item !== undefined && item.hp > 0) {
        item.hp -= 10;
      }
    }
  },
  data: {
    list: [
      { id: 1, name: 'Slime', hp: 100 },
      { id: 2, name: 'Goblin', hp: 200 },
      { id: 3, name: 'Dragon', hp: 500 }
    ]
  }
});

コンポーネントにはボタンのクリックイベントで呼び出されるハンドラの中に、$emitで発火されるカスタムイベントと、引数として各要素のIDをイベントに渡す仕組みを作る。

/*
親子関係のコンポーネント
子から親へ
*/

Vue.component('comp-child', {
  template: `<li>{{ name }} HP: {{ hp }}
    <button v-on:click="doAttack">攻撃!</button></li>`,
  props: { id: Number, name: String, hp: Number },
  methods: {
    doAttack: function() {
      this.$emit('attack', this.id)
    }
  }
});

これで「攻撃」と書かれたボタンをクリックすると、各要素の hp が減らされる。子コンポーネントのテンプレートにあるボタンから、親コンポーネント側のデータを減らした形になる。

カスタムタグ内のネイティブイベント

カスタムタグの中に、クリックイベントのようなネイティブなイベントをv-onで発火させるようにしても、コンポーネント側から明示的に例えばclick$emitを介して呼び出さないかぎり、何も起きない。

<div id="app">
  <!-- .native を付けない次の形はイベント発火しない($emit を介していないので) -->
  <my-icon v-on:click="handleClick" v-bind:val="icon"></my-icon>
</div>
Vue.component('my-icon', {
  template: `<button>My Icon! {{ val }}</button>`,
  props: [ 'val' ]
})

let app = new Vue({
  el: '#app',
  methods: {
    handleClick: function() {
      alert(this.icon);
    }
  },
  data: {
    icon: "Dragon"
  }
});

その場合は.native修飾子を付けることで普通のクリックイベントとして機能する。

<my-icon v-on:click.native="handleClick" v-bind:val="icon"></my-icon>

$emitで渡されるデータ

先の例のIDのように、子が持っていたデータをイベントの引数として渡す場合、プリミティブ型のデータは値渡し(コピー渡し)となるので、そのもとが親のデータであってもリアクティブが解除され元データへの影響は無い。しかしオブジェクト型の場合は参照渡しになるため、データをスコープ外でうっかり変更しても警告が出ない。親のデータがオブジェクト型の場合、コピーして渡すなどの工夫が必要。