こんにちは、コンスキです。
今回は、双方向バインディングをより簡単にできるようになる、v-modelディレクティブについて説明します。
v-modelディレクティブとは
v-onディレクティブとv-bindディレクティブを使った双方向バインディングは、すでに説明しました。しかし、ディレクティブを2つ使っているため記述するコード量がどうしても多くなっていしまいます。
v-modelディレクティブとは、v-onディレクティブとv-bindディレクティブの役割を1つで担っているディレクティブです。
v-modelディレクティブを使うと、v-modelディレクティブ単独で双方向バインディングを実現できるため、少ないコード量で簡単に記述することができます。
v-modelディレクティブの使い方
基本的な書き方
v-modelディレクティブは主にinput要素に対して使います。input要素につけるtype属性によっても書き方が異なるので、2つのtype属性における基本的な書き方をそれぞれ説明します。
入力フォームにおける双方向バインディング【type=”text”】
まずは、フォームに入力した文字を画面の他の部分に表示させる場合の書き方をご紹介します。個人的には、双方向バインディングといったら入力フォームという感じがします(^^)
HTML側↓
<div id="app">
<p>{{ 画面に表示させたい値のキー }}</p>
<input type="text" v-model="画面の他の部分に表示させたい値のキー">
</div>
Javascript側↓
const vm = new Vue({
el: "#app",
data: {
キー:画面に表示させたい値
}
})
コードの赤くなっている部分は、同じもの書かれることを意味しています。{{}}(マスタッシュタグ)で囲んだ部分にdataプロパティに設定した画面に表示させたい値を表示させます。
これだけの記述量で双方向バインディングを使う事ができるとは、すごく便利なディレクティブですよね。
実際にフォームに入力した文字を画面の別の場所表示させるコードを書いてみます。dataプロパティに設定しておく値のキーは「message」にします。
フォームに入力した文字を表示させるコード↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>v-modelディレクティブ</title>
<script src="https://unpkg.com/vue"></script>
</head>
<body>
<div id="app">
<p>フォームに入力された内容:{{ message }}</p>
<input type="text" v-model="message">
</div>
<script>
const vm = new Vue({
el: "#app",
data: {
message: ""
}
})
</script>
</body>
</html>
ブラウザ上の表示↓
フォームに入力した「こんにちは」という内容が、コードの中で{{}}で囲んだ部分にも表示されていると思います。
ラジオボタンにおける双方向バインディング【type=”radio”】
次はラジオボタンを押すとその値を画面に表示されるコードの書き方です。ToDoリストの未完了/完了済みの切り替えなどに応用できそうですね。
HTML側↓
<div id="app">
<p>{{ 表示する値のキー }}</p>
<div v-for="変数名 in ラジオボタン値の配列のキー" v-bind:key="ラジオボタン値の配列のキー.id">
<input type="radio" v-bind:value="変数名.value" name="sample" v-model="表示する値のキー">{{ 変数名.value }}
</div>
</div>
Javascript側↓
const vm = new Vue({
el: "#app",
data: {
キー: "表示する値の初期値"
キー: [
{
value: "ラジオボタンの値1",
id: 1
},
{
value: "ラジオボタンの値2",
id: 2
},
{
value: "ラジオボタンの値3",
id: 3
},
︙ //ラジオボタンの値を入れたオブジェクトが続く
]
}
})
言葉で書くと逆に分かりづらいと思うので、ラジオボタンの双方向バインディングを使った具体的なプログラムを書きます。次のプログラムは好きな動物を質問するアンケートのようなものです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>v-modelディレクティブ</title>
<script src="https://unpkg.com/vue"></script>
</head>
<body>
<div id="app">
<p>Q.あなたが好きな動物はなんですか?</p>
<div v-for="radioValue in radioValues" v-bind:key="radioValue.id">
<input type="radio" v-bind:value="radioValue.value" name="sample" v-model="displayedValue">{{ radioValue.value }}
</div>
<p>あなたの回答は{{ displayedValue }}です。</p>
</div>
<script>
const vm = new Vue({
el: "#app",
data: {
displayedValue: "まだ未選択",
radioValues: [
{
value: "イヌ",
id: 1
},
{
value: "ネコ",
id: 2
},
{
value: "ウサギ",
id: 3
}
]
}
})
</script>
</body>
</html>
ブラウザ上に表示してみると次のようになります。
表示される値の初期値が「まだ未選択」という文字列になっているため、はじめはこのように表示されます。
次にラジオボタンを選択すると選択したラジオボタンの値が「まだ未選択」の部分に表示されます。
このときはウサギを選択したため「まだ未選択」の部分には「ウサギ」が表示されました。
v-modelディレクティブの注意点
繰り返しになりますがv-modelディレクティブを使った双方向バインディングは記述量が少なくて、v-onとv-bindを使った双方向バインディングよりも簡単に記述することができます。
フォームに入力した内容を画面の別の部分に表示させるコードで比較してみると次のようになります
v-on + v-bind による双方向バインディング↓
<div id="app">
<p>フォームに入力された内容:{{ message }}</p>
<input v-bind:value="message" v-on:input="changeMessage">
</div>
<script>
const vm = new Vue({
el: "#app",
data: {
message: ""
},
methods: {
changeMessage: function(event) {
this.message = event.target.value;
}
}
})
</script>
v-modelによる双方向バインディング↓
<div id="app">
<p>フォームに入力された内容:{{ message }}</p>
<input type="text" v-model="message">
</div>
<script>
const vm = new Vue({
el: "#app",
data: {
message: ""
}
})
</script>
フォームが1つの場合、記述量の差はごくわずかですが、フォームの個数が増えるほどコード量の差は広がっていきます。
違いは少ないにしても、たしかに記述量がv-modelのほうが少ない事がわかりました。しかし、この2つの双方向バインディングの使い分け方には注意点があります。単純にコードの記述量だけではどちらを使うか決められません。
その注意点とは、入力された内容が画面に反映されるタイミングの違いです。
v-onディレクティブとv-bindディレクティブを使った双方向バインディングでは、フォームの内容が変更される度に画面の他の場所に入力された内容が反映されて表示されます。
一方で、v-modelディレクティブを使った双方向バインディングでは、フォームの内容が変更されても、入力が確定されない限り画面の他の場所に入力された内容が反映されません。
言葉だけでは分かりづらいと思いますので、動作の違いを実際に目で見てみましょう。
v-on + v-bind↓
v-model↓
おわかりいただけるでしょうか。これでもかなり分かりづらいのですが、v-modelの場合は入力内容の下にある線が消えて入力が確定されてから画面の別の部分に表示されています。
日本語や中国語などの変換と入力の確定を必要とするする入力では、入力した内容が画面に反映されるタイミングの違いにご注意下さい。
サンプルプログラム
それでは、v-modelを使ったサンプルプログラムを作っていきます。今回はのプログラムは、前回のBMI計算機のようなプログラムにラジオボタンを付け加えます。
加えたラジオボタンの役割は、「肥満気味です」という警告が出るBMIの最低値を設定するものです。最低値は25と30の2つがあります。
25を選択した状態であるとBMIが25以上の人に対して「肥満気味です」という警告が表示されます。また、30を選択した状態であるとBMIが25以上出会っても30未満の人に対しては警告が表示されなくなり、BMI30以上の人かに対してのみ「肥満です」の警告が表示されるようになります。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>v-modelディレクティブ</title>
<script src="https://unpkg.com/vue"></script>
</head>
<body>
<div id="app">
<div>
名前:<input v-on:input="setName" v-on:keydown.enter.exact="createPerson()" v-bind:value="newPerson.name"
placeholder="名前を入力して下さい">
身長(cm):<input v-on:input="setHeight" v-on:keydown.enter.exact="createPerson()" v-bind:value="newPerson.height"
placeholder="身長を入力して下さい">
身長(cm):<input v-on:input="setWeight" v-on:keydown.enter.exact="createPerson()" v-bind:value="newPerson.weight"
placeholder="体重を入力して下さい">
<button v-on:click="createPerson()">計算する</button>
<p v-if="nameAlert" v-bind:style="alertObjects[0]">名前を入力して下さい。</p>
<p v-if="heightAlert" v-bind:style="alertObjects[1]">身長を入力して下さい。</p>
<p v-if="weightAlert" v-bind:style="alertObjects[2]">体重を入力して下さい。</p>
</div>
<!-- 警告を表示するBMIの最低値を設定するラジオボタン -->
<p>警告を表示する対象者を選択して下さい</p>
<div v-for="(radioValue, index) in radioValues" v-bind:key="radioValue.id">
<input type="radio" v-bind:value="radioValue.value" name="sample" v-model="minBMI">{{ messageOption[index] }}(BMIが{{ radioValue.value }}以上)の方に対して
</div>
<p>※現在、BMIが{{ minBMI }}以上の方のみに「{{ changeMessage }}です」という警告を表示する設定になっています。</p>
<!-- 他の配列の要素の指定を行うためにindex配列peopleのインデックスを使う -->
<div v-for="(person, index) in people" v-bind:key="person.name">
<p>名前:{{ person.name }}</p>
<p>身長:{{ person.height }}cm</p>
<p>体重:{{ person.weight }}kg</p>
<p v-bind:style="styleOject[index]">BMI:{{calcBMI[index]}}</p>
<p v-show="isBitBig[index]">{{ changeMessage }}です</p>
<br>
</div>
</div>
<script>
const vm = new Vue({
el: "#app",
data: {
people: [],
//BMIがminBMI以上の時に適用させるstyleオブジェクト
alertObject: {
color: '#FF7070',
fontWeight: 'bold',
border: '4px double #FF7070',
display: 'inline'
}
,
alertObjects: [{}, {}, {}],
newPerson: {
name: undefined,
height: undefined,
weight: undefined
},
nameValue: undefined,
heightValue: undefined,
weightValue: undefined,
nameAlert: false,
heightAlert: false,
weightAlert: false,
minBMI: 25,
messageOption: ["肥満気味", "肥満"],
radioValues: [
{
value: 25,
id: 1
},
{
value: 30,
id: 2
}
],
},
computed: {
calcBMI: function () {
var BMI = [];
for (var i = 0; i < this.people.length; i++) {
var mHeight = this.people[i].height / 100;
BMI.push(Math.floor(this.people[i].weight / mHeight / mHeight));
}
return BMI
},
isBitBig: function () {
var flag = [];
for (var i = 0; i < this.people.length; i++) {
if (this.calcBMI[i] >= this.minBMI) {
flag.push(true);
} else {
flag.push(false);
}
}
return flag
},
styleOject: function () {
var styleOjects = []; //全員のsyleオブジェクトを入れておくリスト
for (var i = 0; i < this.people.length; i++) {
//BMIがminBMI以上だったらdataプロパティにあるstyleオブジェクト返し、minBMI未満だったら空のオブジェクトを返す
var so = this.isBitBig[i] ? this.alertObject : {};
styleOjects.push(so);
}
return styleOjects
},
changeMessage: function () {
var message = this.minBMI === 25 ? this.messageOption[0] : this.messageOption[1];
return message
}
},
methods: {
setName: function (event) {
this.nameValue = event.target.value;
this.newPerson.name = this.nameValue;
},
setHeight: function (event) {
this.heightValue = event.target.value;
this.newPerson.height = this.heightValue;
},
setWeight: function (event) {
this.weightValue = event.target.value;
this.newPerson.weight = this.weightValue;
},
createPerson: function () {
// 空欄があればアラートを表示するフラグ、スタイルを設定
this.nameAlert = !this.nameValue ? true : false;
this.alertObjects[0] = !this.nameValue ? this.alertObject : {};
this.heightAlert = !this.heightValue ? true : false;
this.alertObjects[1] = !this.heightValue ? this.alertObject : {};
this.weightAlert = !this.weightValue ? true : false;
this.alertObjects[2] = !this.weightValue ? this.alertObject : {};
if (this.nameValue && this.heightValue && this.weightValue) {
this.people.push(this.newPerson);
this.newPerson = {
name: undefined,
height: undefined,
weight: undefined
}
this.nameValue = undefined;
this.heightValue = undefined;
this.weightValue = undefined;
}
}
}
})
</script>
</body>
</html><!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>v-modelディレクティブ</title>
<script src="https://unpkg.com/vue"></script>
</head>
<body>
<div id="app">
<div>
名前:<input v-on:input="setName" v-on:keydown.enter.exact="createPerson()" v-bind:value="newPerson.name"
placeholder="名前を入力して下さい">
身長(cm):<input v-on:input="setHeight" v-on:keydown.enter.exact="createPerson()" v-bind:value="newPerson.height"
placeholder="身長を入力して下さい">
体重(kg):<input v-on:input="setWeight" v-on:keydown.enter.exact="createPerson()" v-bind:value="newPerson.weight"
placeholder="体重入力して下さい">
<button v-on:click="createPerson()">計算する</button>
<p v-if="nameAlert" v-bind:style="alertObjects[0]">名前を入力して下さい。</p>
<p v-if="heightAlert" v-bind:style="alertObjects[1]">身長を入力して下さい。</p>
<p v-if="weightAlert" v-bind:style="alertObjects[2]">体重を入力して下さい。</p>
</div>
<!-- 警告を表示するBMIの最低値を設定するラジオボタン -->
<p>警告を表示する対象者を選択して下さい</p>
<div v-for="(radioValue, index) in radioValues" v-bind:key="radioValue.id">
<input type="radio" v-bind:value="radioValue.value" name="sample" v-model="minBMI">{{ messageOption[index] }}(BMIが{{ radioValue.value }}以上)の方に対して
</div>
<p>※現在、BMIが{{ minBMI }}以上の方のみに「{{ changeMessage }}です」という警告を表示する設定になっています。</p>
<!-- 他の配列の要素の指定を行うためにindex配列peopleのインデックスを使う -->
<div v-for="(person, index) in people" v-bind:key="person.name">
<p>名前:{{ person.name }}</p>
<p>身長:{{ person.height }}cm</p>
<p>体重:{{ person.weight }}kg</p>
<p v-bind:style="styleOject[index]">BMI:{{calcBMI[index]}}</p>
<p v-show="isBitBig[index]">{{ changeMessage }}です</p>
<br>
</div>
</div>
<script>
const vm = new Vue({
el: "#app",
data: {
people: [],
//BMIがminBMI以上の時に適用させるstyleオブジェクト
alertObject: {
color: '#FF7070',
fontWeight: 'bold',
border: '4px double #FF7070',
display: 'inline'
}
,
alertObjects: [{}, {}, {}],
newPerson: {
name: undefined,
height: undefined,
weight: undefined
},
nameValue: undefined,
heightValue: undefined,
weightValue: undefined,
nameAlert: false,
heightAlert: false,
weightAlert: false,
minBMI: 25,
messageOption: ["肥満気味", "肥満"],
radioValues: [
{
value: 25,
id: 1
},
{
value: 30,
id: 2
}
],
},
computed: {
calcBMI: function () {
var BMI = [];
for (var i = 0; i < this.people.length; i++) {
var mHeight = this.people[i].height / 100;
BMI.push(Math.floor(this.people[i].weight / mHeight / mHeight));
}
return BMI
},
isBitBig: function () {
var flag = [];
for (var i = 0; i < this.people.length; i++) {
if (this.calcBMI[i] >= this.minBMI) {
flag.push(true);
} else {
flag.push(false);
}
}
return flag
},
styleOject: function () {
var styleOjects = []; //全員のsyleオブジェクトを入れておくリスト
for (var i = 0; i < this.people.length; i++) {
//BMIがminBMI以上だったらdataプロパティにあるstyleオブジェクト返し、minBMI未満だったら空のオブジェクトを返す
var so = this.isBitBig[i] ? this.alertObject : {};
styleOjects.push(so);
}
return styleOjects
},
changeMessage: function () {
var message = this.minBMI === 25 ? this.messageOption[0] : this.messageOption[1];
return message
}
},
methods: {
setName: function (event) {
this.nameValue = event.target.value;
this.newPerson.name = this.nameValue;
},
setHeight: function (event) {
this.heightValue = event.target.value;
this.newPerson.height = this.heightValue;
},
setWeight: function (event) {
this.weightValue = event.target.value;
this.newPerson.weight = this.weightValue;
},
createPerson: function () {
// 空欄があればアラートを表示するフラグ、スタイルを設定
this.nameAlert = !this.nameValue ? true : false;
this.alertObjects[0] = !this.nameValue ? this.alertObject : {};
this.heightAlert = !this.heightValue ? true : false;
this.alertObjects[1] = !this.heightValue ? this.alertObject : {};
this.weightAlert = !this.weightValue ? true : false;
this.alertObjects[2] = !this.weightValue ? this.alertObject : {};
if (this.nameValue && this.heightValue && this.weightValue) {
this.people.push(this.newPerson);
this.newPerson = {
name: undefined,
height: undefined,
weight: undefined
}
this.nameValue = undefined;
this.heightValue = undefined;
this.weightValue = undefined;
}
}
}
})
</script>
</body>
</html><!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>v-modelディレクティブ</title>
<script src="https://unpkg.com/vue"></script>
</head>
<body>
<div id="app">
<div>
<input v-on:input="setName" v-on:keydown.enter.exact="createPerson()" v-bind:value="newPerson.name"
placeholder="名前を入力して下さい">
<input v-on:input="setHeight" v-on:keydown.enter.exact="createPerson()" v-bind:value="newPerson.height"
placeholder="身長を入力して下さい">
<input v-on:input="setWeight" v-on:keydown.enter.exact="createPerson()" v-bind:value="newPerson.weight"
placeholder="体重入力して下さい">
<button v-on:click="createPerson()">計算する</button>
<p v-if="nameAlert" v-bind:style="alertObjects[0]">名前を入力して下さい。</p>
<p v-if="heightAlert" v-bind:style="alertObjects[1]">身長を入力して下さい。</p>
<p v-if="weightAlert" v-bind:style="alertObjects[2]">体重を入力して下さい。</p>
</div>
<!-- 警告を表示するBMIの最低値を設定するラジオボタン -->
<p>警告を表示する対象者を選択して下さい</p>
<div v-for="(radioValue, index) in radioValues" v-bind:key="radioValue.id">
<input type="radio" v-bind:value="radioValue.value" name="sample" v-model="minBMI">{{ messageOption[index] }}(BMIが{{ radioValue.value }}以上)の方に対して
</div>
<p>※現在、BMIが{{ minBMI }}以上の方のみに「{{ changeMessage }}です」という警告を表示する設定になっています。</p>
<!-- 他の配列の要素の指定を行うためにindex配列peopleのインデックスを使う -->
<div v-for="(person, index) in people" v-bind:key="person.name">
<p>名前:{{ person.name }}</p>
<p>身長:{{ person.height }}cm</p>
<p>体重:{{ person.weight }}kg</p>
<p v-bind:style="styleOject[index]">BMI:{{calcBMI[index]}}</p>
<p v-show="isBitBig[index]">{{ changeMessage }}です</p>
<br>
</div>
</div>
<script>
const vm = new Vue({
el: "#app",
data: {
people: [],
//BMIがminBMI以上の時に適用させるstyleオブジェクト
alertObject: {
color: '#FF7070',
fontWeight: 'bold',
border: '4px double #FF7070',
display: 'inline'
}
,
alertObjects: [{}, {}, {}],
newPerson: {
name: undefined,
height: undefined,
weight: undefined
},
nameValue: undefined,
heightValue: undefined,
weightValue: undefined,
nameAlert: false,
heightAlert: false,
weightAlert: false,
minBMI: 25,
messageOption: ["肥満気味", "肥満"],
radioValues: [
{
value: 25,
id: 1
},
{
value: 30,
id: 2
}
],
},
computed: {
calcBMI: function () {
var BMI = [];
for (var i = 0; i < this.people.length; i++) {
var mHeight = this.people[i].height / 100;
BMI.push(Math.floor(this.people[i].weight / mHeight / mHeight));
}
return BMI
},
isBitBig: function () {
var flag = [];
for (var i = 0; i < this.people.length; i++) {
if (this.calcBMI[i] >= this.minBMI) {
flag.push(true);
} else {
flag.push(false);
}
}
return flag
},
styleOject: function () {
var styleOjects = []; //全員のsyleオブジェクトを入れておくリスト
for (var i = 0; i < this.people.length; i++) {
//BMIがminBMI以上だったらdataプロパティにあるstyleオブジェクト返し、minBMI未満だったら空のオブジェクトを返す
var so = this.isBitBig[i] ? this.alertObject : {};
styleOjects.push(so);
}
return styleOjects
},
changeMessage: function () {
var message = this.minBMI === 25 ? this.messageOption[0] : this.messageOption[1];
return message
}
},
methods: {
setName: function (event) {
this.nameValue = event.target.value;
this.newPerson.name = this.nameValue;
},
setHeight: function (event) {
this.heightValue = event.target.value;
this.newPerson.height = this.heightValue;
},
setWeight: function (event) {
this.weightValue = event.target.value;
this.newPerson.weight = this.weightValue;
},
createPerson: function () {
// 空欄があればアラートを表示するフラグ、スタイルを設定
this.nameAlert = !this.nameValue ? true : false;
this.alertObjects[0] = !this.nameValue ? this.alertObject : {};
this.heightAlert = !this.heightValue ? true : false;
this.alertObjects[1] = !this.heightValue ? this.alertObject : {};
this.weightAlert = !this.weightValue ? true : false;
this.alertObjects[2] = !this.weightValue ? this.alertObject : {};
if (this.nameValue && this.heightValue && this.weightValue) {
this.people.push(this.newPerson);
this.newPerson = {
name: undefined,
height: undefined,
weight: undefined
}
this.nameValue = undefined;
this.heightValue = undefined;
this.weightValue = undefined;
}
}
}
})
</script>
</body>
</html>
コードが非常に長くなってしまってすみません。
ブラウザ上には次のように表示されると思います。
フォーム名前、身長、体重を入力して「計算する」というボタンを押すと名前、身長、体重そして計算されたBMIの一覧が表示されます。
「肥満気味」と書かれたラジオボタンが選択されていれば、BMIが25以上の人の一覧には警告が表示されます。「肥満」と書かれたラジオボタンが選択されていれば、BMIが30以上の人のみ警告が表示されます。
例として、名前が「太郎」で身長が「165」cm、体重が「85」kgの人のBMIを計算すると次のように表示されます。
この状態で、警告を表示する対象者を「肥満(BMIが30以上)の方」に変更します。すると、以下のように表示されると思います。
まるで囲んであるところが変化します。これはラジオボタンと表示するデータを双方向バインディングで結びつけているためです。
これで説明を終わりにします。最後まで読んでくれてありがとうございました。今回でVue.jsの基本的なディレクティブはほとんど説明し終わりました。
次回は、Vue.jsのライフサイクルフックというものについて説明します。ライフサイクルを使うと「ページが読み込まれたときに関数を使いたい」というようなことも実現できるので学んでみて下さい。
まとめ
今回わかったこと
- ・v-modelディレクティブは双方向バインディングを簡単に実現できる
- ・v-modelディレクティブは主にinput要素に使うがtypeの属性値によっても使い方が異なる
- ・v-on + v-bind の双方向バインディングとv-modelの双方向バインディングは全く同じわけではない
コメント