LoginSignup
223
213

More than 5 years have passed since last update.

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

Posted at

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

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

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

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

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

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

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. 子コンポーネントのvaluepropにfooを渡す
  2. 子コンポーネントのinputイベント時に、fooにイベント引数を代入する

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

data

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

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

computed

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

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

使い道はこんな感じ:

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

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

watch

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

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

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

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

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

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

  • 親 -> 子へデータを受け渡す際にはv-bindpropsを使う(props down)
  • 子 -> 親へデータを受け渡す際には$emitv-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

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

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

まずは基本形

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

Feb-03-2019 01-29-57.gif

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

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>

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

いったんデータを蓄える

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

Feb-11-2019 10-37-12.gif

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

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>

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

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

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

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

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

EventUpPropsDown2.018.png

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

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

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

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でそれぞれデータバインドします:

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を宣言する必要がありますが。

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-bindv-onを書かず、parentDataをまるごとデータバインドしているかのように書くことができます:

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

参考サイトなど

環境

  • vue: 2.5.22
  • vue-cli-service: 3.4.0
223
213
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
223
213