👁

問い合わせフォームを作ろう

この章では、DOM操作・非同期処理を組み合わせた問い合わせフォームをハンズオン形式で学習します。これにより、入力バリデーション・HTTPステータスに応じたエラー処理・送信中の表示を備えた実用的なフォームを実装できるようになります。

1. 本章の概要

1.1 本章の目的

前章 JavaScriptの応用構文 で扱ったDOM操作・非同期処理は、いずれも「書き方を1つずつ確認する」ための単発の例でした。本章では、1つのハンズオンとして問い合わせフォームを作りながら、実際のフォーム実装で必要になる入力バリデーション・HTTPステータスに応じたエラー処理・送信中のUX(送信中の表示・二重送信の防止)まで踏み込みます。ここまで作れると、素のJavaScriptだけで「入力を受け取ってサーバに送るUI」を一通り実装できる状態になります。

1.2 ハンズオンの流れ

最初に index.htmlstyle.css でフォームのUIを用意し、以降は app.js を段階的に書き加えながら、フォームUIの準備 → 入力値の取得と送信 → 入力バリデーション → 送信中の表示 と、少しずつ動きを加えます。最終的に、実際の問い合わせフォームに近い動作を1画面で完結させます。送信先には、前章で紹介した httpbin.org のエンドポイントをそのまま利用します。

1.3 事前準備

必要なツール

この章では、以下のツールを使用します。まだインストールしていない場合は、リンク先の手順に沿って準備をお願いします。

ツール名 関連箇所 理由
Google Chrome(またはFirefox / Edge / Safari) 作成したページを開き、DevToolsで動作を確認する
Visual Studio Code Visual Studio Codeのインストール index.htmlapp.js を作成・編集するエディタとして使用する

2. フォームUIの準備

まずは、以降のセクションで扱う index.htmlstyle.css を用意します。index.html にはフォーム本体(名前と本文の入力欄、送信ボタン、送信結果を表示する領域)を配置し、style.css にフォームの見た目を整えるスタイルを書きます。

2.1 作業用フォルダを用意する

以降のセクションで扱うファイルをひとつにまとめるため、まず専用の作業用フォルダを用意します。

任意の場所に frontend-contact-form フォルダを作成し、Visual Studio Codeの「ファイル」→「フォルダーを開く」から、作成した frontend-contact-form フォルダを開きます。

frontend-contact-form  ← このフォルダを作成

Visual Studio Codeのエクスプローラーに frontend-contact-form フォルダが表示されていれば、フォルダの準備は完了です。

index.html を作成する

用意したフォルダに、フォーム本体のHTMLを配置します。名前と本文の入力欄、送信ボタン、送信結果を表示する領域を1つの <form> にまとめます。

index.html を作成します。Visual Studio Codeのエクスプローラーで frontend-contact-form フォルダを右クリックし、「新しいファイル」を選択して index.html という名前で作成してください。

frontend-contact-form/
└── index.html  ← このファイルを作成

作成した index.html に以下の内容を記述して保存します。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>問い合わせフォーム</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <h1>お問い合わせ</h1>
    <p id="status"></p>
    <form id="contact-form">
      <div>
        <label for="name">お名前</label>
        <input type="text" id="name" name="name" />
      </div>
      <div>
        <label for="message">お問い合わせ内容</label>
        <textarea id="message" name="message"></textarea>
      </div>
      <button type="submit" id="submit-btn">送信</button>
    </form>
    <script src="app.js" defer></script>
  </body>
</html>

style.css を作成する

HTMLだけではブラウザ既定のスタイルで表示されるため、フォームの見た目を整えるCSSを追加します。

style.css を作成します。Visual Studio Codeのエクスプローラーで frontend-contact-form フォルダを右クリックし、「新しいファイル」を選択して style.css という名前で作成してください。

frontend-contact-form/
├── index.html
└── style.css  ← このファイルを作成

作成した style.css に以下の内容を記述して保存します。

body {
  font-family: sans-serif;
  max-width: 600px;
  margin: 40px auto;
  padding: 0 20px;
}
form div {
  margin-bottom: 16px;
}
label {
  display: block;
  margin-bottom: 4px;
  font-weight: bold;
}
input[type="text"],
textarea {
  width: 100%;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
  font-family: inherit;
}
textarea {
  min-height: 100px;
}
button {
  padding: 8px 24px;
  background-color: #0066cc;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
button:disabled {
  background-color: #999;
  cursor: not-allowed;
}
#status {
  margin-bottom: 16px;
  padding: 12px;
  background-color: #f5f5f5;
  border-left: 4px solid #0066cc;
}
#status:empty {
  display: none;
}

2.2 ブラウザで表示を確認する

HTMLとCSSが揃ったので、実際にブラウザで開いて表示を確認します。

index.html をブラウザで開くと、「お問い合わせ」の見出し、名前と本文の入力欄、送信ボタンが以下のように表示されれば、UIの準備は完了です。

送信結果を表示する <p id="status"> はCSS側で中身が空のときは非表示にしているため、この時点では枠が見えません(詳細はこのあとのCSS解説で扱います)。

2.3 コード解説

<form> から順に、HTMLとCSSの主要なパーツを見ていきます。

<form id="contact-form">

<form> はフォーム全体を包む要素です。JavaScriptから取得できるように id を付けています。

<input type="text" id="name" name="name" />
<textarea id="message" name="message"></textarea>

名前は1行入力の <input>、お問い合わせ内容は複数行入力の <textarea> で受け取ります。前章で扱ったフォームには required を付けていましたが、本章ではJavaScript側で入力バリデーションを扱うため、あえて外しています。

<button type="submit" id="submit-btn">送信</button>

type="submit" を付けたボタンをクリックすると、そのボタンが属する <form> のsubmitイベントが発火します。後で送信中の見た目を切り替えるため、id を付けておきます。

<p id="status"></p>

送信結果やエラーメッセージを表示するための領域です。フォームの上に配置しておき、JavaScriptから textContent を書き換えて使います。中身が空のときはCSS側で枠ごと非表示にしているため、初期表示時は何も出ません(詳細はこのあとのCSS解説で扱います)。

style.css は、前章 HTML/CSSの基本 で扱った内容の復習を兼ねて、フォームの見た目を整える最小限のスタイルを当てています。ポイントとなる書き方を以下で確認します。

input[type="text"],
textarea {
  width: 100%;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
  font-family: inherit;
}

input[type="text"] は属性セレクタで、type="text"<input> だけを対象にします。カンマ区切りで textarea にも同じスタイルを一括で当てています。box-sizing: border-box を指定すると、width: 100%paddingborder の分も含めた計算になるため、指定した幅どおりに親要素へきれいに収まります。

button:disabled {
  background-color: #999;
  cursor: not-allowed;
}

:disabled は疑似クラスで、<button disabled> のように無効化された状態のボタンだけを対象にします。「送信中の表示と二重送信の防止」のセクションでJavaScriptから disabled = true を設定するときに、このスタイルが「送信中は押せない」ことを視覚的に示すフィードバックになります。

#status:empty {
  display: none;
}

:empty も疑似クラスで、子要素もテキストも一切持たない状態の要素だけを対象にします。<p id="status"> は初期状態では中身が空なので、この指定によって枠自体が表示されず、送信結果やエラーメッセージが入ったときにだけ枠が現れる動きになります。

3. 入力値の取得と送信

フォームの入力値を取り出し、httpbin.org/post へJSONとして送信するところまでを実装します。ここでは、fetch の失敗を「ネットワークエラー」と「サーバがエラーステータスを返した場合」の両方で捕捉できるようにします。

フォームには、送信ボタンが押されたときやEnterキーで送信されたときに発火する submit イベントがあります。ブラウザは既定でsubmitを受け取るとページ遷移(またはリロード)を行うため、JavaScriptから通信を制御する場合は event.preventDefault() で既定動作を止める必要があります。書き方は以下のとおりです。

フォーム要素.addEventListener("submit", (event) => {
  event.preventDefault();
  // 送信処理
});

入力要素の値は .value プロパティから読み出せます。<input> でも <textarea> でも同じ書き方で取得できます。

fetch は第2引数のオプションで、HTTPメソッド・ヘッダ・ボディを指定できます。ここで注意したいのは、fetch のPromiseは「HTTPリクエストが送れて、レスポンスが返ってきた」時点で解決されるという点です。サーバが 404 Not Found500 Internal Server Error を返しても、Promiseは成功(resolve)として解決されます。

成功レスポンス(2xx)とエラーレスポンス(4xx / 5xx)を区別するには、レスポンスオブジェクトの ok プロパティを確認します。書き方は以下のとおりです。

if (!response.ok) {
  throw new Error("エラーメッセージ");
}

response.ok は、HTTPステータスが 200299 の範囲にあるときだけ true になります。それ以外のステータスでは false となるため、明示的に throw してエラー扱いにしないと、失敗レスポンスがそのまま then 側に流れてしまいます。

app.js を作成して実装する

上で確認したsubmitイベントとfetchの書き方を組み合わせて、app.js にフォーム送信の処理を実装します。まず、app.js を作成します。Visual Studio Codeのエクスプローラーで frontend-contact-form フォルダを右クリックし、「新しいファイル」を選択して app.js という名前で作成してください。

frontend-contact-form/
├── index.html
├── style.css
└── app.js  ← このファイルを作成

これは、submit イベントを捕まえて入力値を送信し、成功時はレスポンスの内容を、失敗時はエラーメッセージを画面に表示するコードです。作成した app.js に以下の内容を記述して保存します。

const form = document.querySelector("#contact-form");
const status = document.querySelector("#status");

form.addEventListener("submit", async (event) => {
  event.preventDefault();

  const payload = {
    name: document.querySelector("#name").value,
    message: document.querySelector("#message").value,
  };

  try {
    const response = await fetch("https://httpbin.org/post", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      throw new Error(`サーバがエラーを返しました(${response.status})`);
    }

    const data = await response.json();
    status.textContent = `送信に成功しました。サーバに届いた内容: ${JSON.stringify(data.json)}`;
  } catch (error) {
    status.textContent = `送信に失敗しました: ${error.message}`;
  }
});

3.1 動作確認

ブラウザで index.html を再読み込みし、名前とお問い合わせ内容を入力します。

「送信」ボタンを押すと、フォームの上に配置した <p id="status"> に、以下のような送信結果が表示されます。

送信に成功しました。サーバに届いた内容: {...} の形で、入力した名前とお問い合わせ内容がそのまま画面に返ってきます。入力内容がそのまま画面に反映されていることが動作確認のポイントで、fetch の POST リクエストが httpbin.org に届き、httpbin.org/post が返す json フィールドを取り出して画面に反映できていることが確認できます。これで「入力 → 送信 → レスポンス表示」の骨格が動くようになりました。

3.2 コード解説

submit イベントを捕まえてから fetch でPOST送信するまでの流れを、キーとなる部分ごとに見ていきます。

form.addEventListener("submit", async (event) => {
  event.preventDefault();

コールバックに async を付けているのは、この中で await を使うためです。フォームの既定動作を止めてから、非同期の送信処理へ進みます。

const payload = {
  name: document.querySelector("#name").value,
  message: document.querySelector("#message").value,
};

<input><textarea>.value プロパティで、入力された文字列を取り出してJavaScriptのオブジェクトにまとめています。

const response = await fetch("https://httpbin.org/post", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(payload),
});

fetch の第2引数で、HTTPメソッド・ヘッダ・ボディを指定しています。JSONを送るときは、Content-Typeapplication/json を指定し、JSON.stringify でオブジェクトを文字列化して body に渡す組み合わせが一般的です。

if (!response.ok) {
  throw new Error(`サーバがエラーを返しました(${response.status})`);
}

response.ok を確認し、false4xx / 5xx)だった場合は自分でエラーを投げて catch 側に処理を流します。この1行を書かないと、たとえば「送信先URLを打ち間違えて404が返った」ケースを成功扱いしてしまい、あとの data.json を読む時点で予期しない動作をします。

} catch (error) {
  status.textContent = `送信に失敗しました: ${error.message}`;
}

ネットワーク切断など通信そのものの失敗と、上で自分で throw したHTTPステータスエラーの両方が、この catch ブロックに流れてきます。ユーザには「何が起きたか」を1行のメッセージで示します。

💡 ポイント
fetch は成功レスポンスとエラーレスポンスの区別をアプリケーション側に任せる設計になっています。この点は axios などのHTTPクライアントライブラリと異なります(axios は既定で 4xx / 5xx を例外として投げます)。fetch を使う場合は、response.ok の判定を書き忘れないよう意識してください。

4. 入力バリデーションの追加

現状のコードは、入力欄が空でもそのまま送信できてしまいます。実運用のフォームでは、送信前に入力内容の最低限のチェックを行い、問題があればユーザに知らせて送信を中止するのが自然です。本セクションでは、以下の2つのバリデーションを追加します。

  • 名前・お問い合わせ内容の空チェック
  • お問い合わせ内容の文字数上限チェック(200文字以内)

app.js にバリデーションを追加する

これは、fetch を呼ぶ前にバリデーションを実行し、エラーがあれば <p id="status"> にメッセージを表示して return で処理を中止するコードです。app.js を以下の内容で上書き保存します。

const form = document.querySelector("#contact-form");
const status = document.querySelector("#status");

form.addEventListener("submit", async (event) => {
  event.preventDefault();

  const name = document.querySelector("#name").value.trim();
  const message = document.querySelector("#message").value.trim();

  if (name === "" || message === "") {
    status.textContent = "お名前とお問い合わせ内容の両方を入力してください。";
    return;
  }

  if (message.length > 200) {
    status.textContent = "お問い合わせ内容は200文字以内で入力してください。";
    return;
  }

  try {
    const response = await fetch("https://httpbin.org/post", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name, message }),
    });

    if (!response.ok) {
      throw new Error(`サーバがエラーを返しました(${response.status})`);
    }

    const data = await response.json();
    status.textContent = `送信に成功しました。サーバに届いた内容: ${JSON.stringify(data.json)}`;
  } catch (error) {
    status.textContent = `送信に失敗しました: ${error.message}`;
  }
});

4.1 動作確認

ブラウザで index.html を再読み込みしてから、まず入力欄を空のまま「送信」を押すと、<p id="status"> に以下のようなバリデーションメッセージが表示されます。

「お名前とお問い合わせ内容の両方を入力してください。」のメッセージが表示されます。

次に、お問い合わせ内容に 200 文字を超える長い文字列を入力して送信すると、以下のようなバリデーションメッセージが表示されます。

「お問い合わせ内容は 200 文字以内で入力してください。」のメッセージが表示されます。どちらのケースも、赤枠のメッセージが表示されつつ通信は発生しないことが動作確認のポイントで、fetch の呼び出しには到達せず、DevTools の Network タブでも通信が発生していないことが確認できます。

4.2 コード解説

追加した2つのバリデーション(空チェック・文字数上限チェック)の書き方を順に見ていきます。

const name = document.querySelector("#name").value.trim();
const message = document.querySelector("#message").value.trim();

String.prototype.trim は、文字列の先頭と末尾の空白文字(スペース・タブ・改行など)を取り除きます。これにより、スペースだけが入力された場合も空扱いとして判定できます。

if (name === "" || message === "") {
  status.textContent = "お名前とお問い合わせ内容の両方を入力してください。";
  return;
}

いずれかの入力が空だった場合、エラーメッセージを表示して return で関数を抜けます。これで以降の fetch は実行されません。

if (message.length > 200) {
  status.textContent = "お問い合わせ内容は200文字以内で入力してください。";
  return;
}

String.prototype.length で文字数を確認します。上限を超えていた場合も同様に、メッセージを出して処理を中止します。

📝 HTML標準のrequiredとの違い
<input required> を付けると、ブラウザ標準の仕組みで空入力時の送信をブロックし、「このフィールドを入力してください」というツールチップが表示されます。実装コストがかからない一方、メッセージのカスタマイズや複雑な条件(複数入力の関係、文字数上限、正規表現による形式チェックなど)を扱うのは難しくなります。実運用ではHTML標準の required と、JavaScriptによる詳細なバリデーションを組み合わせて使うのが一般的です。本章ではJavaScript側の書き方を学ぶため、required を外したうえで自前のチェックを実装しています。

5. 送信中の表示と二重送信の防止

現状のコードでは、送信ボタンを連打すると同じ内容が何度もサーバに送られてしまいます。また、通信中はユーザに何も伝わらないため、「押せていないのか、送信中なのか」が分かりません。本セクションでは、以下の2つを追加します。

  • 送信中は送信ボタンを 無効化disabled)し、ラベルを「送信中…」に切り替える
  • 通信完了後(成功・失敗の両方)に、ボタンを元の状態に戻す

「成功・失敗のどちらでも必ず元に戻す」処理には、try / catchfinally ブロックを使うのが最適です。finally はエラーの有無に関係なく、try ブロックを抜ける直前に必ず実行されます。書き方は以下のとおりです。

try {
  // 通常の処理
} catch (error) {
  // エラー処理
} finally {
  // エラーの有無にかかわらず必ず実行する処理
}

app.js に送信中の表示処理を追加する

これは、送信開始時にボタンを無効化して「送信中…」ラベルに切り替え、通信完了後に finally で確実に元へ戻すコードです。app.js を以下の内容で上書き保存します。

const form = document.querySelector("#contact-form");
const submitBtn = document.querySelector("#submit-btn");
const status = document.querySelector("#status");

form.addEventListener("submit", async (event) => {
  event.preventDefault();

  const name = document.querySelector("#name").value.trim();
  const message = document.querySelector("#message").value.trim();

  if (name === "" || message === "") {
    status.textContent = "お名前とお問い合わせ内容の両方を入力してください。";
    return;
  }

  if (message.length > 200) {
    status.textContent = "お問い合わせ内容は200文字以内で入力してください。";
    return;
  }

  submitBtn.disabled = true;
  submitBtn.textContent = "送信中…";
  status.textContent = "";

  try {
    const response = await fetch("https://httpbin.org/post", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name, message }),
    });

    if (!response.ok) {
      throw new Error(`サーバがエラーを返しました(${response.status})`);
    }

    const data = await response.json();
    status.textContent = `送信に成功しました。サーバに届いた内容: ${JSON.stringify(data.json)}`;
  } catch (error) {
    status.textContent = `送信に失敗しました: ${error.message}`;
  } finally {
    submitBtn.disabled = false;
    submitBtn.textContent = "送信";
  }
});

5.1 動作確認

ブラウザで index.html を再読み込みし、名前とお問い合わせ内容を入力してから「送信」ボタンを押すと、通信中の一瞬だけボタンが灰色になり、ラベルが「送信中…」に切り替わります。この間はボタンを何度クリックしても再送信されず、通信が完了するとボタンが「送信」の表示・押下可能な状態に戻り、<p id="status"> に成功メッセージが表示されます。

5.2 コード解説

送信開始時と finally で行っている、ボタンの状態切り替えの書き方を見ていきます。

submitBtn.disabled = true;
submitBtn.textContent = "送信中…";
status.textContent = "";

disabled プロパティに true を代入すると、ボタンがクリックを受け付けなくなり、ブラウザ既定の無効化スタイル(多くの場合は灰色)が適用されます。あわせてラベルを「送信中…」に変更し、直前のメッセージ表示をクリアしています。

} finally {
  submitBtn.disabled = false;
  submitBtn.textContent = "送信";
}

finally ブロックは、try を正常に抜けた場合、catch を実行した場合、そして catch の中でさらにエラーが投げられた場合のすべてで必ず実行されます。ここでボタンを元の状態に戻すことで、通信が失敗しても「ボタンが押せないまま」という状態を防げます。

💡 ポイント
送信中の表示の切り替えを try ブロックの中に書くと、バリデーションで先に return した場合や、事前に何らかの理由で throw された場合に finally が呼ばれない状況が生まれます。本章の実装のように「バリデーションを先に済ませてから、UIの切り替え → try に入る」順序で書くと、finally で元に戻す処理を確実に走らせられます。

6. まとめ

この章では、DOM操作・非同期処理を組み合わせて、入力バリデーション・HTTPステータス判定・送信中の表示を備えた問い合わせフォームを体験しました。

  • <form>submit イベントで event.preventDefault() を呼ぶと、ブラウザ既定のページ遷移を止められる
  • fetch の第2引数で method / headers / body を指定してPOST送信できる
  • fetch4xx / 5xx のレスポンスでも成功として解決するため、response.ok を明示的に確認する必要がある
  • 送信前に .value.trim().length で空チェック・文字数チェックを行い、問題があれば return で処理を中止できる
  • 送信ボタンの disabledtrue に切り替えると、二重送信を防止できる
  • try / catch / finallyfinally ブロックで、成功・失敗にかかわらず送信中の表示を確実に元へ戻せる

次の章では、JavaScriptに型を加えたTypeScriptを学びつつ、実際にJavaScriptコードをTypeScriptへ書き換える作業を体験します。

この教材は役に立ちましたか?

いいねをたくさんいただけると、制作者の励みになり、より多くのセクションが作れるようになります。

感想を一言(任意)

いただいたコメントは次の制作のヒントになります。ぜひお気軽にご投稿ください。

このコメントは他の受講生には公開されません。DevOps Camp運営が、教材改善のために確認します。

0 / 2000