はじめに
いよいよ終盤です。
今回はフォームを追加できるようなフォームを作成していきます。
例えば追加ってボタンを押したらフォームが追加されるみたいな。
まずは名前とメールアドレス入力できたらいいでしょう。
理想はこんな感じ。
https://dev.classmethod.jp/articles/react-hook-form-use-field-array/
それではltgです
続き
ということでgithubです
せっかくなのでページ変えましょう。
pagesディレクトリの下にaddableForm.tsxみたいにして。
まずはスキーマ宣言して適当に体裁整えていきます。
nameとemailを配列で持つようにするのでこんな感じ。
const addableFormSchema = z.object({
profile: z.array(z.object({
name: z.string().min(1,"必須"),
email: z.string().min(1,"必須").email("形式が不正")
}))
})
type addableFormSchema = z.infer<typeof addableFormSchema>
でuseFormを使います。
export default function AddableForm() {
const { control, handleSubmit, reset } = useForm<addableFormSchema>({
mode: "onChange",
resolver: zodResolver(addableFormSchema),
});
const result = useFieldArray({
control: control,
name: "profile",
});
return <>{console.log(result)}</>;
こんな感じ。新しくuseFieldArrayを使います。
controlは自分もうまく言語化できませんが、
設定することで親元のformへ接続することが可能になるもので
nameはスキーマで宣言した配列の名前を設定し、これは必須です。
では http://localhost:3000/addableForm にとんで中身を見てみましょう。

何やら色々はいっているobjectみたいですね。
とりあえず直近の実務で使いそうなものはappend、fields、removeでしょうか。
appendは末尾にフォームを追加で
fieldsはそのFormに設定する配列そのもののデータで
removeはそのフォームを削除するみたいですね。
ということでそれらを取り出してまずはフォームと追加ボタンと送信ボタンを作ってみましょう。
できたのがこちら。

コードがこちら。
export default function AddableForm() {
const { register, control, handleSubmit } = useForm<addableFormSchemaType>({
mode: "onChange",
resolver: zodResolver(addableFormSchema),
defaultValues: { profile: [{ name: "", email: "" }] },
});
const { fields, append, remove } = useFieldArray({
control: control,
name: "profile",
});
const onSubmit: SubmitHandler<addableFormSchemaType> = (
data: addableFormSchemaType,
e?: BaseSyntheticEvent
) => console.log(data);
const onError: SubmitErrorHandler<addableFormSchemaType> = (
errors: FieldErrors<addableFormSchemaType>,
e?: BaseSyntheticEvent
) => console.log(errors);
return (
<div className="flex items-center justify-center h-screen">
<form
onSubmit={handleSubmit(onSubmit, onError)}
className="flex flex-col gap-1"
>
{fields.map((field, index) => (
<div key={index} className="flex items-center gap-2 last:mb-3">
<input
{...register(`profile.${index}.name`)}
placeholder="名前"
className="border p-1"
/>
<input
{...register(`profile.${index}.email`)}
placeholder="email"
className="border p-1"
/>
<button
type="button"
onClick={() => append({ name: "", email: "" })}
className="border hover:bg-slate-300 transition-all p-1"
>
追加
</button>
<button
type="button"
onClick={() => remove(index)}
className="border hover:bg-slate-300 p-1"
>
削除
</button>
</div>
))}
<div className="flex flex-col w-full gap-2">
<button
type="submit"
className="border hover:bg-blue-400 transition-all"
>
送信
</button>
</div>
</form>
</div>
);
}
試しに3つに増やして、何も入力せずに送信して、errorsを見てみると

と出てきていました。
一方で全部正常に入力して送信し、中身を見ると


OKそうですね。
というわけで、またエラーをUIに表示するために修正します。
このエラー表示にデザインにてこづりました。grid使ってます。
flexでやるとどうにも片方しかエラーない時高さズレるので。

<div className="flex items-center justify-center h-screen">
<form
onSubmit={handleSubmit(onSubmit, onError)}
className="flex flex-col gap-1"
>
{fields.map((field, index) => (
<div key={index} className="last:mb-3">
<div className="grid grid-cols-12 gap-2">
<input
{...register(`profile.${index}.name`)}
placeholder="名前"
className="border p-1 col-span-5"
/>
<input
{...register(`profile.${index}.email`)}
placeholder="email"
className="border p-1 col-span-5"
/>
<button
type="button"
onClick={() => append({ name: "", email: "" })}
className="border hover:bg-slate-300 transition-all p-1 col-span-1"
>
追加
</button>
<button
type="button"
onClick={() => remove(index)}
className="border hover:bg-slate-300 transition-all p-1 col-span-1"
>
削除
</button>
</div>
<div className="grid grid-cols-12 gap-2">
{errors.profile?.[index]?.name && (
<span className="text-red-500 text-sm col-span-5">
{errors.profile[index].name.message}
</span>
)}
{errors.profile?.[index]?.email && (
<span className="text-red-500 text-sm col-span-5">
{errors.profile[index].email.message}
</span>
)}
</div>
</div>
))}
<div className="flex flex-col w-full gap-2">
<button
type="submit"
className="border hover:bg-blue-400 transition-all"
>
送信
</button>
</div>
</form>
</div>
最後に今何件データがあるのかというのとエラーがあったら送信ボタンが押せないようにしましょう。
前者は{fields.length}でいけそうですね。
後者は formStateのisValidを新たに取り出す必要がありそうです。
ということで修正。

<div className="flex items-center justify-center h-screen">
<form
onSubmit={handleSubmit(onSubmit, onError)}
className="flex flex-col gap-1"
>
{fields.map((field, index) => (
<div key={index} className="last:mb-3">
<div className="grid grid-cols-12 gap-2">
<input
{...register(`profile.${index}.name`)}
placeholder="名前"
className="border p-1 col-span-5"
/>
<input
{...register(`profile.${index}.email`)}
placeholder="email"
className="border p-1 col-span-5"
/>
<button
type="button"
onClick={() => append({ name: "", email: "" })}
className="border hover:bg-slate-300 transition-all p-1 col-span-1"
>
追加
</button>
<button
type="button"
onClick={() => remove(index)}
disabled={fields.length === 1}
className={`border p-1 col-span-1 ${
fields.length === 1
? "bg-slate-300 cursor-not-allowed"
: "hover:bg-slate-300 transition-all"
}`}
>
削除
</button>
</div>
<div className="grid grid-cols-12 gap-2">
{errors.profile?.[index]?.name && (
<span className="text-red-500 text-sm col-span-5">
{errors.profile[index].name.message}
</span>
)}
{errors.profile?.[index]?.email && (
<span className="text-red-500 text-sm col-span-5">
{errors.profile[index].email.message}
</span>
)}
</div>
</div>
))}
<div className="flex flex-col w-full gap-2">
<button
type="submit"
className={`border ${
!isValid
? "bg-slate-400 cursor-not-allowed"
: "hover:bg-blue-400 transition-all"
}`}
disabled={!isValid}
>
送信 {`${fields.length}件`}
</button>
</div>
</form>
</div>
正しい時も 機能していそうですね


ということで動的なフォームができました。
※ただ今気づきましたが、このフォームにはUIにおいてバグというかよろしくない 挙動を示します。見つけたらうまい具合に修正して教えてください。ぱくります。
終わりに
ということで1日でなんとか動的なフォームを作り切ることができました。
最初はzodとは?動的なフォームとは?というところからでしたが
react-hook-formについてもzodについても多少なりとも理解が進んだのでよかったです。
多分明日実務でこんな感じのフォームを作るので今日中にしれてよかったです。
実務はこれにapi通信が絡んだり項目が多かったり、初期値に前もって入れていたりと
そういう情報が加わるので複雑ですが頑張りたいです。
何はともあれよかったです。
これにて一旦旅は終了です。お疲れ様でした。