Chapter 05

フォーム入力バインディング

OJK
OJK
2021.10.18に更新

今回は HTML のフォーム入力とデータプロパティのバインディングについて説明します。フォームからの入力を受け付けることによって一気にアプリケーションらしくなります。フォーム入力の種類ごとに対応するデータプロパティの型が異なるので、代表的な要素についてひととおり眺めていきましょう。

雛形コード
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>petite-vue入門</title>
</head>
<body>

  <p>Petite Vue!!</p>

  <script src="https://unpkg.com/petite-vue"></script>
  <script src="script.js"></script>
</body>
</html>
script.js
'use strict';

PetiteVue.createApp({

}).mount();

単方向バインディング

そもそもの話として、フォーム入力とデータプロパティのバインディングは v-bind ではできないのでしょうか。

例えば、input 要素(テキストボックス)の値であれば、「v-bind:value="データプロパティ"」としてやれば対応づけできそうな気がします。以下のようなコードで、テキストボックスへの入力を p 要素に表示できないでしょうか。

HTML
<input :value="text">
<p>{{text}}</p>
JS
PetiteVue.createApp({
  text: ''
}).mount();

残念ながら、このコードは意図したようには動きません。ただし、データプロパティの定義のほうで text = 'abc' としてやると、テキストボックスに「abc」と入力された状態になり、p 要素にも「abc」と表示されます。

この振る舞いからわかるように、v-bind は「データプロパティ → HTML」という単方向のバインディングになります。「HTML → データプロパティ」という方向にはデータを渡してくれません。

v-on を使えば、これを強引に双方向のバインディング、つまり「データプロパティ ↔ HTML」にすることができます。

HTML
<input @input="log" :value="text">
<p>{{text}}</p>
JS
PetiteVue.createApp({
  text: '',
  log(ev) {
    this.text = ev.currentTarget.value;
  }
}).mount();

このコードを実行すると、テキストボックスに入力した文字が p 要素に表示されます。ボタン等を押すことなく即座に表示されていくのでちょっと驚くかもしれません。

仕組みを説明しておくと、

  1. テキストボックスに文字を入力すると input イベントが発生する
  2. input イベントが発生するとコールバック関数 log を実行するように v-on で登録する
  3. コールバック関数 log の中で、データプロパティ text にテキストボックス(input 要素)の value 属性値を代入する

このようにして「HTML → データプロパティ」の流れを強制的に作っています。チャプター 3 で学んだ Event オブジェクトが登場しましたね。

しかし毎回このような処理を手動で書くのは面倒ですよね。petite-vue には、ちゃんと双方向にバインディングしてくれるディレクティブが用意されているので大丈夫です。

v-model ディレクティブ

フォーム入力系の要素に v-model ディレクティブ を追加すると、value 属性とデータプロパティは双方向にバインディングされます。

v-modelディレクティブ
<HTML要素名 v-model="データプロパティ名">

v-model というディレクティブに直接データプロパティ名を指定します。v-bind のように「v-model:value="データプロパティ"」とは書かないので注意してください。

さきほどのテキストボックスの例を v-model で書き換えてみましょう。

HTML
<input v-model="text">
<p>{{text}}</p>
JS
PetiteVue.createApp({
  text: '',
}).mount();

v-bind だけを使って実現しようとした冒頭のコードとほぼ同じですね。「:value」が「v-model」に変わっただけです。input イベントやコールバック関数を使う必要もなく、極めて簡潔です。なお、v-model には省略形はありません。省略形があるのは v-on と v-bind だけです。

テキストボックスの場合、データプロパティが受け取るのは文字列となります。したがって、データプロパティの初期値には空文字列 ' ' を指定します。フォームに何かしら初期値を設定しておきたい場合はデータプロパティに値を設定しておいても構いませんが、プレースホルダ用途なら素直に placeholder 属性を使います。

v-model の記法はフォーム入力の種類によって少しずつ異なります。input[type="email"] 要素や textarea 要素など、入力値が単体の文字列になるものはテキストボックスと同じです。しかし、ラジオボタンやチェックボックス、ドロップダウンメニューなどは扱いが少し異なります。本チャプターの残りはそれら代表的なフォーム入力をひととおり見ていくことにします。

なお、素の JavaScript での入力フォームの扱いを忘れてしまったという人は、こちら を先に復習しておくとよいでしょう。

ラジオボタン

ラジオボタンの場合は次のようになります。label 要素は省略しています。

HTML
<input type="radio" name="box" v-model="box" value="大判">玉手箱(大)<br>
<input type="radio" name="box" v-model="box" value="小判">玉手箱(中)<br>
<input type="radio" name="box" v-model="box" value="老化">玉手箱(小)

<p>{{box}}</p>
JS
PetiteVue.createApp({
  box: ''
}).mount();

全ての input 要素に v-model を追加する必要があります。ラジオボタンから受け取る入力値は単体の文字列(value 属性値)なので、データプロパティの型は「文字列」になります。なお、name 属性値とデータプロパティ名がどちらも「box」になっていますが、一致させる必要はありません。

いずれかのラジオボタンが最初に選択されている状態にしたいときは、v-model に指定したデータプロパティの初期値をそのラジオボタンの value 属性値と同じにしてください。petite-vue を使用すると、checked 属性よりもデータプロパティの値のほうが優先されるからです。

さて、同じことを素の JavaScript で書くと次のようになります。petite-vue なら 3 行ですから、かなり簡潔になっていることがわかりますね。

JS(素のJavaScript)
const p = document.querySelector('p');
const radio = document.querySelectorAll('input[type="radio"]');

for (const btn of radio) {
  btn.addEventListener('change', () => {
    if (btn.checked == true) {
      p.textContent = btn.value;
    }
  });
}

チェックボックス

チェックボックスは、複数選択か単体チェックボックスかで振る舞いが変わります。

複数選択ボックス

複数選択のチェックボックスの場合は次のようになります。label 要素と name 属性は省略しています。

HTML
<input type="checkbox" v-model="box" value="大判"> 玉手箱(大)<br>
<input type="checkbox" v-model="box" value="小判"> 玉手箱(中)<br>
<input type="checkbox" v-model="box" value="老化"> 玉手箱(小)

<p>{{box}}</p>
JS
PetiteVue.createApp({
  box: []
}).mount();

チェックボックスも全ての input 要素に v-model を追加します。ラジオボタンとの違いは、データプロパティの型が「配列」になることです。チェックした順にその value 属性値が配列に追加されていきます。チェックを外すと配列から外れます。

この 3 行の petite-vue のコードと同じことを素の JavaScript で書くと次のようになりますが、読むのも嫌になりますね。

JS(素のJavaScript)
const p = document.querySelector('p');
const checkbox = document.querySelectorAll('input[type="checkbox"]');
const values = [];

for (const box of checkbox) {
  box.addEventListener('change', () => {
    if (box.checked == true) {
      values.push(box.value);
    } else {
      const removeIndex = values.indexOf(box.value);
      values.splice(removeIndex, 1);
    }
    p.textContent = values;
  });
}

単体チェックボックス

単体のチェックボックスの場合、データプロパティの値はブール値(true/false)になります。input 要素に value 属性値で別の値を与えても変更できません(ブール値になります)。

HTML
<input type="checkbox" v-model="hasTime"> ひま?
<p>{{hasTime ? 'はい' : 'いいえ'}}</p>
JS
PetiteVue.createApp({
  hasTime: false // 最初にチェックを入れておかない場合
}).mount();

上記の例は最初にチェックを入れておかない場合です。最初からチェックを入れておきたいときはデータプロパティ hasTime の初期値を true にします。

ドロップダウンメニュー

ドロップダウンメニューの場合は次のようになります。なお、multiple 属性ありのケースは割愛します(チェックボックスと同じく配列になります)。

HTML
<select v-model="box">
  <option selected disabled value="">手土産は?</option>
  <option value="大判">玉手箱(大)</option>
  <option value="小判">玉手箱(中)</option>
  <option value="老化">玉手箱(小)</option>
</select>

<p>{{box}}</p>
JS
PetiteVue.createApp({
  box: ''
}).mount();

v-model は select 要素に追加します(option 要素ではありません)。ラジオボタンと同じく、受け取る入力値は単体の文字列(value 属性値)なので、データプロパティの型は「文字列」になります。

プレースホルダ用の option 要素では、value 属性で明示的に空文字列 " " を指定してください。データプロパティ box の初期値も同じく空文字 ' ' にしておきます。空文字以外の初期値を設定したければそれでも構わないのですが、とにかくプレースホルダの value 属性値とデータプロパティの初期値と一致させておく必要があります。

試しに、プレースホルダ用 option 要素の value 属性を省略してみてください。selected 属性を付けているのにも関わらず、「手土産は?」という選択肢がドロップダウンメニューに表示されなくなります(何も表示されません)。

HTML
<option selected disabled>手土産は?</option>
<!-- 最初の選択肢に何も表示されなくなる -->

これはラジオボタンのところでも触れたように、petite-vue を使用すると、データプロパティの値が selected 属性よりも優先されるためです。select 要素は(selected 属性の有無は無視して)データプロパティの値と一致する value 属性値を持った option 要素を表示しようとしますが、そんなものは存在しないので「何も表示されない」状態になります。