created_at
updated_at
tags
toc

Form で submit されたデータの収集と FormData & URLSearchParams

Intro

<form> の onsubmit をフックして、入力された値を <input> から集めて送るといった処理はよくある。

このとき、submit されたデータの収拾方法はいくつかある。

submit に限らず、そのイベントに付随する情報は、基本的にイベントオブジェクトに内包されている。

Form を例に、イベントオブジェクトを意識したコーディングについて解説する。

Form Submit

Form が Submit されたことをフックして、処理を挟む場面はよくある。

HTML がこうであった場合。

<form id=login action=/login method=post>
  <fieldset form=login>
    <legend>Login</legend>
    <label for=username>Username</label>
    <input type=text name=username id=username value=jxck>
    <label for=password>Password</label>
    <input type=password name=password id=password value=thisismypassword>
    <button type=submit>login</button>
  </fieldset>
</form>

JS は以下のように書かれる場合がある。

document.querySelector('#login').onsubmit = (e) => {
  e.preventDefault()
  const username = document.querySelector('#username')
  const password = document.querySelector('#password')
  process_input({username, password})
}

ここでは、取得するデータは 2 つしかないが、大きなフォームでは多数の <input> を探索する必要がある。

この例を改善しつつ解説していく。

e.target

最も簡単な改善は、document からのクエリをやめることだ。

e.target には、対象の DOM 、ここでは <form> が入っている。

<input> はその子要素なので、わざわざ document を起点にする必要はない。

document.querySelector('#login').onsubmit = (e) => {
  e.preventDefault()
  const username = e.target.querySelector('#username')
  const password = e.target.querySelector('#password')
  process_input({username, password})
}

FormData

Form で Submit されたデータは、FormData を経由して取得することができる。

つまり、FormData に変換しさえすれば、submit 対象のデータは全て手に入っている。

このオブジェクトは、get()set() など Map のようなインタフェースを持つ。

(なお new Map(form_data) すれば、実際の Map にもなる)

また、そのまま XHR や fetch を使ってそのまま POST することができる。

document.querySelector('#login').onsubmit = (e) => {
  e.preventDefault()
  const form_data = new FormData(e.target)
  validate_username(form_data.get('username'))
  validate_password(form_data.get('password'))
  fetch('/login', {
    method: 'POST',
    body: form_data
  })
}

ただし、注意点としてこのとき POST される Content-Type は multipart/form-data になる。

つまり Body は以下のようなフォーマットだ。

// content-type:multipart/form-data; boundary=----WebKitFormBoundaryPfqUKvtarA1EFkbV


------WebKitFormBoundaryPfqUKvtarA1EFkbV
Content-Disposition: form-data; name="username"

jxck
------WebKitFormBoundaryPfqUKvtarA1EFkbV
Content-Disposition: form-data; name="password"

examplepassword
------WebKitFormBoundaryPfqUKvtarA1EFkbV--

大抵のサーバは、これでも問題なく処理できるだろう。

しかし、File でもない限り HTML Form からは application/form-url-encoded で送られてくるという前提で実装されたものもあるだろう。

URLSearchParams

URLSearchParams は、URL の標準化の際に QueryString 部分をサポートするために導入された。

しかし、これは FormData を引数にインスタンスを生成することができる。

また、そのまま POST の Body にすれば、application/form-url-encoded として送ることができる。

document.querySelector('#login').onsubmit = (e) => {
  e.preventDefault()
  const form_data = new FormData(e.target)
  const url_search_params = new URLSearchParams(form_data)
  fetch('/login', {
    method: 'POST',
    body: url_search_params
  })
}

つまり Body は以下のようなフォーマットだ。

// content-type:application/x-www-form-urlencoded;charset=UTF-8

username=jxck&password=thisismypassword

JSON

API バックエンドなどに対して JSON で送りたい場合もあるだろう。

せっかく FormData までは取得できているので、これを Object に変換してからシリアライズすれば良い。

ここでは FormData が iterable であるこを利用してオブジェクトを組み立ててみる。

document.querySelector('#login').onsubmit = (e) => {
  e.preventDefault()
  const data = Object.fromEntries(new FormData(e.target).entries())
  const json = JSON.stringify(data)
  fetch('/login', {
    method: 'POST',
    body: json
  })
}

(ただし Form に <select> などが入る場合は修正が必要 https://labs.jxck.io/form/input-type/)

beforeSubmitCallback

submit に callback を仕込む仕様の提案がかなり前に出ている。

Need callback for form submit data

document.registerElement('input', {
  prototype: {
    proto: HTMLElement.prototype,
      beforeSubmitCallback: function() {
        switch (this.type) {
        case 'checkbox':
          if (this.checked) {
            return this.value;
          }
          return undefined;
        }
      }
  }
});

進捗は微妙だが、もし実装されると、JSON で Post したい場合に、Fetch を使わずにフォーマットの変換だけでよくなるのかもしれない。

DEMO

動作するデモを以下に用意した。