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

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

## 序説

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

- [【JavaScript】「formをjsonにしてpost」する。](https://qiita.com/hnkyi/items/3f2faf2848cc2de0cf8c)
- [フォームの内容をJSON形式で取得「JSON Form」](https://webkaru.net/jquery-plugin/json-form/)
- [postするデータをjson化してpostすればcollectionなどにも対応出来そう。](https://pod.hatenablog.com/entry/2015/03/28/111337)

皆さん苦労されているようですね。
試行錯誤の結果、私は素直に[jsForm](https://github.com/corinis/jsForm)を採用することとしました。

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

## サンプルコード(HTML)

まずは私が最終的な実証のために使った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`要素の部分に結果を表示します。

[^1]: [jsFormのQuickstart](https://github.com/corinis/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以降を強引に指定しても正しく動作しません。

## サンプルコード(JavaScript)

続いて、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`要素の値を利用します。
    - `contentType`を`application/json`にしているため、ローカルで実行しようとすると、多くのブラウザでは[CORS](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)ポリシーに反するということでJavaScriptの動作が停止します。これを避けるにはサーバーにCORSの対応を行うか、ブラウザのセキュリティポリシーを変更する必要があります。詳しい解説は割愛します。
    - サーバーと通信するのではなく、単にJSONの文字列を見たいだけの場合は`②送信`～`②-b 失敗`の部分をコメントアウトして、その後に`$('#display').text(text);`と入れればいいでしょう。

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

### 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](https://github.com/corinis/jsForm/wiki/JsForm-Documentation)を参照してください。
- `$('form').submit(jsonSend);`
    - フォームのsubmitボタンにjsonSend関数を設定します。

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

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

### 全体初期化

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

## 動作

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

|![起動直後のsample.html](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612816/e2c4eeb1-9984-6fa7-3d14-8baee486b66c.png)
|:-:|

値を入力します。

|![値を入力したsample.html](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612816/a0a0f361-a0fe-5964-cc3a-1cfc610aedf0.png)
|:-:|

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

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

|![送信ボタンを押した後の結果表示の部分](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612816/822d6f3f-db99-2e8e-f90f-4001b4ab6bce.png)
|:-:|

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

|![リクエストヘッダとレスポンスヘッダ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612816/0cfa1d78-d2cb-b827-0e1c-8d806a6dbb1e.png)
|:-:|

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

|![リクエストボディ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612816/46984fb5-f57e-e95f-3bb3-bc9338e25729.png)
|:-:|

## 配列の増減

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

```HTML: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-field`は`div[class="collection"]`のそれと同じものを指定します。

### 動作

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

|![修正したsample.htmlの起動後](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612816/93626fe5-70db-bc49-2bb1-edb31534cadf.png)
|:-:|

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

|![修正したsample.htmlに値を入力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612816/5c8597ee-bc5e-0336-e2df-a4c957d4bdb5.png)
|:-:|

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

|![修正したsample.htmlの結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612816/f44d1b3a-a6b1-1ae6-4d55-505ec8cfd2fb.png)
|:-:|

## 制限

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

### 配列に関する制限

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

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

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

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

### 書式に関する制限

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

```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として以下のような形式で欲しい人は多いでしょう。私もその一人です。

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

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

|![望まぬ結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612816/c0c77bb8-8427-ce4e-3e31-f39a8d1d7f04.png)
|:-:|

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


## まとめ

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

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