remix + conform で複数フォームの送信
はじめに
こんにちは、hirotooooです。
Remixのformライブラリでconformが良いと聞いたので、使ってみたら
- フォームの送信のやり方
-
Each child in a list should have a unique "key" prop.
の発生
など、色々詰付いた部分があるので、備忘録的な感じで最終的にどうしたかについて説明していきます。
最終的な動作:
コード解説
schema
const schema = z.object({
products: z.array(
z
.string({ required_error: "入力は必須です" })
.min(2, "2文字以上入力してください")
),
});
schemaを定義しています。
action
export const action = async ({ request, params }: ActionFunctionArgs) => {
try {
const formData = await request.formData();
const intent = formData.get("intent");
switch (intent) {
case "create": {
console.log("create");
const index = parseInt(formData.get("index") as string);
const product = formData.get("product") as string;
// ...処理
break;
}
case "update": {
console.log("update");
const index = parseInt(formData.get("index") as string);
const product = formData.get("product") as string;
// ...処理
break;
}
case "delete": {
console.log("delete");
const product = formData.get("product");
// ...処理
break;
}
}
return new Response("完了", { status: 200 });
} catch (error) {
return new Response(`エラー ${error}`, {
status: 500,
});
}
};
ここでは、actionに送信後の処理を書いています。
formの内容にintentを設定し、intentによって処理を分岐しています。
この中で、dbへの登録・削除・更新等を行います。
conform
const submit = useSubmit();
const [form, fields] = useForm({
defaultValue: sampleData,
onValidate: ({ formData }) => parseWithZod(formData, { schema }),
shouldValidate: "onBlur",
shouldRevalidate: "onInput",
});
const products = fields.products.getFieldList();
formの設定を行います。
- defaultValue:初期データの設定
- onValidate:zodからバリデーション項目を設定
- shouldValidate/shouldRevalidate:バリデーションのタイミングを設定
const products = fields.products.getFieldList();
とすることで、productsでmapが使えるようになります。
useSubmit
を使用して、handleでsubmit送信を行うようにしています。
handle
追加
function handleBlurUpdateProducts(e: React.ChangeEvent<HTMLInputElement>) {
const formElement = e.currentTarget.form;
if (!formElement) {
alert("formがnullです。");
return;
}
const formData = new FormData(formElement);
const submission = parseWithZod(formData, { schema });
if (submission.status !== "success") {
return submission.reply();
}
const updateProduct = e.currentTarget.value;
const index = products.findIndex(
(product) => product.value === updateProduct
);
const isNewProduct = index + 1 > sampleData.products.length;
sampleData.products[index] = updateProduct;
formData.append("index", index.toString());
formData.append("product", updateProduct);
formData.append("intent", isNewProduct ? "create" : "update");
console.log(`sampleData: ${JSON.stringify(sampleData.products)}`);
submit(formData, { method: "POST" });
}
追加ようのhandleです。Blurでフォーカスが外されたときに実行されます。
const formElement = e.currentTarget.form;
で、formElementを取得しています。formが存在するか確認するために使用します。
const updateProduct = e.currentTarget.value;
で、変更された値だけを取得しています。
const index = products.findIndex( (product) => product.value === updateProduct );
では、配列の位置を特定しています。この値で、新しい値か更新する値かを判断したり、配列の更新する部分を特定します。
sampleData.products[index] = updateProduct;
は、サンプルデータを更新するためのコードです。
削除
function handleClickDeleteProduct(product: string) {
const formData = new FormData();
formData.append("intent", "delete");
formData.append("product", product);
sampleData.products = sampleData.products.filter((p) => p !== product);
submit(formData, { method: "POST" });
}
これは、削除するFormを送信するhandleです。
sampleData.products = sampleData.products.filter((p) => p !== product);
は、サンプルデータを削除するためのコードです。
Form
return (
<form
method="POST"
{...getFormProps(form)}
className="h-screen w-full grid justify-center items-center"
>
...
</form>
);
<form method="POST" {...getFormProps(form)}>
の{…getFormProps(form)}でformのプロパティを設定しています。
詳しくは以下のガイドラインをご覧下さい。
リスト部分
<Card className="col-span-1 space-y-4 lg:col-span-3">
...
<CardContent className="grid gap-2">
{products.map((product, index) => (
<div key={product.key}>
<div className="flex gap-2">
<Input
placeholder="入力"
defaultValue={product.value!}
onBlur={handleBlurUpdateProducts}
{...getInputProps(product, { type: "text" })}
key={product.key}
/>
{/* ダミーボタン:Enterキーを押したときに、他のボタンのform送信を防止するため。 */}
<button
type="submit"
name="dummy"
className="hidden"
disabled
/>
<button
onClick={() => handleClickDeleteProduct(product.value!)}
{...form.remove.getButtonProps({
name: fields.products.name,
index: index,
})}
className="w-9 h-9 flex cursor-pointer items-center justify-center rounded-full p-2 hover:bg-slate-100"
>
<TrashIcon className="text-red-500" />
</button>
</div>
<p className="text-red-500 text-sm">
{products[index].errors
? products[index].errors![0]
: fields.products.errors
? fields.products.errors[0]
: ""}
</p>
</div>
))}
</CardContent>
...
</Card>
{products.map((product, index) => (
は、conformで設定していたconst products = fields.products.getFieldList();
を使用しています。
<div key={product.key}>
を設定しないと、Key Props Errorが起こるので、注意。
<Input
placeholder="入力"
defaultValue={product.value!}
onBlur={handleBlurUpdateProducts}
{...getInputProps(product, { type: "text" })}
key={product.key}
/>
ここのonBlurでhandleBlurUpdateProducts
を呼び出しています。
{...getInputProps(product, { type: "text" })}
は、Inputのプロパティを設定しています。
key={product.key}
の設定は、Key Props Error防止のためです。
<button
onClick={() => handleClickDeleteProduct(product.value!)}
{...form.remove.getButtonProps({
name: fields.products.name,
index: index,
})}
className="w-9 h-9 flex cursor-pointer items-center justify-center rounded-full p-2 hover:bg-slate-100"
>
<TrashIcon className="text-red-500" />
</button>
削除ボタンです。onClickでhandleClickDeleteProduct
を呼び出しています。
{...form.remove.getButtonProps({name: fields.products.name, index: index,})}
では、buttonをクリックすると、動的に削除されるプロパティを設定しています。
<p className="text-red-500 text-sm">
{products[index].errors
? products[index].errors![0]
: fields.products.errors
? fields.products.errors[0]
: ""}
</p>
バリデーションエラーを表示する部分です。
リストの追加
<CardFooter>
<Button
variant="outline"
{...form.insert.getButtonProps({
name: fields.products.name,
})}
>
+ 追加
</Button>
</CardFooter>
追加するボタンです。
{...form.insert.getButtonProps({name: fields.products.name,})}
では、ボタンをクリックするとproductsにフィールドが追加されるプロパティを設定しています。
終わりに
この記事では、Remix + conformを使ったフォーム送信の例を紹介しました。
conformを使えば、動的なフォーム操作ができます。参考になれば幸いです。
Discussion