0
4

More than 3 years have passed since last update.

HTMLフォームをJSONフォーマットに変換するjQueryライブラリであるjsFormの使い方

Last updated at Posted at 2020-04-13

HTMLフォームからJSONフォーマットに変換

序説

HTMLのフォーム関連要素(form, input, textarea, etc.)の値をJSON形式に変換したい(そしてそれをajax等で送信したい)という需要はそれなりにあるようです。私もその必要があったのでそれを探したり試したりしました。私がググった結果では以下のようなものが見つかっています。

皆さん苦労されているようですね。
試行錯誤の結果、私は素直にjsFormを採用することとしました。

ところが、このjsFormですが一癖も二癖もあって実用に持っていくまでにかなりの苦戦を強いられたのでした。そこで、その悪戦苦闘の結果をここにまとめ、皆様のお役に立てるようにするものであります。

サンプルコード(HTML)

まずは私が最終的な実証のために使ったHTMLを示します。

sample.html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>FORM to JSON</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="./js/jquery.jsForm.js"></script>
    <script src="./js/sample.js"></script>
    <link rel='stylesheet' href="./style/sample.css"></script>
  </head>

  <body>
    <h1>JSON送信</h1>
    <form 
      action="https://api.example.com/v1/objects" 
      method="post"
      id="first_test" name="test" 
    >
      <label for="data.foo.first"   >foo.first:    </label><input type="text"   name="data.foo.first">   <br>
      <label for="data.foo.second.a">foo.second.a: </label><input type="text"   name="data.foo.second.a"><br>
      <label for="data.foo.second.b">foo.second.b: </label><input type="text"   name="data.foo.second.b"><br>
      <label for="data.foo.third"   >foo.third:    </label><input type="text"   name="data.foo.third">   <br>
      <fieldset class='array'>
        <legend>配列</legend>
        <div class="collection" data-field="data.hoge">
          <div class="item">
            <label for="hoge.first"   >hoge.first:   </label><input type="text"   name="hoge.first">       <br>
            <label for="hoge.second"  >hoge.second:  </label><input type="text"   name="hoge.second">      <br>
          </div>
        </div>
      </fieldset>
      <label for="data.bar.first"   >bar.first:    </label><input type="text"   name="data.bar.first">   <br>
      <label for="data.bar.second"  >bar.second:   </label><input type="text"   name="data.bar.second">  <br>
      <input type="submit" name="submit" value="送信">
    </form>

    <h2>結果表示</h2>
      <pre id="display">
NO CONTENTS
      </pre>
  </body>
</html>

サンプルコード(HTML)の解説

詳しく見ていきます。

  • head要素のscript要素1つ目はjQueryを参照しています。jsFormはjQueryライブラリですのでこれは必須になります。
  • script要素2つ目はjsFormを参照しています。これはjsFormのページからダウンロードしてくる必要があります1
  • script要素3つ目はこのHTMLに適用させるスクリプトです。別途解説します。
  • link要素は、私が別途作ったフォームをきれいに見せるためにCSSです。その内容はここで解説はしません。不気味に思うのであれば消しましょう。
  • body要素に入ってform要素は、JSONによる送信を行わない場合においては一般的な方法(application/x-www-form-urlencoded)でリソースにアクセスしに行くものとします。実際にサーバーにデータを送信する場合はaction属性の値を変更してください。
  • 各input要素ですが、name属性の値が重要になります。プレフィックス.値1[.値2][.値3]...となるようにします。プレフィックスは省略不可能です(デフォルトはdata)。プレフィックスは変更可能です。具体例をあげます。
    • data.foo
    • data.bar.first
    • data.bar.second
    • data.baz.a.b.c.d.e
  • label要素は必要に応じて記述します。
  • クラス指定がclass='collection'かつdata-field属性があるブロック要素(sample.htmlではdiv要素)は、値を配列で取得するときに利用します。data-field属性の属性値の形式はプレフィックス.値1です。この値は1つでなくてはいけません2。具体例は以下のとおりです。
    • data.hoge
  • div[class='collection']要素の中に子要素を設定します(ブロック要素であることが望ましい)。その子要素の中に配列とするinput要素を1つ以上書きます。
    • 子要素の内容であるinput要素のname属性は値1.値2[.値3][.値4]...となります。値1はdata-field属性のものです。プレフィックスが必要ないことに注意してください。具体例は以下のようになります。
      • <div class="collection" data-field="data.hoge">である場合
        • hoge.baz
        • hoge.qux.quux
        • hoge.qux.foobar
        • hoge.a.b.c.d.e.f
    • 匿名ブロックがあるとその部分が正しく表示されません。
  • fieldset要素はそれがないと配列部分がわかりにくいために設定しています。動作そのものに影響はありません。
  • pre要素の部分に結果を表示します。

サンプルコード(JavaScript)

続いて、JavaScriptについてコードを示します。

./js/sample.js
/**
 * formをjsonに変換する
 */
var jsonSend = function(){

  // ①json形式取得
  var text = JSON.stringify($(this).jsForm("get"), undefined, 2);

  // ②送信
  $.ajax({
    'url':           this.action,
    'type':          'post',
    'dataType':      'json',
    'contentType':   'application/json',
    'data':          text
  })  

  // ②-a 成功 - 結果出力
  .done(function(data, textStatus, xhr){
    $('#display').text(JSON.stringify(data, undefined, 2));
  })

  // ②-b 失敗 - エラー出力
  .fail(function(xhr, textStatus, errorThrown){
    var text = JSON.stringify(
      {
        "XMLHttpRequest": xhr,
        "textStatus": textStatus,
        "errorThrown": errorThrown
      }, 
      undefined, 2
    );
    $('#display').text(text);
  });

  // ③ キャンセル
  // 通常フォーム(application/x-www-form-urlencoded)の送信を抑止
  return false;

};

/**
 * フォームの初期化
 */
var formInit = function(){
  var data = {
    "foo": {
      "first": "foo.first",
      "second": {
        "a": "foo.second.a",
        "b": "foo.second.b"
      },
      "third": "foo.third"
    },
    "bar": {
      "first": "bar.first",
      "second": "bar.second"
    },
    "hoge": [
      {
        "first": "hoge[0].first",
        "second": "hoge[0].second"
      },
      {
        "first": "hoge[1].first",
        "second": "hoge[1].second"
      }
    ]
  };
  $('form').jsForm({"data": data});
  $('form').submit(jsonSend);
};

/*
 * 全体初期化
 */
$(function(){
  formInit();
});

サンプルコード(JavaScript)の解説

jsonSend関数

submitボタンが押されたときの処理が書かれています。

  • ①json形式取得
    • ($(this).jsForm("get")3がjQueryの拡張された部分で、これでフォームデータがJSON形式(の前駆となる辞書型配列)として取得できます。
    • これを行うにはフォームに対してjsFormの設定を事前にしておく必要があります。それに関しては後述します。
    • 取得した辞書型配列をJSON.stringifyで処理すればフォームのJSON化がたちまちのうちに完成です。
  • ②送信
  • ②-a 成功 - 結果出力
  • ②-b 失敗 - エラー出力
    • なんの変哲もないajax送信の処理です。
    • actionの値はHTMLのform要素の値を利用します。
    • contentTypeapplication/jsonにしているため、ローカルで実行しようとすると、多くのブラウザではCORSポリシーに反するということでJavaScriptの動作が停止します。これを避けるにはサーバーにCORSの対応を行うか、ブラウザのセキュリティポリシーを変更する必要があります。詳しい解説は割愛します。
    • サーバーと通信するのではなく、単にJSONの文字列を見たいだけの場合は②送信②-b 失敗の部分をコメントアウトして、その後に$('#display').text(text);と入れればいいでしょう。

formInit関数

フォームが読み込まれたときに行われるフォームの初期化処理が書かれています。

  • var data = ...
    • この変数のリテラルの構造は $(this).jsForm("get") で得られるものと全く同じです。
    • この内容を利用して、フォームの値を初期化することができます。これについては後述します。
    • 配列であるhogeを利用してフォームの初期化を行わないと、<div class="collection" data-field="data.hoge">の中身が全く表示されません(要素そのものがなかったことにされてしまいます)。
  • $('form').jsForm({"data": data});
    • フォームに対して事前に行っておくjsFormの設定です。
    • これを行わないと$(this).jsForm("get")を行っても正しく動作しません4
    • 引数指定でフォームの値の初期化を行うことができます({"data": {初期化データ})5
      • 存在しない<input name="*">を初期化すると、それは$(this).jsForm("get")の結果として取得されるので注意が必要です。
    • プレフィックスの変更もここでできます("prefix": "プレフィックス")
    • その他、引数の詳しい情報はjsFormのDocumentationを参照してください。
  • $('form').submit(jsonSend);
    • フォームのsubmitボタンにjsonSend関数を設定します。

全体初期化

ページの読み込み終了後にformInit関数を動作させます。

動作

では実際の画面を見てみましょう。sample.htmlを開きます。

起動直後のsample.html

値を入力します。

値を入力したsample.html

送信ボタンを押します6。サーバーリクエストまで求めていない場合は「②送信②-b 失敗の部分をコメントアウトして...」で対応します。フォームがJSONに変換されたことがわかります。

送信ボタンを押した後の結果表示の部分

実際のHTTPリクエストを見てみます。リクエストヘッダ(=要求ヘッダ)にはcontent-type: application/jsonがあるのがわかります。

リクエストヘッダとレスポンスヘッダ

リクエストボディ(=要求ペイロード)はJSONの文字列になっていることがわかります。

リクエストボディ

配列の増減

サンプルでは配列hogeの数は2つとなっていますが、コントロールを追加することでユーザーがそれを増減できるようになります。

sample.html(修正部分のみ)
      <fieldset class='array'>
        <legend>配列</legend>
        <div class="collection" data-field="data.hoge">
          <div class="item">
            <label for="hoge.first"   >hoge.first:   </label><input type="text"   name="hoge.first">       <br>
            <label for="hoge.second"  >hoge.second:  </label><input type="text"   name="hoge.second">       <br>
            <button class="delete">削除</button>
          </div>
        </div>
      </fieldset>
      <button class="add" data-field="data.hoge">追加</button><br/>

注意点

  • ボタンにクラス(追加ボタンにはadd, 削除ボタンにはdelete)が設定されていなければなりません。
  • 削除ボタンは配列のinput要素と同じ子要素の中になければいけません。
  • 追加ボタンのdata-fielddiv[class="collection"]のそれと同じものを指定します。

動作

修正したsample.htmlを動作させます。

修正したsample.htmlの起動後

配列を2つ増やして値を入れてみます。

修正したsample.htmlに値を入力

増やした配列の値が正しく反映された結果が表示されます。

修正したsample.htmlの結果

制限

なかなかいい筋をしているjsFormですが、すべての人の理想を叶えてくれるというわけではなさそうです。

配列に関する制限

配列はレベル1の階層でのみ実現できます。すなわち、以下のようなデータ構造を作成することはjsFormでは不可能です。

bad_datastructure.json
{
  "hoge": {
    "foo": ["1", "2", "3"],
    "bar": ["巨人", "大鵬", "卵焼き"],
    "baz": [{ "A": "a" }, { "B": "b" }, { "C": "c" }]
  }
}

以下のような構造であれば実現可能です。

valid_datastructure.json
{
  "hoge": [
    {
      "foo": "1",
      "bar": "巨人",
      "baz": { "A": "a" }
    },
    {
      "foo": "2",
      "bar": "大鵬",
      "baz": { "B": "b" }
    },
    {
      "foo": "3",
      "bar": "卵焼き",
      "baz": { "C": "c" }
    }
  ]
}

書式に関する制限

フォームの中で配列や辞書型を使いたいという考えの方もいるかもしれません。例えば以下のようなHTMLがあったとします。

hope.html
    <form 
      action="https://gzigruksti.execute-api.ap-northeast-1.amazonaws.com/prac1/test/requestecho" 
      method="post"
      id="first_test" name="test" 
    >
      <label for="data.foo.first"    >foo.first:    </label><input type="text"   name="data.foo.first">    <br>
      <label for="data.foo.second[a]">foo.second[a]:</label><input type="text"   name="data.foo.second[a]"><br>
      <label for="data.foo.second[b]">foo.second[b]:</label><input type="text"   name="data.foo.second[b]"><br>
      <label for="data.foo[third]"   >foo[third]:   </label><input type="text"   name="data.foo[third]">   <br>
      <fieldset class='array1'>
        <legend>配列1</legend>
        <label for="data.hoge[]"   >hoge[]:         </label><input type="text"   name="data.hoge[]">       <br>
        <label for="data.hoge[]"   >hoge[]:         </label><input type="text"   name="data.hoge[]">       <br>
      </fieldset>
      <fieldset class='array2'>
        <legend>配列2</legend>
        <label for="data.bar[0]"   >bar[0]:         </label><input type="text"   name="data.bar[0]">       <br>
        <label for="data.bar[2]"   >bar[2]:         </label><input type="text"   name="data.bar[2]">       <br>
      </fieldset>
      <input type="submit" name="submit" value="送信">
    </form>

このHTMLに対応するJSONとして以下のような形式で欲しい人は多いでしょう。私もその一人です。

hope.js
{
  "foo": {
    "first": "あいうえお",
    "second": {
      "a": "かきくけこ",
      "b": "さしすせそ"
    },
    "third": "たちつてと"
  },
  "hoge": ["なにぬねの", "はひふへほ"],
  "bar": ["ばびぶべぼ", null, "ぱぴぷぺぽ"]
}

残念ながらこれはうまく行きません。実際にやってみると以下のような結果になります。配列は展開されず、ドット区切り記法のみが有効というのがわかります。また、同一の属性名は最後のもののみが有効です。

望まぬ結果
disappointment.js
{
  "foo": {
    "first": "あいうえお",
    "second[a]": "かきくけこ",
    "second[b]": "さしすせそ"
  },
  "foo[third]": "たちつてと",
  "hoge[]": "はひふへほ",
  "bar[0]": "ばびぶべぼ",
  "bar[2]": "ぱぴぷぺぽ"
}

まとめ

jsFormにおける配列や辞書の取扱いに不満がある人はいるでしょう。しかし、この内容であっても通用する範囲は相当に広いのではないでしょうか。

jsFormの利用方法の解説はこれまでもチラホラとありましたが、その詳細や注釈はあってなかったようなもので、私は実用的に利用ができるようになるまで相当に苦労しました。その苦労を他人が繰り返さぬよう、是非ともこの記事の内容をお役立てください。


  1. jsFormのQuickstartのソースコードを見る限りではWEB上にあるリソースを開放しているように見えますが(2020年4月10日現在)、実際にhttps://raw.github.com/corinis/jsForm/master/src/jquery.jsForm.jsにアクセスすると301 Moved Permanentlyが帰ってきます。そのLocationに示されたURLであるhttps://raw.githubusercontent.com/corinis/jsForm/master/src/jquery.jsForm.jsにアクセスするとそのレスポンスヘッダがContent-Type: text/plain かつ X-Content-Type-Options: nosniffであるため、ブラウザのセキュリティ機構がスクリプトの読み込みを停止してしまいます。 

  2. data.hoge.bazのように値2以降を強引に指定しても正しく動作しません。 

  3. HTMLの構造およびスクリプトの処理の流れを考慮すると、ここでは$(this)$('form')が等価になります。 

  4. $('form').jsForm()で設定、$('form').jsForm("get")で取得という流れです。 

  5. 値の初期化を行わないのであれば引数が一切ない$('form').jsForm()で構いません。今回の例では配列部分が存在するために値の初期化が事実上必須になってます。 

  6. https://api.example.com/v1/objectsはリクエストボディがそのままレスポンスボディとして返るものとします。 

0
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
4