AstroページでHTMLフォームを構築する
オンデマンドでレンダリングされるAstroページは、フォームの表示と処理の両方に対応できます。このレシピでは、標準的なHTMLフォームでデータをサーバーへ送信します。フロントマターのスクリプトでサーバー側の処理を行い、クライアントへはJavaScriptを送信しません。
:::tip[Astro Actionsでフォームを構築する]Astro v4.15ではActionsが追加され、基本的なHTMLフォームよりも、データ検証や送信結果に基づくUI更新などの利点が得られます。こちらの方法を使いたい場合は、Actionsガイドをご覧ください。:::
前提条件
- サーバーアダプターをインストールしたAstroプロジェクト。
レシピ
フォーム本体とハンドリングコードを含む
.astroページを作成(または特定)します。たとえば、登録ページを追加します。--- --- <h1>Register</h1>ページに
<form>タグと各種入力フィールドを追加します。各入力には、その値の意味を表すname属性を付けます。必ず送信用の
<button>または<input type="submit">要素を含めます。--- --- <h1>Register</h1> <form> <label> Username: <input type="text" name="username" /> </label> <label> Email: <input type="email" name="email" /> </label> <label> Password: <input type="password" name="password" /> </label> <button>Submit</button> </form>バリデーション属性を使い、JavaScriptが無効でも動作する基本的なクライアントサイド検証を追加します。
この例では、
requiredは、入力が埋まるまで送信を防ぎます。minlengthは、入力文字数の下限を設定します。type="email"は、有効なメール形式のみを受け付ける検証を導入します。
--- --- <h1>Register</h1> <form> <label> Username: <input type="text" name="username" required /> </label> <label> Email: <input type="email" name="email" required /> </label> <label> Password: <input type="password" name="password" required minlength="6" /> </label> <button>Submit</button> </form>:::tip複数フィールドを参照する独自の検証は、
<script>タグとConstraint Validation APIで追加できます。より複雑な検証ロジックを簡単に記述するには、フロントエンドフレームワークと、React Hook FormやFelteのようなフォームライブラリを使う方法もあります。:::
フォーム送信によりブラウザーは同じページを再リクエストします。データ転送方式をURLパラメーターではなく
Requestボディで送るために、フォームのmethodをPOSTに変更します。--- --- <h1>Register</h1> <form method="POST"> <label> Username: <input type="text" name="username" required /> </label> <label> Email: <input type="email" name="email" required /> </label> <label> Password: <input type="password" name="password" required minlength="6" /> </label> <button>Submit</button> </form>フロントマターで
POSTメソッドを判定し、Astro.request.formData()でフォームデータへアクセスします。POSTがフォーム送信でない場合などformDataが不正なケースに備え、try ... catchで囲みます。--- export const prerender = false; // 「server」モードでは不要です if (Astro.request.method === "POST") { try { const data = await Astro.request.formData(); const name = data.get("username"); const email = data.get("email"); const password = data.get("password"); // 取得したデータを処理する } catch (error) { if (error instanceof Error) { console.error(error.message); } } } --- <h1>Register</h1> <form method="POST"> <label> Username: <input type="text" name="username" required /> </label> <label> Email: <input type="email" name="email" required /> </label> <label> Password: <input type="password" name="password" required minlength="6" /> </label> <button>Submit</button> </form>サーバー側でフォームデータを検証します。これは、悪意のある送信を防ぐためにクライアント側と同じ検証を含めるべきですし、フォーム検証を持たないレガシーブラウザーへの対応にも役立ちます。
また、クライアント側では行えない検証も含められます。たとえば、メールアドレスがすでにデータベースに存在するかどうかを確認します。
エラーメッセージは
errorsオブジェクトに保持し、テンプレートで参照することでクライアントへ返すことができます。--- export const prerender = false; // Not needed in 'server' mode import { isRegistered, registerUser } from "../../data/users" import { isValidEmail } from "../../utils/isValidEmail"; const errors = { username: "", email: "", password: "" }; if (Astro.request.method === "POST") { try { const data = await Astro.request.formData(); const name = data.get("username"); const email = data.get("email"); const password = data.get("password"); if (typeof name !== "string" || name.length < 1) { errors.username += "Please enter a username. "; } if (typeof email !== "string" || !isValidEmail(email)) { errors.email += "Email is not valid. "; } else if (await isRegistered(email)) { errors.email += "Email is already registered. "; } if (typeof password !== "string" || password.length < 6) { errors.password += "Password must be at least 6 characters. "; } const hasErrors = Object.values(errors).some(msg => msg) if (!hasErrors) { await registerUser({name, email, password}); return Astro.redirect("/login"); } } catch (error) { if (error instanceof Error) { console.error(error.message); } } } --- <h1>Register</h1> <form method="POST"> <label> Username: <input type="text" name="username" /> </label> {errors.username && <p>{errors.username}</p>} <label> Email: <input type="email" name="email" required /> </label> {errors.email && <p>{errors.email}</p>} <label> Password: <input type="password" name="password" required minlength="6" /> </label> {errors.password && <p>{errors.password}</p>} <button>Register</button> </form>