Q1テキストの欄に 3 文字入力して Tab で移ると、input と change は何回ずつ起きますか?
フォーム — 入力値と 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">
checked も同じで、チェックを入れても HTML に checked 属性は付きません。数量の欄を空にすると value は空の文字列になり、Number("") は 0 を返すため、数に直した後では未入力と 0 の入力を見分けられません。
入力のたびに数え直す — 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")); // 下書きを保存
input のリスナーは 1 文字ごとに呼ばれるので、下書きの保存のように入力のたびには要らない処理は、入力を終えたときの change に登録します。下の表は、利用者の操作ごとに、input と change のどちらのイベントが起きるかをまとめたものです。
| 操作 | input イベント | change イベント |
|---|---|---|
| テキストの欄に 1 文字入力する | 1 文字ごとに起きる | 起きない |
| 入力を終えて、欄の外を押すか Tab で移る | 起きない | 値が変わっていれば起きる |
| チェックボックスを押して切り替える | 起きる | 起きる |
送信を取りやめる — 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 は送信ボタンではなく form 要素で起きるので、リスナーは form に登録し、入力欄で Enter を押して送信したときも同じリスナーで受け取ります。
送信ボタンの click() は本当に送信する
form の中の button は type を書かないと送信ボタンになり、止めずに click() すると送信され、再読み込みまで結果が返りません。dispatchEvent の submit は送信されませんが、{ cancelable: true } が無いと preventDefault() が効きません。
入力欄の値をまとめて集める — FormData
配送先の登録フォームで、氏名・郵便番号・置き配の有無を 1 つのオブジェクトにまとめたいとします。欄ごとに querySelector と value を書くと、欄を足すたびに読み取る行も書き足すことになります。フォームを送信するときは、欄の name 属性(値に付ける名前)と値が組になって送られます。
FormData(form の中の欄から、name 属性の名前と値の組を集めるオブジェクト)は new FormData(form) で作り、get("zip") で名前から値を読みます。Object.fromEntries(Object.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 と zip | name 属性あり・テキストの欄 | 佐藤 花子 と 150-0001 |
| dropOff | name 属性あり・チェックの無いチェックボックス | 入らず、get は null を返す |
| notify | name 属性あり・チェックの入ったチェックボックス | on(value 属性を書かないときの値) |
FormData は Map と同じく [名前, 値] の組を順に取り出せるので、Object.fromEntries にそのまま渡せます。欄を足しても、name 属性を付けておけば、読み取る行を書き足す必要はありません。
FormData をそのまま出力すると {} になる
練習問題のコンソールはオブジェクトを JSON の文字列に変えて出力するため、console.log(shippingData) のように FormData を渡すと、欄の値が集まっていても {} と表示されます。中身は get で読むか、Object.fromEntries でオブジェクトにしてから確かめます。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2form の submit のリスナーで event.preventDefault() を呼ぶと、取りやめられるのは?
Q3チェックの無いチェックボックス name="dropOff" で、FormData の get("dropOff") は?