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

コンポーネントのスコープ

コンポーネントインスタンスはそれぞれが自分のスコープを持っている。 スコープ内のデータやメソッドには、thisを使ってアクセスできる。

this.message    // スコープ内データ
this.update  // スコープ内メソッド

コンポーネントは、自分以外のコンポーネントのデータやメソッドに直接アクセスすることはできない。そこでコンポーネント間でデータ共有や連携するための仕組みが必要になる。

親子コンポーネント

あるコンポーネントのテンプレート内で他のコンポーネントのカスタムタグを使うと、親子関係になる。

<div id="app">
  <my-component></my-component>
</div>
/*
親子関係のコンポーネント
*/

Vue.component('comp-child', {
  template: '<p>My Child Component</p>'
});

let myComponent = {
  template: '<div><comp-child></comp-child></div>',
};

let app = new Vue({
  el: '#app',
  data: {
    //........ 
  },
  methods: {
    //........
  },
  components: {
    'my-component': myComponent
  }
});

表示結果

<div id="app">
  <div>
    <p>My Child Component</p>
  </div>
</div>

ルートインスタンスも中に子コンポーネントを含むとみなすと、便宜上親コンポーネントとして解釈される。子コンポーネントの中に、別の子コンポーネントを含むこともでき、階層化されたそれらのコンポーネントは、DOMツリーのように構造化される。

propsを用いた親から子へのデータ渡し

例えば親コンポーネントで子コンポーネントを使用する際、属性で子に渡すデータを指定する。ここでは子コンポーネントを2回利用し、それぞれ別の文字列をvalといった属性名で指定する。

// 親コンポーネント
let myComponent = {
  template: `<div>
    <comp-child val="これは子A"></comp-child>
    <comp-child val="これは子B"></comp-child>
    </div>`,
};

/** 
 #app 側で親コンポーネントを利用
*/
let app = new Vue({
  el: '#app',
  //..................
  components: {
    'my-component': myComponent
  }
});

コンポーネント側でpropsオプションで親から受け取りたい属性名を指定しておく。

// 子コンポーネント
Vue.component('comp-child', {
  template: `<p>{{ val }} in Child Component</p>`,
  props: ["val"]
});

表示結果

<div id="app">
  <div>
    <p>これは子A in Child Component</p>
    <p>これは子B in Child Component</p>
  </div>
</div>

このやり方でインスタンスごとに別々のデータを持たせることができる。

データバインディングで親コンポーネントのリアクティブなデータを子に渡す

<comp-child v-bind:val="message" class="item"></comp-child>

ローカルに定義されたコンポーネント間で親から子へデータを渡す。

データバインディングを使えば、ローカルに定義されたコンポーネント間で親から子へデータを渡すことも問題なくできる。

<div id="app">
  <my-component></my-component>
</div>

メインコンストラクタの外部で、親コンポーネントをローカルに定義しておく。

let msg_data = { message: 'Message from Parent Comp!' };

let myComponent = {
  template: `<div>
      <comp-child v-bind:val="p_message" class='parent'></comp-child>
    </div>`,
  data: function() {
    return { p_message: msg_data.message }
  }
};

let app = new Vue({
  el: '#app',
  components: {
    'my-component': myComponent
  }
});

コード上は親コンポーネントの前に、子コンポーネントもローカルに定義しておく。ここは共通利用する子コンポーネントならグローバルな定義でもよいが。

let child_comp = Vue.component('comp-child', {
  template: '<p id="child" class="child">{{val}}</p>',
  props: ['val']
});

表示結果

<p id="child" class="child parent">Message from Parent Comp!</p>

コンポーネントに対する属性付与

コンポーネントの中で利用する子コンポーネントのカスタムタグに属性を与えた場合、propsとして受け取っていないものは、子コンポーネント側のテンプレートのルートタグの属性として、上書きまたは、複数値が設定可能なclass属性などの場合マージされる。

<div id="app">
  <my-component></my-component>
</div>
// 子コンポーネント
Vue.component('comp-child', {
  template: '<p id="child" class="child">My Child Component</p>'
});

// 親コンポーネントのカスタムタグに付与された属性は、子コンポーネントへ上書きまたはマージされる 
let myComponent = {
  template: '<div><comp-child id="parent" class="parent"></comp-child></div>',
};


let app = new Vue({
  el: '#app',
  data: {
    //......
  },
  components: {
    'my-component': myComponent
  }
});

表示結果

<div>
  <p id="parent" class="child parent">My Child Component</p>
</div>

コンポーネントを親コンポーネントでリストレンダリングして複数回使う

<div id="app">
  <ul>
    <comp-child v-for="item in list"
      v-bind:key="item.id"
      v-bind:name="item.name"
      v-bind:hp="item.hp"></comp-child>
  </ul>
</div>
Vue.component('comp-child', {
  template: '<li>{{ name }} HP.{{ hp }}</li>',
  props: ['name', 'hp']
});

var app = new Vue({
  el: '#app',
  data: {
    list: [
      { id: 1, name: 'slime', hp: 100 },
      { id: 2, name: 'goblin', hp: 200 },
      { id: 3, name: 'dragon', hp: 500 },
    ]
  }
});

表示結果

<div id="app">
  <ul>
    <li>slime HP.100</li>
    <li>goblin HP.200</li>
    <li>dragon HP.500</li>
  </ul>
</div>

propsで受け取ったデータは子の側で勝手に書き換えられない

propsはリアクティブな状態で親側のデータを受け取ることができる。したがって親側でデータが更新されると、子側も同期する。子側のメソッド内でthisを使って親から渡されたデータにアクセスできる。 ただし、子側で親から渡されたデータを勝手に書き換えることはできない。

<div id="app">
  <ul>
    <comp-child v-for="item in list"
      v-bind:key="item.id"
      v-bind:name="item.name"
      v-bind:hp="item.hp"></comp-child>
  </ul>
</div>
/*
props で受け取ったデータを子コンポーネント側で勝手に書き換えることはできない
*/

Vue.component('comp-child', {
  template: `<li>{{ name }} HP.{{ hp }}
      <button v-on:click="doAttack">攻撃する</button></li>`,
  props: ['name', 'hp'],
  method: {
    doAttack: function() {
      // 勝手に攻撃!
      this.hp -= 10;
    }
  }
});

var app = new Vue({
  el: '#app',
  data: {
    list: [
      { id: 1, name: 'slime', hp: 100 },
      { id: 2, name: 'goblin', hp: 200 },
      { id: 3, name: 'dragon', hp: 500 },
    ]
  }
});

子側のテンプレート内にボタンを設置し、クリックすると親のデータの持つhpを−10するメソッドdoAttackが起動するようにしてあるが、実際にはエラーになる。

vue.js:597 [Vue warn]: Invalid handler for event "click": got undefined

found in

---> <CompChild>
       <Root>
....................................

これを回避して子コンポーネントでデータを変更したい場合は、算出プロパティで新しいデータを作成するか、元データに変更を加える場合は子から親へ通信を行う$emitを使う。