この記事の目次
検索ボタンへ書いた処理が、Enterでは動いたり動かなかったりする。さらに別の操作から送信を呼ぶと、入力チェックが抜けてしまう。Enterで送信するフォームを扱うときは、キーを追加で監視する前に、フォームの送信イベントへ処理をまとめます。
HTMLのform、label、JavaScriptのイベント登録が読める人向けです。架空の資料室で、固定の資料名を検索するページを作ります。通信や申込受付は行いません。ブラウザー内の検索処理を、入力検証を通った送信から一度だけ呼ぶ練習です。
clickとsubmitは、同じ出来事ではない
clickはボタンなどへの操作、submitはフォームの送信が要求された出来事です。submitイベントはボタンではなくformで発生します。入力がrequiredなどの条件を満たさない場合、ブラウザーの検証で止まり、submitイベントには進みません。
「Enterではclickが絶対に起きない」という説明も正確ではありません。この例のように有効な送信ボタンがあるフォームでは、テキスト欄のEnterによる暗黙の送信が、既定ボタンのclickを経由することがあります。そのため、クリックだけのコードでも一見動く場面があります。MDNのsubmitイベントの説明では、その条件と入力検証の順序を確認できます。
それでも処理の入口をsubmitへ置く理由は、ボタンの操作方法ではなく、検証後のフォーム送信を扱いたいからです。clickの中で検索を実行すると、フォームが入力不正で送信を止める前に検索だけ進む可能性があります。また、スクリプトからrequestSubmitを呼ぶ経路も、ボタンのclick監視に任せられません。
検索回数が見える一枚のページを保存する
次をsearch.htmlとして保存してブラウザーで開きます。JavaScriptの登録が終わるまでfieldset全体を無効にし、準備前の送信を防いでいます。資料名と検索結果は練習用です。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>資料室の検索|送信の練習</title>
<style>
* { box-sizing: border-box; }
body { margin: 16px; font-family: sans-serif; line-height: 1.8; color: #222; }
main { max-width: 560px; margin: auto; }
fieldset { min-width: 0; padding: 16px; border: 1px solid #555; }
input { display: block; width: 100%; font: inherit; margin-block: 8px; }
button { font: inherit; }
p { overflow-wrap: anywhere; }
:focus-visible { outline: 3px solid #174ea6; outline-offset: 3px; }
</style>
</head>
<body>
<main>
<h1>資料室の検索(架空の練習)</h1>
<p>入力は保存・送信しません。「地図」などで試せます。</p>
<form id="search-form">
<fieldset id="fields" disabled>
<legend>資料名で探す</legend>
<label for="query">検索語</label>
<input id="query" type="search" required aria-describedby="query-help">
<p id="query-help">空白以外の文字を入力してください。</p>
<button type="submit">検索する</button>
</fieldset>
</form>
<p id="result" role="status">まだ検索していません。</p>
<p>検索実行回数:<span id="count">0</span></p>
<noscript><p>この練習にはJavaScriptを有効にしてください。</p></noscript>
</main>
<script>
const form = document.querySelector('#search-form');
const input = document.querySelector('#query');
const result = document.querySelector('#result');
const counter = document.querySelector('#count');
const records = ['町の地図', '古い時刻表', '植物の図鑑'];
let count = 0;
input.addEventListener('input', () => {
input.setCustomValidity('');
result.textContent = '入力を変更しました。検索する操作で結果を更新します。';
});
form.addEventListener('submit', event => {
event.preventDefault();
const query = input.value.trim();
if (query === '') {
input.setCustomValidity('空白以外の文字を入力してください。');
result.textContent = '検索していません。検索語を確認してください。';
input.reportValidity();
return;
}
const matches = records.filter(title => title.includes(query));
count += 1;
counter.textContent = String(count);
result.textContent = matches.length > 0
? `「${query}」の検索結果:${matches.join('、')}`
: `「${query}」に一致する資料はありません。`;
});
document.querySelector('#fields').disabled = false;
</script>
</body>
</html>
「地図」を入力して検索すると「町の地図」が出て、実行回数が1になります。ページを読み直してから同じ語を入力し、入力欄でEnterを押した場合も同じ結果が一回だけ出れば、二つの操作を共通の入口で扱えています。Enterの扱いは端末や入力方法にもよるため、送信ボタンも残します。
ハンドラーはページ末尾で一度だけ登録しています。部品を何度も生成するアプリへ移すなら、イベントの登録と解除の記事で扱う寿命の管理も必要です。今回は再初期化しない一枚のHTMLに限定しています。
必須入力の検証と、空白だけの判定を分担する
空欄はrequiredで送信前に止まります。ただし半角スペースだけの文字列は、空の値ではありません。この教材では前後をtrimした結果が空かを独自に調べ、検索を実行しない条件にしています。入力検証のすべてをrequired一つへ任せないためです。
setCustomValidityへ説明文を渡すと、その欄に独自の検証エラーが残ります。利用者が文字を直したときは、inputイベントで空文字を設定して解除します。解除し忘れると、正しい語へ直しても次のsubmitへ進めません。MDNのsetCustomValidityの説明にある、メッセージの設定と解除を対にしています。
結果の表示はtextContentで行い、検索語をHTMLへ変換しません。出力先の基本は文字をそのまま表示する記事と共通です。一方、検索結果を出した後に入力を編集したら、古い結果をそのまま最新の結果に見せず、検索操作を待つ案内へ戻します。
二重実行と、検証を飛び越える呼び出しを直す
一つ目の失敗は、元のclickの検索処理を残したまま、同じ処理をsubmitへも追加することです。送信ボタンのクリックから両方に進むと、一回の操作で二回検索されます。修正では検索本体をsubmit側だけに置きます。回数を0へ戻し、クリックとEnterを一回ずつ行って1、2と増えるか再確認します。
keydownでEnterを拾って独自に検索する追加処理も、この完成例には不要です。標準の送信と二重になったり、日本語入力の変換確定と混ざったりする入口を増やすためです。textareaの改行を検索へ変えるような拡張も行いません。
二つ目は、別のJavaScriptからform.submit()を呼び、「同じsubmitハンドラーが動く」と考える失敗です。このメソッドは入力検証とsubmitイベントを通りません。検証を含む経路を要求するならrequestSubmit()を選びます。requestSubmitの公式説明で両者の違いを確認できます。
完成例を開いた開発者ツールのConsoleでは、次の一行で比較できます。自分で作ったこの練習ページだけで実行してください。
document.querySelector('#search-form').requestSubmit();
検索語が「地図」なら結果が更新されて回数が1増え、空欄なら回数は増えません。独自エラーを設定した後は、欄を実際に入力し直してから試します。スクリプトでvalueだけを書き換えてもinputイベントは自動で発生せず、この例の解除処理を通らないためです。
通信を加えるときは、送信の先にも確認が要る
preventDefaultは、フォームの標準の送信動作を止めるために、ハンドラーの先頭で呼んでいます。ここではサーバーへ移動せずローカル検索をするので必要です。actionのURLへ通常のフォーム送信を行いたい設計では、無条件に止めると本来の送信を妨げます。
fetchで受付を作る場合は、送信中・成功・失敗の表示、再操作、サーバー側の入力検証を追加します。今回の回数表示は処理の重複を観察する道具で、二重受付を防ぐ仕組みではありません。入力欄へnameも付けていないため、実際にフォームデータを送る構成へ広げるときは、送る項目も設計し直します。
四つの入力と、三つの入口で確かめる
15分程度を目安に、ページを読み直して表の順に確認します。空白だけの行から次へ進むときは、入力欄を手で編集し、独自エラーを解除してください。
| 入力 | 操作 | 期待する結果 |
|---|---|---|
| 地図 | クリック | 町の地図、回数1 |
| 地図 | 入力欄でEnter | 町の地図、回数2 |
| 空欄 | クリック | 必須入力の案内、回数2のまま |
| 半角スペース三つ | Enter | 空白以外を求める案内、回数2のまま |
| 星 | ConsoleのrequestSubmit | 一致する資料なし、回数3 |
続けてTabで入力欄と送信ボタンへ移動し、ボタンにいる状態でEnterを押しても一回だけ進むか確かめます。幅320pxで入力欄と結果がはみ出さず読めることも確認してください。日本語入力の変換確定は、使用する実際の入力環境で別に試します。JavaScriptの無料レッスンでフォームを増やすときも、「どの入口から、どの検証を経て処理へ届くか」をこの表の形で残せます。
TRY A LESSON
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。