この記事の主なターゲットはReactから入った人や、仕組みが分からないと怖くて分からないと怖くて使えないという人と、フォーム部品のコントロールの記載方法をよく忘れる人向けの記事です。
また、上級者向けとしてカスタムコンポーネントへのv-modelの利用方法も書いてみました。
先日「Vueの双方向バインディングがよしなにやってくれすぎててよくわからない」という相談を受けました。
これはおそらくVue.jsから入門した人や、Vueって簡単じゃん、便利じゃんと飛びついた人にとってはよく分からない感覚かもしれませんが、言われてみればなるほどv-modelは多くのことをやっています。
間違ってることとか足りないこととかあったらコメントとか編集リクエストください
それでは処理の概要を見ていきましょう。
v-model の概要
v-model=hoge
は v-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にラップされているのを確認できると思いますがその正体はこいつです。
つまり、v-modelはobserverがdataの変更を感知してv-bind(v-model)しているプロパティを代入を行い、inputやchangeイベント(これらはブラウザJSの標準イベント)でフォームの値が変化を感知してdataに代入を行っています。
v-model の扱いは以上でしょうか。
textではないフォームのコントロール
さて、textの扱いは上記でよいでしょう。
ところでradio buttonは?checkboxは? selectboxはどうでしょう?
主な用途からサンプルコードを考えていきたいと思います。
ラジオボタン
<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
この場合画面はこのようになります。
また、ぽちぽち触るとこんな感じ。
nameの一致でグループ分けが行われるのはHTMLの範疇ですね。
これをvueでよしなにしましょう。
ループの中とかいいんじゃないですかね。
サンプルの都合でnameが1個だけ違うみたいな罠は特に仕込んでません。
<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もありますよ。
<input type="checkbox" name="sample3" value="ぶどう">ぶどう<br>
<input type="checkbox" name="sample3" value="りんご">りんご<br>
<input type="checkbox" name="sample3" value="みかん">みかん
この場合もループで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>
この場合、チェックボックスの選択要素は配列になります。
複数の要素が選択されることがあるわけですからね!
補足:単体のCheckbox
nameを持たない単体のcheckboxの場合、v-modelでバインディングされた値は boolean値をとるようです
フォーム入力バインディング — Vue.js
select box
<select>
<option>初期選択肢</option>
<option value="りんご">りんご</option>
<option value="ぶどう">ぶどう</option>
<option value="みかん">みかん</option>
</select>
Vueで書くときはselectの方に v-model, optionにvalueを設定してあげましょう。
<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=hoge
と v-on:input="hoge = $event.target.value"
のシンタックスシュガーとなっています。
このため、子のコンポーネントは以下の2点が実装されていることを期待しています。
- Props down に value プロパティ
- Emit(Event) up に input イベントとvalue
つまり、以下のようなカスタムテキストフォームなんかをコンポーネントとして定義することができます。
<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
<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>
これだとオブジェクトを渡すとラベルとバリデーション付きInputタグをコンポーネント化できました。
今回はバリデータをコンポーネントの中に埋め込んでしまいましたが、バリデータの定義もitem オブジェクトの中に入れてしまってcomputedの中に入れてしまうとかすると面白いかもしれません。
とかとか。色々使い道があるかもしれないですね!