高度なFormの実装(データバインディング)

前の章ではForm内の各input要素にJSからアクセスする方法をご紹介しました。

先程の例では "inputタグに入力された値をJSから取得する" という流れを説明しましたが、この逆の流れ、つまり "JSの値をinputタグに反映する" という事が出来ます。例を見てみましょう。

"Get random name" ボタンを押すと、入力欄に値が反映されます。これが "JSの値をinputタグに反映する" サンプルとなります。実際のコードを見ていきましょう。

<form id="form">
  <div class="form-example">
    <label for="name">Enter your name: </label>
    <input type="text" name="name" id="name" required>
    <button id="random">Get random name</button>
  </div>

  <br />

  <button type="submit">Submit</button>
</form>

<div id="result"></div>

HTMLには id="random" なボタンを追加しました。他の箇所はほとんど変わっていません。

// 各要素への参照を取得。
const $form = $("#form");
const $name = $("#name");
const $random = $('#random');

// 最新の値を保持する変数(モデル)
let model = {}

// 渡された値を元にモデルの名前を更新し、描画する関数。
const setName = (value) => {
  // モデルのnameを更新。
  model["name"] = value;
  // name欄の値(ユーザに見えてる要素の値)を最新のデータで更新する。
  $name.val(model["name"]);
}

// name欄の文字入力(input)イベントにイベントリスナー(ハンドラー)を登録
$name.on("input", (event) => {
  // nameの更新処理を呼び出す。
  setName(event.target.value);
})

// ボタンのクリック(click)イベントにイベントリスナー(ハンドラー)を登録
$random.on("click", (event) => {
  // デフォルトのフォーム送信の動きを止める(prevent)
  event.preventDefault()
  
  // 1~5のランダムな番号を取得する。
  const i = Math.floor(Math.random() * 5);
  
  // ランダムな番号に該当する要素を名前として利用する。
  setName([
    "John Do",
    "Jane Doe",
    "Taro Yamada",
    "Hanako Suzuki",
    "Saburo Tanaka"
  ][i])
})

// form要素の送信(submit)イベントにイベントリスナー(ハンドラー)を登録
$form.on("submit", (event) => {
  // 1. デフォルトのフォーム送信の動きを止める(prevent)
  event.preventDefault();

  // 2. モデルのnameの最新の値(= 入力値)を取得。
  const name = model["name"];

  // 3. AJAXを使ってデータ送信する。
  $.ajax({
    // 送信先のURL(HTMLのform要素のactionに相当する部分)
    url: "https://httpbin.org/anything",
    // 送信するデータ。デフォルトでは "GET" リクエストなので、クエリ文字列(パラメータ)として付与される。
    data: { name: name }
  }).then((response) => {
    // id="result"な要素のテキストを取得したAPIのレスポンスで置き換える。
    $("#result").text(JSON.stringify(response));
  });
});

JSの方もデータ送信の流れ自体は変わっていませんが、細かい部分を見ていきましょう。

まずは let model = {} にて、入力中のデータを保持するためのオブジェクト(以降モデル)を宣言しています。

// 渡された値を元にモデルの名前を更新し、描画する関数。
const setName = (value) => {
  // モデルのnameを更新。
  model["name"] = value;
  // name欄の値(ユーザに見えてる要素の値)を最新のデータで更新する。
  $name.val(model["name"]);
}

続いてこの setName 関数でモデルの値の更新と、画面への反映( $name.val(model["name"] = input要素のvalueの更新) を行っています。このようにモデルへのデータの保存処理と、UIの更新処理をまとめておくことで、モデル(Model)と画面(View)の表示がズレる(同期されなくなる)ことを防ぐことが出来ます。

このsetNameは以下の2箇所で呼び出されます。

  • $name.on("input", (event) => {...}) = これはinput要素自身にキーボードで文字が入力されるタイミングで発火されるイベントです。キーボード入力ごとにモデルのデータを更新しています。厳密にはsetName関数の内部でUIの更新を呼び出す = 重複した更新(元々input要素のUI更新は文字入力時に行われるので)になりますが、処理の流れをわかりやすくするためにこうしています。

  • $random.on("click", (event) => {...}) = これは "Get random name" ボタンを押した時の処理です。setName関数はinput要素にセットする値の文字列を引数として受け取るようになっているので、このように文字を渡すだけで画面に反映(= 再利用)できるようになっています。

最後の $form.on("submit", (event) => {...} の箇所では以前のように $name.val() としてinput要素の値を取得せずに、 model["name"] としてモデル(JSのオブジェクト変数)の値を取得しています。こうすることで前者のDOMアクセス有りの場合($name.val())と比べてパフォーマンスが良いコード、かつデータの流れが整理されたコードになります。今回のような単一のinput要素の場合ではあまり恩恵を感じられないかもしれませんが、多くの入力欄があるページ(例: 注文・登録フォーム)では差が出てくるかと思います。

このような モデル(Model, データ = JSのオブジェクト)とビュー(UI, HTMLの要素) を分けて考える開発手法を MVVM(エムブイブイエム) と呼び、データ(モデル)とUI(ビュー/画面)を同期する仕組みを データバインディング(Data Binding/データの紐付け) と呼びます。ます。これはWebアプリケーションサーバの開発手法の一つである MVC(エムブイシー) と似た考え方です。MVVMを採用しているフレームワークの例としては Angular / Vue.js といったものが挙げられます。

Angular / Vue.js といったフレームワーク使うだけであれば、 MVVM の概念知らなくてもどうにかなるのですが、仕組みを詳しく理解しておきたい。という方はコラムで解説しているので、見てみてください。

次の章では今までの知識を活かして、簡単なカウンター(Counter/数字のカウント)を行うアプリを作っていこうと思います。

最終更新