はじめに
Still & Spoke のラベル、条件付き座席メモ、レスポンシブレイアウトは用意済みです。現在の処理は送信なしでデモ確認を表示できます。これを実際の非同期リクエストと、スタジオが本当に受け付けた内容を示すフィードバックへ置き換えます。見た目が正しくても、サービスが確認を返すまでは申込ではありません。
HTML フォーム、基本 CSS レイアウト、JavaScript のオブジェクト、関数、DOM 選択とイベントが前提です。JSON、async/await、fetch、HTTP 結果、待機状態、回復可能なフィードバックを学びます。意味付けと条件制約は準備されており、前の Lab は役立ちますが VM ファイルは不要です。編集は /home/labex/project/stillspoke/request.js と feedback.js のみです。他のフロントエンド、API、定員、訪問者を保持します。すべて架空の人物情報で、メールや支払いはありません。60 分は著者の見積もりであり、初心者の実測結果ではありません。
全ファイルの置き換えには Workspace、サイトには Studio を使います。Study form は独立した完成例で、そこでの申込は作品の成果になりません。Studio records は受付済み申込を読みます。Practice room は受付前に一つのリクエストを拒否できる独立練習ページです。各 Lab のデータは独立しています。用意済みのバックエンド、運用環境へのデプロイ、任意リクエストの冪等性、完全なアクセシビリティ準拠は扱いません。
一つの実リクエストを送り応答を待つ
デモ処理を実際の POST に置き換え、待機中の重複操作を防ぎ、本物のサーバー確認を表示します。
Studio で Study form を選びます。Tab で Your name に進み Example Guest、Tab で Email に進み sample@example.test、Tab で Session に進み C と入力して Clay, slowly / 10:00 を読みます。未選択の支援と任意メモを Tab で通過し Reserve my place に進みます。Enter を一度押します。本物の Confirmation SS-… を待って Studio records を開き、名前、メール、時刻を照合します。Back to signup と Work form で戻ります。例は別のデータベースで、作品には既存訪問者しかいません。デモの文章は受付の証拠ではありません。
完成例は独自の見出しと読みやすい時刻を持つ期待結果です。番号は異なります。

用意済み API はこのローカル環境の一部です。公開フィールドとルールは次のとおりです。
| JSON フィールド | 意味 |
|---|---|
name |
前後の空白を除去後、2 文字以上。 |
email |
@example.test で終わる架空アドレス。まずブラウザーが標準構文を検証。 |
session |
公開された安定 ID:10:00 は clay-morning、14:00 は clay-afternoon。午後は満席。 |
support |
座席支援が選ばれたかを表すブール値。 |
note |
支援時は 5 文字以上の文字列、それ以外は任意。 |
POST は新しい申込案を api/bookings に送ります。現在の作品/例ページに対する相対 URL なので、それぞれ別の業務空間に留まります。API は JSON を返します。これはオブジェクト、文字列、数値、ブール値のテキスト形式です。プロパティ名と JSON 文字列は二重引用符を使います。JSON.stringify(data) は JavaScript オブジェクトを JSON テキストへ、response.json() は応答文をオブジェクトへ解析します。Content-Type: application/json ヘッダーは本文の内容をサービスに伝えます。画面の文章や無効化ボタンはサービスの定員ルールを実装しません。
HTTP ステータスは結果を区別します。
| 状態 | 応答と確定する事実 |
|---|---|
201 |
booking に受付値、読める sessionTitle、サーバー生成 id が入ります。受付済みで、確認番号は SS- から始まります。捏造しないこと。 |
422 |
errors はフィールド名を理由へ対応付けます。受付なし。印のある欄を修正。 |
409 |
選択回が満席。受付なし。空きのある回を選択。 |
503 |
制御された練習停止が受付前に拒否。受付なし。安全に再試行できます。 |
fetch は将来の結果を表す Promise を返します。async イベント処理では await で Promise の成功または失敗まで自身の処理を待てますが、ブラウザーは使い続けられます。HTTP 応答を待ってから JSON の解析を待ちます。422 や 503 の HTTP 拒否でも fetch は通常解決し、自動で catch に入りません。状態を明示的に確認します。ネットワークや解析の失敗は、受付の有無を確定しないまま catch に入る場合があります。メッセージには受付不明と Studio records での回復経路が必要です。
submit イベントはボタン操作と欄内 Enter の両方を扱います。event.preventDefault() は通常の移動を伴う送信を止め、この処理から JSON を送れるようにします。標準制約で無効な欄はハンドラーに届く前に送信を止めます。form.elements は見た目の位置や HTML id でなく name で欄を探します。文字は .value、チェックは .checked を使います。
待つ前に pending を設定します。待機中の別の submit はガードが早期 return し、.disabled が標準ボタンを見える形で無効にします。同じページの一つの進行中のリクエストに対する重複操作防止であり、別ページや任意の並行クライアントは保証しません。try が送信し、catch が失敗時の受付不明を伝え、finally は全結果でボタンを戻し受動メッセージを消します。フォームをリセットせず値を保持します。
clearErrors、showMessage、showResponse は feedback.js の関数です。用意済みページは HTML 解析後に defer で scenario.js、validity.js、feedback.js、request.js をこの順に読みます。最初はデータ、2 番目は標準条件ルール、最後の 2 つは送信とフィードバックを協調処理します。現在のフィードバックは正常な 201 に対応し、次にフィールド拒否を改善します。
Workspace で cd により作業場所を選びます。cat > request.js はその一ファイルを置き換えます。引用付き here-document マーカー JS により単独行の JS までをそのまま保存します。Markdown のバッククォートを除いた全体を貼り付けます。プロンプトが戻れば保存済みです。
cd /home/labex/project/stillspoke
cat > request.js <<'JS'
const form = document.querySelector('form');
const reserveButton = form.querySelector('[type="submit"]');
const progress = document.querySelector('#progress');
let pending = false;
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (pending) return;
clearErrors();
document.querySelector('#result').hidden = true;
const data = {
name: form.elements.name.value,
email: form.elements.email.value,
session: form.elements.session.value,
support: form.elements.support.checked,
note: form.elements.note.value
};
pending = true;
reserveButton.disabled = true;
progress.textContent = 'Sending your request...';
try {
const response = await fetch('api/bookings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const body = await response.json();
showResponse(response.status, body);
} catch (error) {
showMessage('No confirmation received. Acceptance is unknown. Check Studio records; wait for the original attempt to be resolved before a new submission.');
} finally {
pending = false;
reserveButton.disabled = false;
progress.textContent = '';
}
});
JS
Studio に戻り作品を更新し、Morgan Vale、morgan@example.test、Clay, slowly / 10:00 を入力・選択して支援は未選択にします。Tab でボタンに進み Enter を一度押します。Sending your request... の間ボタンは無効です。Submission result に Reserved for Morgan Vale、本物の Confirmation SS-…、読める 10:00 タイトルが出るのを待ちます。受動 role="status" メッセージにはフォーカスしません。普通の結果領域ならフォーカスして見出しと結果を見つけやすくできます。
Studio records で完全な新レコードを確認し、Existing Visitor と Avery Reed を残します。古いデモ ID は受付済み申込に存在してはいけません。HTTP 転送成功だけでは不十分で、本当に 201 であり、確認が受付済み booking 由来である必要があります。自己確認:本物の受付済みレコードがあり、進行中の送信では重複操作を禁じ、その後ボタンが戻ること。
サーバーの拒否を正しい欄に関連付ける
本物のサーバー検証拒否を、他の入力を消さない読めるフィールドエラーにします。
ブラウザーはメール構文の誤りを拒否できますが、サービスには練習用アドレスが @example.test で終わるという業務ルールもあります。morgan@other.test は正しい構文なのでサーバーまで届き HTTP 422 を受けます。これは必須欄の標準ポップアップとは別の本物の送信と拒否です。
エラー応答の形は次のとおりです。読むためのデータで、コマンドではありません。
{
"message": "Please correct the marked fields. Nothing was booked.",
"errors": {
"email": "Use an address ending in @example.test for this practice."
}
}
errors.email は API キー email を指します。順序や最初のエラー段落でなく form.elements[name] で対応付けます。角括弧内の文字列キーは対応するプロパティを選びます。Object.keys(body.errors) はプロパティ名の配列、for (const name of ...) は各名前の巡回です。let 変数は再代入できるので、first は最初に印を付けた欄を覚えます。
textContent はサーバー文を HTML として解釈せず文字で表示します。aria-invalid="true" が無効を示し、対応する email-error 段落が既存の aria-describedby で見える理由を提供します。常時のヘルプを色だけに置き換えないでください。次の送信前に古いエラーを消すと、直した欄を古い文章が説明するのを防げます。422 では全体結果を表示後、最初の無効欄へフォーカスします。それ以外なら普通の結果領域へ移せます。受動ライブ通知は別要素で、強制的にフォーカスさせません。
成功文はテンプレートリテラルを使います。バッククォートで文字を囲み、${booking.id} が現在のプロパティ値を挿入します。できた文字列を textContent がそのまま表示します。確認番号は応答オブジェクト由来で、ローカルで作りません。
Workspace で次の関数に feedback.js を置き換えます。
cd /home/labex/project/stillspoke
cat > feedback.js <<'JS'
function clearErrors() {
for (const field of document.querySelectorAll('form [name]')) {
field.removeAttribute('aria-invalid');
const error = document.getElementById(field.name + '-error');
if (error) error.textContent = '';
}
}
function showMessage(text) {
const panel = document.querySelector('#result');
document.querySelector('#message').textContent = text;
panel.hidden = false;
panel.focus();
}
function showResponse(status, body) {
if (status === 201) {
const booking = body.booking;
showMessage(`Reserved for ${booking.name}. Confirmation ${booking.id}. ${booking.sessionTitle}.`);
} else if (status === 422) {
showMessage(body.message);
let first;
for (const name of Object.keys(body.errors)) {
const field = document.querySelector('form').elements[name];
document.getElementById(name + '-error').textContent = body.errors[name];
field.setAttribute('aria-invalid', 'true');
if (!first) first = field;
}
first.focus();
} else {
showMessage(body.message);
}
}
JS
Studio に戻り更新し、Morgan Vale、morgan@other.test、Clay, slowly / 10:00、支援選択、A seat near the entrance を設定して一度送ります。全体の Nothing was booked と新しいメール固有エラーが出るはずです。Email にフォーカスし、ラベルとヘルプ、名前、回、チェック、メモは保持されます。Studio records に拒否アドレスの申込を増やしてはいけません。下書きを離れず記録を見るには、ブラウザーの Open Link in New Tab で Studio records を別ページに開き、元のフォームに戻ります。
アドレスだけ morgan@example.test に直して再送信します。前のエラーが消え、本物の新確認が出て、メモも記録に入ります。別欄の拒否もテストします。他が有効な状態で名前をちょうど 2 個の空白に置き換えます。標準 minlength="2" は入力文字を数えますが、公開ルールは前後空白を削除後に 2 文字を求めます。本物の 422 は常に Email でなく Your name に印とフォーカスを付け、メール、回、メモを保持しなければなりません。その後 Morgan Vale に戻します。同じ架空名で 2 度目の受付済み申込を作っても構いません。この演習はメール一意性システムではありません。既存訪問者は残します。
誤って @ のない morgan.example.test を入力すると POST 前にブラウザーが拒否します。想定内の標準メッセージですが、422 分岐はテストしません。標準構文が有効な morgan@other.test にしてサーバールールを確かめます。編集しても変化しない場合は Workspace で cat feedback.js を見て全体と比べ、綴りを直して Studio を更新します。JavaScript のタイプミスのためデータを削除したりサービスを再起動したりしません。自己確認:実際のサーバーフィールドキーが対応する欄と読めるエラーに関連し、他の入力が残り、修正再送信できること。
満席と安全なサービス停止から回復する
満席と受付前のサービス拒否を区別し、下書きを残して安全に再試行します。
Studio を更新して Casey Brook、casey@example.test、Clay, slowly / 14:00、支援選択、A seat near the entrance を設定します。送信前に選択された 14:00 のタイトルを読みます。最初の C は先頭の Clay 選択肢、もう一度で次に巡回します。キーが常に同じ位置を意味すると思わず、実際の選択を読んでください。
一度送信します。満席なのでサービスは HTTP 409 を返します。That session is full. Nothing was booked; choose another session. を読みます。値が残り送信ボタンが使える状態に戻ります。14:00 の既存訪問者を消して席を空けてはいけません。409 は不正メールや壊れた形式を要さず、正しい提案への業務上の拒否です。
回だけ Clay, slowly / 10:00 にします。Practice room は元フォームを開いたまま別ページを開きます。顧客の申込とは独立した練習領域です。Pause next submission を選び、Next submission will be refused before a booking is accepted. を待ちます。練習ページを閉じるか元の Studio フォームへ戻ります。練習側の Back to signup で開いている下書きを置き換えないでください。
独立ページには受付前拒否の約束が明記されています。この操作中、元フォームを開いたままにします。

元フォームを一度送ります。今度は HTTP 503 で Nothing was booked. Service paused; please retry. と明示されます。名前、アドレス、10:00、チェック、メモは残ります。この送信が一回の停止を消費します。保持した値のまま Reserve my place で Enter をもう一度押します。実際の 201 を待ち、Studio records で Casey Brook、メール、10:00、メモを照合します。既存訪問者と過去の受付済み申込も残します。
503 の練習契約は限定的です。受付前に拒否するから再試行が安全です。接続切断や読めない応答は別です。catch は受付不明であり Nothing was booked ではありません。その場合は別ページで Studio records を開きます。一致レコードが確認探しの手掛かりになることはありますが、同名同値の繰り返しではどの試行が作ったか特定できない場合があります。元の試行が未解決なら再送信しません。正常に記録を読めて一致がなくても拒否の証明ではなく、元リクエストがまだ処理中かもしれません。解決を待つかスタジオ運営者(演習では講師)に相談します。記録も読めなければサービスも待ちます。この演習で安全な再試行を確定するのは、明示された受付前の 503 だけです。冪等性は約束せず、分散決済の流れも教えません。
通常の編集ミスの回復では Workspace に移り保存ファイルを表示します。
cd /home/labex/project/stillspoke
cat request.js
正確な相対 URL api/bookings、JSON.stringify の綴り、2 つの await、finally を確認します。タイプミスならステップ 1 の全体で置き換え、Studio を更新します。新送信の前には上の境界に従って以前の不明試行を解決します。綴りの修正だけでは以前の送信が拒否されたと分かりません。解決済みと確定後、新しい架空名で該当する正常テストを行います。読み取り専用検証は代替処理を入れたり実申込をして作品を修復したりしません。
自己確認:満席と停止が待機終了後に見える結果となり、下書きを保持して修正/安全な再試行ができ、偽の受付番号を出さないこと。同等で理解できる文言も有効です。通常の受付済み申込があり、用意済み定員と訪問者状態が完全に保たれること。
まとめ
応答を待つ POST で JSON を送り、HTTP 拒否と接続の不確実性を区別し、待機中の重複操作を防ぎ、本物の受付確認を表示しました。サーバーエラーを対応欄に関連付け、満席と停止でも入力を残し、接続失敗なら必ず未受付だと決めつけず記録を回復の手掛かりにしました。
