Help us understand the problem. What is going on with this article?

Vue.jsの双方向バインディング再入門

この記事の主なターゲットはReactから入った人や、仕組みが分からないと怖くて分からないと怖くて使えないという人と、フォーム部品のコントロールの記載方法をよく忘れる人向けの記事です。
また、上級者向けとしてカスタムコンポーネントへのv-modelの利用方法も書いてみました。

先日「Vueの双方向バインディングがよしなにやってくれすぎててよくわからない」という相談を受けました。

これはおそらくVue.jsから入門した人や、Vueって簡単じゃん、便利じゃんと飛びついた人にとってはよく分からない感覚かもしれませんが、言われてみればなるほどv-modelは多くのことをやっています。

間違ってることとか足りないこととかあったらコメントとか編集リクエストください

それでは処理の概要を見ていきましょう。

v-model の概要

v-model=hogev-bind:value="hoge"v-on:input="hoge = $event.target.value" のシンタックスシュガーです。
radio, select は束縛するイベントが input ではなく changeになります

言ってみたらいわばこれだけです。

Vue.jsのデータ管理機構 observer に関する補足

Vueのdataメソッドが返す各プロパティは、 observer によって(オブジェクトや配列の場合再帰的に)ラップされており、これが中の値の変更をキャッチして v-bind やcomputed, watchedのトリガーとなって伝搬します。
このため、Objectにキー名を指定して新しいプロパティを生やしたり配列を添え字指定して配列長を延長しながら代入するとobserver にラップしてもらうことができずデータの変更がレンダリングに反映されないこととなります。
(補足しておくと、array.push, pop, shift などの基本的な配列操作は拡張されておりこれらはobserverでラップしたもの常に返すように扱っています。)

Console.logでオブジェクトを見ると生のオブジェクトではなく必ずObserverにラップされているのを確認できると思いますがその正体はこいつです。
スクリーンショット 2020-04-05 22.57.50.png

つまり、v-modelはobserverがdataの変更を感知してv-bind(v-model)しているプロパティを代入を行い、inputやchangeイベント(これらはブラウザJSの標準イベント)でフォームの値が変化を感知してdataに代入を行っています。

v-model の扱いは以上でしょうか。

textではないフォームのコントロール

さて、textの扱いは上記でよいでしょう。
ところでradio buttonは?checkboxは? selectboxはどうでしょう?

主な用途からサンプルコードを考えていきたいと思います。

ラジオボタン

hoge.html
<input type="radio" name="sample1" value="OK">OK<br>
<input type="radio" name="sample1" value="NG">NG<br>
<input type="radio" name="group2" value="3">3

この場合画面はこのようになります。

画面収録 2020-03-25 23.04.43.gif

また、ぽちぽち触るとこんな感じ。
nameの一致でグループ分けが行われるのはHTMLの範疇ですね。

これをvueでよしなにしましょう。
ループの中とかいいんじゃないですかね。
サンプルの都合でnameが1個だけ違うみたいな罠は特に仕込んでません。

radio.vue
<template>
  <div>
    <p>{{ itemValue }}</p>
    <div v-for="(item, index) in items" :key="index">
      <input type="radio" name="sample1" :value="item" v-model="itemValue" />
      {{ item }}
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      itemValue: "りんご",
      items: ["ぶどう", "りんご", "みかん"]
    };
  }
};
</script>

この場合どのように振る舞うでしょうか?
まず、itemsがループして がループの終端まで作成されます。
="valueは" ループの子要素である item, つまり ぶどう、りんご、みかんがそれぞれ入っています。
すべてに v-model="itemValue" がついているので、 dataのitemValueはチェックがついているものが採用されます。

また、双方向でバインドされているので itemValueに別のルートで値を代入した場合、自動的にブラウザ上のラジオボタンの選択状態も切り替えることができます。
ラジオボタンなのでitemValueは常に1つのパラメータしかもちません。
うっかりnameの統一されていないradioボタンを1つのv-modelでバインドしてしまうと、 画面上複数のラジオボタンにチェックをつけられてしまいますが、実際にitemValueが持つ状態は最後に選択された1つだけなので気をつけてください(要調査)
そんなことありませんでした。まあ気をつけておくにこしたことはないです。

チェックボックス

checkboxもありますよ。

checkbox.html
  <input type="checkbox" name="sample3" value="ぶどう">ぶどう<br>
  <input type="checkbox" name="sample3" value="りんご">りんご<br>
  <input type="checkbox" name="sample3" value="みかん">みかん

スクリーンショット 2020-04-05 21.55.31.png

この場合もループでVueにしてみまよう。

select.vue
<template>
<div id="app">
  <p>{{ itemValues }}</p>
  <div v-for="(item, index) in items" :key="index">
    <input type="checkbox" name="sample" :value="item" v-model="itemValues" />
    {{ item }}
  </div>
</div>
</template>

<script>
export default {
  data() {
    return {
      itemValues: ["りんご"],
      items: ["ぶどう", "りんご", "みかん"]
    };
  }
}
</script>

この場合、チェックボックスの選択要素は配列になります。
複数の要素が選択されることがあるわけですからね!

画面収録-2020-04-05-23.05.17.gif

補足:単体のCheckbox
nameを持たない単体のcheckboxの場合、v-modelでバインディングされた値は boolean値をとるようです
フォーム入力バインディング — Vue.js

select box

select.html
  <select>
    <option>初期選択肢</option>
    <option value="りんご">りんご</option>
    <option value="ぶどう">ぶどう</option>
    <option value="みかん">みかん</option>
  </select>

スクリーンショット 2020-04-06 0.12.22.png

Vueで書くときはselectの方に v-model, optionにvalueを設定してあげましょう。

select.vue
<template>
  <div id="app">
    <p>{{itemValue}}</p>
    <select v-model="itemValue">
      <option value="" disabled>選択してください</option>
      <option v-for="(item, i) in items" :key="i" :value="item">{{item}}</option>
    </select>
  </div>
</template>

<script>
export default {
  data() {
    return {
      itemValue: "りんご",
      items: ["ぶどう", "りんご", "みかん"]
    };
  }
};
</script>

v-model の式の初期値がオプションのどれとも一致しない場合、 要素は “未選択” の状態で描画されます。iOS では、この場合 iOS が change イベントを発生させないため、最初のアイテムを選択できなくなります。したがって、上記の例に示すように、disabled な空の値のオプションを追加しておくことをおすすめします。

らしいです。

disabled, checked, selected

Vueのdisabled は booleanでコントロールできます。したがって、
<input :disabled=“isDisable”>
isDisableがtrueならその項目はdisableになります。

checked, selectedに関してはv-modelの状態に同期しているので基本的に気にしなくてよいはずです

値をNumberで受け取りたい

v-model.number="value" というように、 .number修飾子をつけてください。

修飾子には他に .lazy, .trimがあります
フォーム入力バインディング — Vue.js

上級者向け:カスタムコンポーネントにおけるv-model

カスタムコンポーネントでもv-modelを使うことができます。
この場合も概ねフォーム系タグへのシンタックスシュガーと同様で、 v-bind:value=hogev-on:input="hoge = $event.target.value"のシンタックスシュガーとなっています。

このため、子のコンポーネントは以下の2点が実装されていることを期待しています。
- Props down に value プロパティ
- Emit(Event) up に input イベントとvalue

つまり、以下のようなカスタムテキストフォームなんかをコンポーネントとして定義することができます。

customInput.vue
<template>
  <div>
    <label :for="item.id">{{ item.label }}</label>
    <input :id="item.id" v-model="editableValue" @input="emit" />
    <br />
    <p v-for="(errorMessage, i) in errorMessages" :key="i">{{ errorMessage }}</p>
  </div>
</template>

<script>
export default {
  props: ["value", "item"],
  data() {
    return {
      editableValue: this.value // propsを直接編集すると警告がでるのでコピーする
    };
  },
  methods: {
    emit() {
      this.$emit("input", this.editableValue);
    }
  },
  computed: {
    errorMessages() {
      const messages = [];
      if (this.editableValue === "") {
        return messages;
      }

      if (this.editableValue.trim().length < 5) {
        messages.push("5文字以上を設定してください");
      }
      if (this.editableValue.match(/[A-Z]/)) {
        messages.push("大文字は設定できません");
      }

      return messages;
    }
  }
};
</script>

呼び出し側はこんな感じでOK

Parent.vue
<template>
  <div>
    <CustomInput v-model="sampleValue" :item="sampleItem" />
    <p>{{ sampleValue }}</p>
  </div>
</template>

<script>
import customInput from "./hoge";

export default {
  components: { customInput },
  data() {
    return {
      sampleValue: "init",
      sampleItem: {
        id: 1,
        label: "ラベル",
      }
    };
  }
};
</script>

画面収録-2020-04-05-23.05.17.gif

これだとオブジェクトを渡すとラベルとバリデーション付きInputタグをコンポーネント化できました。
今回はバリデータをコンポーネントの中に埋め込んでしまいましたが、バリデータの定義もitem オブジェクトの中に入れてしまってcomputedの中に入れてしまうとかすると面白いかもしれません。

とかとか。色々使い道があるかもしれないですね!

Appendix

フォーム入力バインディング — Vue.js
Vue.jsのv-modelを正しく使う - Qiita

studist
「伝えることを、もっと簡単に」をミッションにビジュアルSOPマネジメントプラットフォームのBtoB SaaS「Teachme Biz」を開発・運営するスタートアップ
https://medium.com/studist-dev
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした