一覧WB-07-1 · フォームと入力

HTMLフォームの入力と送信

参加申込みフォームを作り、FormDataで送信項目を取得します。チェックボックス・ラジオボタン・複数選択、readonlyとdisabled、送信ボタンを順に変更し、同じ名前が複数回現れるデータとリセット後の状態まで確認します。docはフォームを持つ文書です。

説明

フォームの入力欄には、表示や操作のためのid・ラベルと、送信データの名前になるnameがあります。画面に見えている値がすべて送信されるわけではなく、チェック状態、無効化、押した送信ボタンなどによって含まれる項目が決まります。

参加申込みフォームを作り、FormDataで送信項目を取得します。チェックボックス・ラジオボタン・複数選択、readonlyとdisabled、送信ボタンを順に変更し、同じ名前が複数回現れるデータとリセット後の状態まで確認します。docはフォームを持つ文書です。

1ラベルとnameを持つ入力欄

氏名にはnameを付け、呼び名はidだけを持つ状態で作ります。

createFormDocumentはHTMLを別の文書としてブラウザ内に開く関数です。docはその文書、formは中のフォーム要素を指します。

(globalThis.doc=createFormDocument('<!doctype html><html lang="ja"><body><h1>参加申込み</h1><form id="enroll"><p><label for="student">氏名</label><input id="student" name="student" value="あおい"></p><p><label for="nickname">呼び名</label><input id="nickname" value="Aoi"></p></form></body></html>'),globalThis.form=doc.getElementById("enroll")対象フォーム,{labelTarget:doc.querySelector("label").control.idラベルが対応する入力欄,entries:[...new FormData(form)]送信項目の名前と値})

ラベルはforとidによって氏名欄に対応します。一方、FormDataに入るのはname=studentの値だけです。idは文書内の識別子であり、送信時の項目名を自動的に代わりにするものではありません。

2nameを付けて送信対象にする

呼び名の入力欄にname=aliasを付けます。

(doc.getElementById("nickname").name="alias"送信時の項目名を設定,[...new FormData(form)変更後の項目を取得])

alias=Aoiが追加されます。見た目の値は変わっていませんが、送信データに含まれる項目が増えました。nameとvalueの組合せがフォームデータの基本になります。

3チェックされた項目だけを含める

同じname=topicで二つのチェックボックスを追加します。

(form.insertAdjacentHTML("beforeend",'<fieldset id="topics"><legend>参加内容</legend><label><input type="checkbox"複数項目から選ぶ入力 name="topic" value="html" checked初期状態で選択>HTML</label><label><input type="checkbox" name="topic" value="css">CSS</label></fieldset>'),new FormData(form).getAll("topic")同名項目をすべて取得)

チェック済みのhtmlだけが含まれます。チェックされていないcssについて、falseという値が自動送信されるわけではありません。value属性を省略したチェックボックスでは、チェック時の値は通常onになります。

4複数チェックで同名の値を送る

CSSにもチェックを付けます。

(doc.querySelector('input[value="css"]').checked=trueCSSも選択,{first:new FormData(form).get("topic")最初の一値,all:new FormData(form).getAll("topic")すべての値})

getは最初のhtmlだけ、getAllはhtmlとcssを返します。同名項目が複数回現れるフォームデータでは、一つの文字列だけを取り出すと選択内容を失います。

5ラジオボタンは同じグループから一つ

同じname=timeで午前と午後を作り、午後を選びます。

(form.insertAdjacentHTML("beforeend",'<fieldset id="time"><legend>時間帯</legend><label><input type="radio" name="time"同じラジオグループ value="am" checked>午前</label><label><input type="radio" name="time" value="pm">午後</label></fieldset>'),doc.querySelector('input[value="pm"]').checked=true午後を選択,{states:[...doc.querySelectorAll('input[name="time"]')].map(e=>[e.value,e.checked]),sent:送信される値new FormData(form).getAll("time")})

午後を選ぶと午前のチェックが外れ、pmだけが送られます。同じフォームに属し、同じnameを持つラジオボタンがグループになります。異なるnameにすると、同じグループとしての排他選択にはなりません。

6複数選択のselect

multiple付きのselectへ二つの選択肢を用意し、両方選びます。

(form.insertAdjacentHTML("beforeend",'<p id="courses"><label>分野 <select name="course" multiple size="2"複数選択の表示><option value="web" selected>Web</option><option value="network">ネットワーク</option></select></label></p>'),doc.querySelector('option[value="network"]').selected=trueネットワークも選択,new FormData(form).getAll("course")選択済みの値)

course=webとcourse=networkの二値になります。送信されるのは表示文字の「ネットワーク」ではなく、optionに指定したvalueのnetworkです。

7readonlyとdisabled

読取り専用の受付番号と、無効化した内部メモを追加します。

(form.insertAdjacentHTML("beforeend",'<p id="account"><label>受付番号 <input name="ticket" value="T-001" readonly読取り専用></label><label>内部メモ <input name="internal" value="staff" disabled無効化された入力></label></p>'),{ticket:new FormData(form).get("ticket"),internal:new FormData(form).get("internal")})

readonlyのticketはT-001として送信対象に入り、disabledのinternalはnullです。readonlyは対応する入力欄の編集を制限しますが、disabledはフォームデータの構成からも除外します。

8fieldset単位で無効化

参加内容のfieldsetをdisabledにします。

(doc.getElementById("topics").disabled=trueグループを無効化,{checked:選択状態は保持[...doc.querySelectorAll('#topics input')].map(e=>e.checked),sent:送信データからは除外new FormData(form).getAll("topic")})

チェック状態自体は二つともtrueですが、送信されるtopicは空になります。fieldsetの無効化が子のフォームコントロールへ及ぶためです。最初のlegend内にあるコントロールなど、仕様上の例外はこのフォームには置いていません。

9hiddenも名前と値を送る

画面に表示しない種別をhiddenで追加します。

(form.insertAdjacentHTML("beforeend",'<input type="hidden" name="kind" value="workshop"非表示の送信項目>'),new FormData(form).get("kind")送信する値)

kind=workshopが入ります。hiddenは画面に入力欄を表示しないだけで、DOMにも送信データにも値は存在します。利用者が変更できない秘密の値として扱うものではありません。

10押した送信ボタンの値

申込と下書きの二つの送信ボタンを作り、submitterを指定したFormDataと比較します。

(form.insertAdjacentHTML("beforeend",'<p id="actions"><button type="submit" name="action" value="enroll">申込</button><button type="submit" name="action" value="draft">下書き</button></p>'),{without:new FormData(form)送信ボタンの指定なし.get("action"),draft:new FormData(form,doc.querySelector下書きボタンをsubmitterに指定('button[value="draft"]')).get("action")})

送信ボタンを指定しない場合、actionはnullです。下書きボタンをsubmitterにするとdraftになります。同じフォームでも、どのボタンから送信したかが送信データに影響します。

11オブジェクト化で重複名が失われる

courseの二値を持つFormDataを、Object.fromEntriesで一つのオブジェクトへ変換します。

({all:new FormData(form).getAll("course")二つの値を保持,objectValue:Object.fromEntries同じキーの最後の値だけが残る変換(new FormData(form)).course})

getAllではwebとnetworkですが、オブジェクトには最後のnetworkだけが残ります。オブジェクトの同じキーに二値をそのまま保持できないためです。複数値が必要なら配列として集めるなど、変換方法を選びます。

12リセット後の値と無効化

氏名を書き換え、form.resetで初期値へ戻します。

(doc.getElementById("student").value="変更後",form.reset()値と選択を既定値へ戻す,{student:doc.getElementById("student").value,time:new FormData(form).get("time"),courses:new FormData(form).getAll("course"),topicsDisabled:無効化は残るdoc.getElementById("topics").disabled})

氏名はあおい、時間帯はam、分野はwebへ戻ります。resetは入力値や選択を既定値へ戻す操作で、disabled属性を自動的に取り消すものではありません。後から追加した要素やname属性をすべて元に戻す履歴機能でもありません。

まとめ

送信する項目はname・値・選択状態・disabledなどから決まります。表示やidだけでは判断できません。

同名の複数値はgetAllなどで保持し、単一オブジェクトへの変換で落とさないようにします。

readonly、hidden、submitter、resetは異なる役割を持ち、フォームの現在値と送信内容を分けて確認します。

操作履歴と解説を振り返れます。

ブラウザ / HTML・DOM

1
(globalThis.doc=createFormDocument('<!doctype html><html lang="ja"><body><h1>参加申込み</h1><form id="enroll"><p><label for="student">氏名</label><input id="student" name="student" value="あおい"></p><p><label for="nickname">呼び名</label><input id="nickname" value="Aoi"></p></form></body></html>'),globalThis.form=doc.getElementById("enroll"),{labelTarget:doc.querySelector("label").control.id,entries:[...new FormData(form)]})
{
  "labelTarget": "student",
  "entries": [
    [
      "student",
      "あおい"
    ]
  ]
}
2
(doc.getElementById("nickname").name="alias",[...new FormData(form)])
[
  [
    "student",
    "あおい"
  ],
  [
    "alias",
    "Aoi"
  ]
]
3
(form.insertAdjacentHTML("beforeend",'<fieldset id="topics"><legend>参加内容</legend><label><input type="checkbox" name="topic" value="html" checked>HTML</label><label><input type="checkbox" name="topic" value="css">CSS</label></fieldset>'),new FormData(form).getAll("topic"))
[
  "html"
]
4
(doc.querySelector('input[value="css"]').checked=true,{first:new FormData(form).get("topic"),all:new FormData(form).getAll("topic")})
{
  "first": "html",
  "all": [
    "html",
    "css"
  ]
}
5
(form.insertAdjacentHTML("beforeend",'<fieldset id="time"><legend>時間帯</legend><label><input type="radio" name="time" value="am" checked>午前</label><label><input type="radio" name="time" value="pm">午後</label></fieldset>'),doc.querySelector('input[value="pm"]').checked=true,{states:[...doc.querySelectorAll('input[name="time"]')].map(e=>[e.value,e.checked]),sent:new FormData(form).getAll("time")})
{
  "states": [
    [
      "am",
      false
    ],
    [
      "pm",
      true
    ]
  ],
  "sent": [
    "pm"
  ]
}
6
(form.insertAdjacentHTML("beforeend",'<p id="courses"><label>分野 <select name="course" multiple size="2"><option value="web" selected>Web</option><option value="network">ネットワーク</option></select></label></p>'),doc.querySelector('option[value="network"]').selected=true,new FormData(form).getAll("course"))
[
  "web",
  "network"
]
7
(form.insertAdjacentHTML("beforeend",'<p id="account"><label>受付番号 <input name="ticket" value="T-001" readonly></label><label>内部メモ <input name="internal" value="staff" disabled></label></p>'),{ticket:new FormData(form).get("ticket"),internal:new FormData(form).get("internal")})
{
  "ticket": "T-001",
  "internal": null
}
8
(doc.getElementById("topics").disabled=true,{checked:[...doc.querySelectorAll('#topics input')].map(e=>e.checked),sent:new FormData(form).getAll("topic")})
{
  "checked": [
    true,
    true
  ],
  "sent": []
}
9
(form.insertAdjacentHTML("beforeend",'<input type="hidden" name="kind" value="workshop">'),new FormData(form).get("kind"))
"workshop"
10
(form.insertAdjacentHTML("beforeend",'<p id="actions"><button type="submit" name="action" value="enroll">申込</button><button type="submit" name="action" value="draft">下書き</button></p>'),{without:new FormData(form).get("action"),draft:new FormData(form,doc.querySelector('button[value="draft"]')).get("action")})
{
  "without": null,
  "draft": "draft"
}
11
({all:new FormData(form).getAll("course"),objectValue:Object.fromEntries(new FormData(form)).course})
{
  "all": [
    "web",
    "network"
  ],
  "objectValue": "network"
}
12
(doc.getElementById("student").value="変更後",form.reset(),{student:doc.getElementById("student").value,time:new FormData(form).get("time"),courses:new FormData(form).getAll("course"),topicsDisabled:doc.getElementById("topics").disabled})
{
  "student": "あおい",
  "time": "am",
  "courses": [
    "web"
  ],
  "topicsDisabled": true
}

演習完了