Vueのデータバインドの基本

# こんな方に向けて書きました

* 自作コンポーネントで`v-model`したいけどやり方がわからない
* `Avoid mutating a prop directly`のメッセージが出た！
* 「イベントでやり取りする」ってどういうことよ

同様の説明をしている記事を見つけたのでこちらも参考にしてみてください: https://se-tomo.com/2018/11/03/vue-js-コンポーネント間の通信/

# おさえておきたい基本のしくみ

Vueは気軽に使い始められるところがいいところで、[ガイド](https://jp.vuejs.org/v2/guide/)を読み飛ばしてもそれっぽく使えてしまいます。
ですがガイドには結構重要なことが書いてあるので、本当は全部読んだ方がいいです(そういう私も全部は読んでいませんが)。
一方で完全な初学者〜ちょっと使い慣れたくらいでは、ガイドをちゃんと理解しながら読むのは難しいかもしれません。
そこでデータバインドの基本を押さえるにあたって、さしあたり必要な項目をまとめてみました。

私は算出プロパティとかすっかりスルーしていましたが、使い始めるとかなり便利でした・・・もっと早く知りたかった(ガイド読み飛ばしていたのが悪いですね。はい。)

## `v-bind`, `:value`

親 -> 子にデータを受け渡す際、親側で記述します。親からデータを渡すための窓口のイメージです。

`v-bind:プロパティ名`もしくは`:プロパティ名`という書き方をします。プロパティ名は、後述する子要素の`props`の変数名のことです。

## `props`

親 -> 子にデータを受け渡す際、子側で記述します。子がデータを受け取るための窓口のイメージです。

## `v-on`, `@input`

子で何らかの入力やアクション(イベントという)があった際、どんな処理をするのかを、親側で記述します。

`v-on:イベント名`もしくは`@イベント名`という書き方をします。

## `$emit`

子でクリックや文字入力など(イベントという)があったことを親に通知します。
__この仕組みを使って 子 -> 親にデータを受け渡します。__ 実はこれがVueの一見面倒なところであり、とても大事な箇所です。

`$emit`の際には「何がおきたか(イベント名)」と「引数」を渡すことができます。
引数にはなんでも渡せてしまうので、何を渡すのかを親子間で取り決めておく必要があります。

## `v-model`

`<input v-model="foo">`みたいな形でおなじみ`v-model`。
使う際にはとっても楽ちんですが、自作コンポーネントでこれをやるにはきちんとデータバインドの基本がわかっていないとできません。

`v-model="foo"`は、`v-bind:value="foo" @input="foo = $event"`と同じです。

つまり、親コンポーネントが`foo`というデータを持っていて、

1. 子コンポーネントの`value`propに`foo`を渡す
2. 子コンポーネントの`input`イベント時に、`foo`にイベント引数を代入する

という2つを行います。
`value`と`input`という名称は基本的には固定ですが、子コンポーネント側で変更することも一応可能です。

## `data`

コンポーネント自身が持つデータです。
`props`は親から渡されたデータをそのまま格納するだけで変更しないのが原則ですので、実質的に`data`が唯一のデータです。

テンプレートでデータバインドするには、基本的にはこの`data`か次の`computed`を使います。

## `computed`

`data`や`props`を加工し、あたかも通常の`data`かのように振る舞うメソッドのこと。正式には算出プロパティと言います。

実態は`data`または`props`のgetterとsetterです。テンプレートで使うときは、通常の`data`と全く同じように使います。

使い道はこんな感じ:

* フォーマットなど、`data`に何らかの加工をしてからテンプレートで表示する
* 算出setterを使うことで、代入するだけでevent upする
* __`props`を中継する算出プロパティを使って、親から渡されたデータをそのまま表示 & 入力を直ちにevent upする__

3番目の方法を使ってデータバインドすると色々嬉しいことがあります。あとで実例を挙げて解説します。

## `watch`

`data`や`props`の変更を監視して、指定したメソッドを実行します。

`computed`も似たような感じですが、`watch`の方が汎用性が高いです。
ですがVue公式ガイドでは`watch`よりも`computed`を推してます。

## `v-bind.sync`, `$emit('update:value')`

双方向バインディングっぽいことをするためのディレクティブとevent upです。初めての方には難しいのでいったん後回し。

## データバインドの全体像をつかもう

Vueの親子コンポーネント間でデータをやりとりするときの鉄則は __Events Up, Props Down__　です。
すなわち、

* 親 -> 子へデータを受け渡す際には`v-bind`と`props`を使う(props down)
* 子 -> 親へデータを受け渡す際には`$emit`と`v-on`を使う(events up)

後者をサボり、 __直接`props`をいじるとデバッグコンソールで怒られます。__

```
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"
vue.runtime.esm.js:601
```

絵にするとこんな感じ。`v-model`を多用しているとわかりにくいですが、完全な双方向バインディングではなく一方通行のデータの流れが見えてきます。

![EventUpPropsDown2.001.png](https://qiita-image-store.s3.amazonaws.com/0/156941/03918ac4-397f-5a22-b8a5-d19de6a83f9c.png)


# データバインドのパターンあれこれ

では、実際にコードでデータバインドの基本を見ていきましょう。

## まずは基本形

propで受け、computedで入出力し、変更をemitし、親はイベントで受けるパターンです。

![Feb-03-2019 01-29-57.gif](https://qiita-image-store.s3.amazonaws.com/0/156941/4359a859-9311-3ab0-8053-f0ac67df6fc7.gif)


上の絵のようなコンポーネントを作成してみます。
外側の枠が`Parent.vue`, 内側の枠が`Child.vue`に対応します。

```vuejs:Parent.vue
<template>
  <div class="parent">
    <Child :value="parentMsg" @input="parentMsg = $event"/>
    <div>入力内容: {{ parentMsg }}</div>
    <button @click="onClickRandomButton">ランダム</button>
  </div>
</template>

<script>
import Child from "./Child.vue";

export default {
  name: "Parent",
  components: {
    Child
  },
  data: function() {
    return {
      parentMsg: "",
      fruits: [
        "オレンジ",
        "バナナ",
        "グレープフルーツ",
        "クランベリー",
        "レモン",
        "マンゴー",
        "キウイフルーツ"
      ]
    };
  },
  methods: {
    onClickRandomButton: function() {
      const randomIndex = Math.floor(Math.random() * this.fruits.length);
      this.parentMsg = this.fruits[randomIndex];
    }
  }
};
</script>

```

```vuejs:Child.vue
<template>
  <div class="child">
    <label>
      果物:
      <input v-model="msg">
    </label>
  </div>
</template>

<script>
export default {
  name: "Child",
  props: {
    value: String
  },
  computed: {
    msg: {
      get: function() {
        return this.value;
      },
      set: function(newValue) {
        this.$emit("input", newValue);
      }
    }
  }
};
</script>

```

`Child`では`msg`算出プロパティをテンプレートで使っています。`msg`はget時にはpropで渡された値をそのまま中継しますが、set時にはpropに代入するのでなく`$emit("input", newValue)`でイベント発火し親コンポーネントに入力があったことを伝えています。
この方法だとテンプレート内で`v-model`を使うことができるためおすすめです。

![EventUpPropsDown2.001.png](https://qiita-image-store.s3.amazonaws.com/0/156941/da68ae00-9e84-8143-5f13-d26602eb3758.png)

## いったんデータを蓄える

propで受け、watchでdataに流し込み、dataをデータバインドし、入力時のイベントでemitし、親はイベントで受けるパターンです。
何らかの理由で算出プロパティを使いたくない場合はこちら。

![Feb-11-2019 10-37-12.gif](https://qiita-image-store.s3.amazonaws.com/0/156941/aa9054d6-75b9-0bb4-4c3f-d4b47cd53121.gif)


上の絵のように、子コンポーネントでの入力を直ちに親コンポーネントに反映せず、
ボタンクリックで親に変更を通知してみます。
算出プロパティを使うと直ちに反映されてしまうので、いったんデータを蓄えるこちらのパターンが適しています。

```vuejs:Parent.vue
<template>
  <div class="parent">
    <Child :value="parentMsg" @input="parentMsg = $event"/>
    <div>入力内容: {{ parentMsg }}</div>
    <button @click="onClickRandomButton">ランダム</button>
  </div>
</template>

<script>
import Child from "./Child.vue";

export default {
  name: "Parent",
  components: {
    Child
  },
  data: function() {
    return {
      parentMsg: "",
      fruits: [
        "オレンジ",
        "バナナ",
        "グレープフルーツ",
        "クランベリー",
        "レモン",
        "マンゴー",
        "キウイフルーツ"
      ]
    };
  },
  methods: {
    onClickRandomButton: function() {
      const randomIndex = Math.floor(Math.random() * this.fruits.length);
      this.parentMsg = this.fruits[randomIndex];
    }
  }
};
</script>

```

```vuejs:Child.vue
<template>
  <div class="child">
    <label>
      果物:
      <input v-model="msg">
    </label>
    <div>
      <button @click="onClickButton">反映↓</button>
    </div>
  </div>
</template>

<script>
export default {
  name: "Child",
  props: {
    value: String
  },
  data: () => ({
    msg: ""
  }),
  methods: {
    onClickButton: function() {
      this.$emit("input", this.msg);
    }
  },
  watch: {
    value: function(newValue) {
      this.msg = newValue;
    }
  }
};
</script>

```

![EventUpPropsDown2.014.png](https://qiita-image-store.s3.amazonaws.com/0/156941/f953d371-1d20-4ea7-1f28-93c4fafadcfb.png)


テンプレートで使うのはあくまでも`msg`とし、propの変更をwatchにより監視して`msg`を更新する方法です。
propの変更は常時watchされるので、「ランダム」ボタンを押した際には即時子コンポーネント側のdataは変更されます。
一方子コンポーネントの変更は監視されておらず、入力があったタイミングで`input`イベントが発火されるようには指定していない(入力とイベント発火を切り離している)ため、「反映」ボタンをクリックしたタイミングで初めて親コンポーネントに入力内容が通知されます。


## (非推薦) 親のデータを直接書き換える

propでObjectを受け、Objectを書き換えるパターンです。

親コンポーネントから受けとったpropがオブジェクトの場合には、オブジェクトの内容を子で書き換えてもVueに怒られたりしません。
怒られないという事実に気が付いてしまい、結構楽なので使ってみようかと思ったこともありましたが、推薦はされないようです。
でもちゃんとリアクティブになってるんだよなぁ。

![EventUpPropsDown2.018.png](https://qiita-image-store.s3.amazonaws.com/0/156941/a555ad99-ad41-42ca-e3d4-0a9573ac720e.png)


これがやりたいのは、「たくさんのキー・値を含むオブジェクトなので、個別にpropを受け取るのではなく、オブジェクト丸ごと受け取ってデータバインドしたい」みたいなケースだと思います。
そういう場合に対応できるのが、次項の「syncパターン」です。

## 親からObjectを受け取って中身を書き換えたい: syncパターン

たくさんのキー・値を含むオブジェクトだからといって、オブジェクト丸ごと受け取り、propを直接書き換えてしまうのはお行儀がいいとは言えません。
面倒でも、個別のプロパティをpropsで宣言し、データの変更時にevent upするのが本来の方法とされています。
ですがそれだと下のように、たくさんデータバインドしなければならず面倒です。

```vuejs:Child.vue/一部
<template>
  <div class="child">
    <label class="item">氏名:
      <input v-model="nameComputed">
    </label>
    <label class="item">好きな果物:
      <input v-model="fruitComputed">
    </label>
    <label class="item">好きな花:
      <input v-model="flowerComputed">
    </label>
  </div>
</template>

<script>
export default {
  name: "Child",
  props: {
    name: String,
    fruit: String,
    flower: String
  },

略

}

```

`Child.vue`で定義した3つのprop(name, fruit, flower)に対して、`Parent.vue`でそれぞれデータバインドします:

```vuejs:Parent.vue/あっているが面倒な書き方
<template>
  <div class="parent">
    <Child
      :name="parentData.name"
      @updated-name="parentData.name = $event"
      :fruit="parentData.fruit"
      @updated-fruit="parentData.fruit = $event"
      :flower="parentData.flower"
      @updated-flower="parentData.flower = $event"
    />
    <div>入力内容:
      <div>氏名: {{parentData.name}}</div>
      <div>好きな果物: {{parentData.fruit}}</div>
      <div>好きな花: {{parentData.flower}}</div>
    </div>
  </div>
</template>

<script>
import Child from "./Child.vue";

export default {
  name: "Parent",
  components: {
    Child
  },
  data: function() {
    return {
      parentData: {
        name: "",
        fruit: "",
        flower: ""
      }
    };
  }
};
</script>

```

そんなとき、子コンポーネント側で以下の条件を満たせば、`v-bind.sync`という省略記法が使えます。

* events upするときのイベント名を`updated:プロパティ名`としておく

これで、親 -> 子にデータバインドする際の記述を大幅に省略できます。
残念ながら子コンポーネント側にはたくさんのpropsとcomputed dataを宣言する必要がありますが。

```vuejs:Child.vue/全部
<template>
  <div class="child">
    <label class="item">氏名:
      <input v-model="nameComputed">
    </label>
    <label class="item">好きな果物:
      <input v-model="fruitComputed">
    </label>
    <label class="item">好きな花:
      <input v-model="flowerComputed">
    </label>
  </div>
</template>

<script>
export default {
  name: "Child",
  props: {
    name: String,
    fruit: String,
    flower: String
  },
  computed: {
    nameComputed: {
      get: function() {
        return this.name;
      },
      set: function(newValue) {
        this.$emit("update:name", newValue);
      }
    },
    fruitComputed: {
      get: function() {
        return this.fruit;
      },
      set: function(newValue) {
        this.$emit("update:fruit", newValue);
      }
    },
    flowerComputed: {
      get: function() {
        return this.flower;
      },
      set: function(newValue) {
        this.$emit("update:flower", newValue);
      }
    }
  }
};
</script>

```

3つのprop(name, fruit, flower)それぞれの`v-bind`と`v-on`を書かず、`parentData`をまるごとデータバインドしているかのように書くことができます:

```vuejs:Parent.vue/あっていてシンプルな書き方
<template>
  <div class="parent">
    <Child v-bind.sync="parentData" />
    <div>入力内容:
      <div>氏名: {{parentData.name}}</div>
      <div>好きな果物: {{parentData.fruit}}</div>
      <div>好きな花: {{parentData.flower}}</div>
    </div>
  </div>
</template>

あとは一緒

```

![EventUpPropsDown2.017.png](https://qiita-image-store.s3.amazonaws.com/0/156941/f53d57e2-e925-de26-1365-e98fbe3ceae1.png)


# 参考サイトなど

* [Is mutating object props bad practice?](https://forum.vuejs.org/t/is-mutating-object-props-bad-practice/17448)
* [Vue ガイド: プロパティ - Vue.js](https://jp.vuejs.org/v2/guide/components-props.html#オブジェクトの受け渡し)

# 環境

* vue: 2.5.22
* vue-cli-service: 3.4.0