順番に読み進めながら学べます

フォーム — 入力値と submit

入力欄の値を読む value と checked、入力で起きる input と change イベント、送信を取りやめる submit と preventDefault、欄をまとめて集める FormData を扱います。

クリックは addEventListener で受け取れますが、フォームでは入力欄に打ち込まれた値を読み、送信前に内容を確かめます。送信ボタンを押すとブラウザがページを切り替えるため、確かめ終わる前に送られないようにする書き方も必要です。

この記事では、入力欄の値を読む value と、フォームの送信を受け取る submit イベントを扱います。

入力された値を読み取る — value と checked

注文フォーム(入力欄や送信ボタンを form 要素でまとめた部分)で、数量の欄に入力された数を読み、合計金額を計算したいとします。input 要素は終了タグを書かない要素で、中に子の要素も文字も持てないため、textContent を読んでも空の文字列しか返りません。

入力欄(type 属性で種類を決める input 要素や、複数行の textarea 要素)の value(欄に今入っている値を文字列で読み書きするプロパティ)は quantity.value のように読み、チェックの有無は checked で読みます。HTML に書く value 属性はページを開いたときの初期値で、入力しても書き換わりません。

document.body.innerHTML = `<form id="order-form"><input id="quantity" type="number" value="1"><input id="gift-wrap" type="checkbox"></form>`;
const quantity = document.querySelector("#quantity");
const giftWrap = document.querySelector("#gift-wrap");

// 利用者が 3 と入力し、ギフト包装にチェックを入れたことにする
quantity.value = "3";
giftWrap.checked = true;

// 数値の欄でも value は文字列なので、数に直してから計算する
console.log(quantity.value + 1);              // 31
console.log(Number(quantity.value) * 1200);   // 3600
console.log(giftWrap.checked);                // true

// outerHTML に出るのは、HTML に書いた value 属性のまま
console.log(quantity.outerHTML);   // <input id="quantity" type="number" value="1">
quantity の input が持つ 2 つの value
value 属性HTML に書いたvalue="1"入力しても1 のままouterHTML にvalue="1" が出るvalue プロパティ今の入力値"3"入力のたびに変わるquantity.valueで読む
代入した "3" は value プロパティにだけ入り、value 属性は 1 のままです。入力された値は、value プロパティから読みます

checked も同じで、チェックを入れても HTML に checked 属性は付きません。数量の欄を空にすると value は空の文字列になり、Number("") は 0 を返すため、数に直した後では未入力と 0 の入力を見分けられません。

レンタカーの予約で、日数とチャイルドシートの有無から料金を計算します。値を代入した日数の欄 rentalDays とチェックボックス childSeat は宣言済みです。

① 日数の欄の値の型を表示してください。

② 日数を数に直し、1 日 6000 円の料金を表示してください。

③ チェックがあれば 1 日あたり 500 円を足し、日数分の料金を表示してください。

(正しく実行できれば解説が表示されます)

JavaScript / TypeScript エディタ

コードを実行してください

入力のたびに数え直す — input と change イベント

社内ブログの投稿画面で、タイトルの欄の下に「2 / 40」のような文字数を出し、入力するたびに更新したいとします。ページを開いたときに value を 1 回読んで表示するだけでは、その後に入力された文字が数に反映されません。

入力欄では、文字を入力するたびに input イベントが、値を変えて欄から離れたときに change イベントが起きます。コンソールでは入力できないので、dispatchEvent(渡したイベントを、その要素で起こすメソッド)で同じ名前のイベントを起こします。渡すイベントは new Event("input") のように、イベント名を指定して作ります。

document.body.innerHTML = `<form id="post-form"><input id="post-title"><p id="title-count">0 / 40</p></form>`;
const postTitle = document.querySelector("#post-title");
const titleCount = document.querySelector("#title-count");

// input: 入力のたびに、文字数を数え直す
postTitle.addEventListener("input", () => {
  titleCount.textContent = `${postTitle.value.length} / 40`;
});
// change: 値を変えて欄から離れたときに、下書きを保存する
postTitle.addEventListener("change", () => console.log("下書きを保存"));

// value に代入しただけ
postTitle.value = "週報";
console.log(titleCount.textContent);   // 0 / 40

// イベントを起こすと、同じ名前で登録したリスナーが呼ばれる
postTitle.dispatchEvent(new Event("input"));
console.log(titleCount.textContent);   // 2 / 40
postTitle.dispatchEvent(new Event("change"));   // 下書きを保存
代入だけでは文字数が変わらない
postTitle.value= "週報"input イベントは起きないリスナーは呼ばれない0 / 40 のまま代入の後にdispatchEvent(…)input イベントが起きるリスナーがvalue.length を読む2 / 40 に書き換わる
value への代入では input イベントが起きず、0 / 40 のままです。dispatchEvent で起こすと、リスナーが呼ばれます

input のリスナーは 1 文字ごとに呼ばれるので、下書きの保存のように入力のたびには要らない処理は、入力を終えたときの change に登録します。下の表は、利用者の操作ごとに、input と change のどちらのイベントが起きるかをまとめたものです。

操作input イベントchange イベント
テキストの欄に 1 文字入力する1 文字ごとに起きる起きない
入力を終えて、欄の外を押すか Tab で移る起きない値が変わっていれば起きる
チェックボックスを押して切り替える起きる起きる

購入画面で、「別の住所に送る」にチェックがあるときだけ住所の欄を表示します。チェックボックス otherAddress と住所の欄 addressFields は宣言済みです。

① チェックが入ったら、住所の欄から hidden の class を外すリスナーを登録してください。

② クリックを起こす前に、住所の欄の HTML を表示してください。

③ チェックボックスのクリックを起こし、チェックの有無と住所の欄の HTML を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

送信を取りやめる — submit と preventDefault

メールマガジンの登録フォームで、メールアドレスに @ が無ければエラー文を出し、送信させないようにしたいとします。送信ボタンを押すと、ブラウザはフォームの内容を送って別のページに切り替えるため、リスナーでエラー文を入れても、ページごと消えてしまいます。

フォームが送信される直前には、form 要素で submit イベントが起きます。そのリスナーで preventDefault(イベントに続けてブラウザが行う動作を取りやめるメソッド)を呼ぶと、送信は行われません。取りやめが効いたかは、イベントの defaultPrevented(効いていれば true になるプロパティ)で読みます。

document.body.innerHTML = `<form id="newsletter-form"><input id="newsletter-email"><button>登録する</button><p id="email-error"></p></form>`;
const newsletterForm = document.querySelector("#newsletter-form");
const emailInput = document.querySelector("#newsletter-email");
const emailError = document.querySelector("#email-error");

// 送信の直前に呼ばれる。@ が無いときだけ送信を取りやめる
newsletterForm.addEventListener("submit", (event) => {
  if (!emailInput.value.includes("@")) {
    event.preventDefault();
    emailError.textContent = "@ が入っていません";
  }
});

// 送信の代わりに、取りやめられる submit イベントを作って起こす
emailInput.value = "sato.example.jp";
const submitEvent = new Event("submit", { cancelable: true });
newsletterForm.dispatchEvent(submitEvent);
console.log(submitEvent.defaultPrevented);   // true(preventDefault が呼ばれた)
console.log(emailError.textContent);         // @ が入っていません
@ の有無と preventDefault で変わる結末
submit のリスナーが呼ばれる@ が無いsato.example.jp@ があるsato@example.jp@ が無いsato.example.jppreventDefault()とエラー文preventDefault()を呼ばないpreventDefault()を書き忘れた送信されずエラー文が残るブラウザが送信する送信されてエラー文も消える
@ が無いときに preventDefault() を呼ぶと、エラー文が残ります。呼び忘れると、入れたエラー文ごとページが切り替わります

左と右の列は同じ入力で、preventDefault() を呼んだかどうかだけが違います。submit は送信ボタンではなく form 要素で起きるので、リスナーは form に登録し、入力欄で Enter を押して送信したときも同じリスナーで受け取ります。

送信ボタンの click() は本当に送信する

form の中の button は type を書かないと送信ボタンになり、止めずに click() すると送信され、再読み込みまで結果が返りません。dispatchEvent の submit は送信されませんが、{ cancelable: true } が無いと preventDefault() が効きません。

パスワードの長さを確かめます。passwordFormnewPasswordpasswordError は宣言済みです。

① 8 文字未満なら送信を取りやめてエラー文を入れ、それ以外は空にするリスナーを登録してください。

② 「cat2026」で取りやめられる submit イベントを起こし、取りやめたかを表示してください。

③ 「cat2026!」でイベントを作り直して起こし、取りやめたかとエラーの欄の HTML を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

入力欄の値をまとめて集める — FormData

配送先の登録フォームで、氏名・郵便番号・置き配の有無を 1 つのオブジェクトにまとめたいとします。欄ごとに querySelectorvalue を書くと、欄を足すたびに読み取る行も書き足すことになります。フォームを送信するときは、欄の name 属性(値に付ける名前)と値が組になって送られます。

FormData(form の中の欄から、name 属性の名前と値の組を集めるオブジェクト)は new FormData(form) で作り、get("zip") で名前から値を読みます。Object.fromEntriesObject.entries とは逆に、[名前, 値] の並びからオブジェクトを作るメソッド)に渡すと、欄をまとめたオブジェクトになります。

document.body.innerHTML = `<form id="shipping-form">
  <input id="address-search" value="渋谷区">
  <input name="recipient" value="佐藤 花子">
  <input name="zip" value="150-0001">
  <input name="dropOff" type="checkbox">
  <input name="notify" type="checkbox" checked>
</form>`;
const shippingForm = document.querySelector("#shipping-form");

// form の中の欄から、名前と値の組を集める
const shippingData = new FormData(shippingForm);
console.log(shippingData.get("zip"));       // 150-0001
console.log(shippingData.get("dropOff"));   // null

// 名前と値の組から、オブジェクトを作る
const shipping = Object.fromEntries(shippingData);
console.log(JSON.stringify(shipping));
// {"recipient":"佐藤 花子","zip":"150-0001","notify":"on"}
欄の条件FormData の値
住所の検索欄name 属性なし入らない
recipient と zipname 属性あり・テキストの欄佐藤 花子 と 150-0001
dropOffname 属性あり・チェックの無いチェックボックス入らず、get は null を返す
notifyname 属性あり・チェックの入ったチェックボックスon(value 属性を書かないときの値)

FormData は Map と同じく [名前, 値] の組を順に取り出せるので、Object.fromEntries にそのまま渡せます。欄を足しても、name 属性を付けておけば、読み取る行を書き足す必要はありません。

FormData をそのまま出力すると {} になる

練習問題のコンソールはオブジェクトを JSON の文字列に変えて出力するため、console.log(shippingData) のように FormData を渡すと、欄の値が集まっていても {} と表示されます。中身は get で読むか、Object.fromEntries でオブジェクトにしてから確かめます。

求人サイトの応募フォームで、送信前に入力内容を確かめます。フォーム applyForm は宣言済みです。

① submit のリスナーで送信を取りやめ、欄の値をオブジェクトにまとめて JSON の文字列で表示してください。

② 同じリスナーで、配信に同意する欄の値を読み、値が無ければ「同意なし」と表示してください。

③ 取りやめられる submit イベントを起こしてください。

JavaScript / TypeScript エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1テキストの欄に 3 文字入力して Tab で移ると、input と change は何回ずつ起きますか?

Q2form の submit のリスナーで event.preventDefault() を呼ぶと、取りやめられるのは?

Q3チェックの無いチェックボックス name="dropOff" で、FormData の get("dropOff") は?