Torihaji's Growth Diary

Little by little, no hurry.

Nextjs x RHF x Zodを使って フォームを作ってみる part3

はじめに

いよいよ終盤です。

今回はフォームを追加できるようなフォームを作成していきます。

例えば追加ってボタンを押したらフォームが追加されるみたいな。

まずは名前とメールアドレス入力できたらいいでしょう。

理想はこんな感じ。

https://dev.classmethod.jp/articles/react-hook-form-use-field-array/

それではltgです

続き

ということでgithubです

GitHub - torihazi/next_form

せっかくなのでページ変えましょう。

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通信が絡んだり項目が多かったり、初期値に前もって入れていたりと

そういう情報が加わるので複雑ですが頑張りたいです。

何はともあれよかったです。

これにて一旦旅は終了です。お疲れ様でした。