AstroページでHTMLフォームを構築する
オンデマンドでレンダリングされるAstroページは、フォームの表示と処理の両方に対応できます。このレシピでは、標準的なHTMLフォームでデータをサーバーへ送信します。フロントマターのスクリプトでサーバー側の処理を行い、クライアントへはJavaScriptを送信しません。
Astro v4.15ではActionsが追加され、基本的なHTMLフォームよりも、データ検証や送信結果に基づくUI更新などの利点が得られます。こちらの方法を使いたい場合は、Actionsガイドをご覧ください。
- サーバーアダプターをインストールしたAstroプロジェクト。
-
フォーム本体とハンドリングコードを含む
.astro
ページを作成(または特定)します。たとえば、登録ページを追加します。src/pages/register.astro ------<h1>Register</h1> -
ページに
<form>
タグと各種入力フィールドを追加します。各入力には、その値の意味を表すname
属性を付けます。必ず送信用の
<button>
または<input type="submit">
要素を含めます。src/pages/register.astro ------<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"
は、有効なメール形式のみを受け付ける検証を導入します。
src/pages/register.astro ------<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>複数フィールドを参照する独自の検証は、
<script>
タグとConstraint Validation APIで追加できます。より複雑な検証ロジックを簡単に記述するには、フロントエンドフレームワークと、React Hook FormやFelteのようなフォームライブラリを使う方法もあります。
-
フォーム送信によりブラウザーは同じページを再リクエストします。データ転送方式をURLパラメーターではなく
Request
ボディで送るために、フォームのmethod
をPOST
に変更します。src/pages/register.astro ------<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
で囲みます。src/pages/register.astro ---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
オブジェクトに保持し、テンプレートで参照することでクライアントへ返すことができます。src/pages/register.astro ---export const prerender = false; // Not needed in 'server' modeimport { 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>