Open18

フォームライブラリの conform を試す

myttymytty
myttymytty

Conform はプログレッシブエンハンスメントを意識して作られたフォームライブラリ。
その他の特徴は、type-safe なフィールド推論が可能なこと、a11y ヘルパーが用意されていること、そしてServer Actions に対応していること。
この辺りは、試しながら理解していけたらなと思う。

myttymytty
myttymytty

チュートリアルをやってみようか。
チュートリアルは Remix メインで書かれているし、自分好みに変更しつつ実装していこう。

myttymytty

インストール

npm install @conform-to/react @conform-to/zod --save
myttymytty

schema の定義。

import { z } from "zod";

export const schema = z.object({
  userName: z
  .string({ required_error: 'ユーザー名を入力してください'})
  .min(1, { message: 'ユーザー名を入力してください' })
  .min(2, { message: 'ユーザー名は2文字以上にしてください' })
  .max(14, { message: 'ユーザー名は10字以内にしてください' }),
  email: z.string({ required_error: 'メールアドレスを入力してください'}).email({ message: 'メールアドレスの形式で入力してください' }),
})
myttymytty

action の定義。
parseWithZod 関数を利用する。この関数を利用することで、フォームデータを zod のスキーマに従ってパースすることができる。エラーがある場合はエラーを返す。

'use server'
import { parseWithZod } from "@conform-to/zod"
import { schema } from "./schema"
import { redirect } from "next/navigation"

export const userRegister = async (formData: FormData) => {
  const submission = parseWithZod(formData, { schema })

  // パースに失敗した場合は、クライアントに報告
  if (submission.status !== 'success') {
    return submission.reply()
  }

  console.log(submission.value)

  return redirect('/home')
}
myttymytty

あ、userFormState 利用するから以下のように修正する必要があるわ。エラー出てた。
prevState の型は一旦 any で。

'use server'
import { parseWithZod } from "@conform-to/zod"
import { schema } from "./schema"
import { redirect } from "next/navigation"

export const userRegister = async (prevState: any, formData: FormData) => {
  const submission = parseWithZod(formData, { schema })

  // パースに失敗した場合は、クライアントに報告
  if (submission.status !== 'success') {
    return submission.reply()
  }

  console.log(submission.value)

  return redirect('/home')
}
myttymytty

フォームの作成。
フォームメタデータは conform の useForm フックで管理できるみたい。

Form.tsx
'use client'

import { useFormState } from "react-dom"
import { userRegister } from "../actions"
import { useForm } from "@conform-to/react"

export const Form = () => {
  const [lastResult, action] = useFormState(userRegister, undefined)

  const [form, fields] = useForm({
    lastResult,
    // バリデーションのタイミング。入力時にバリデーションを行う
    shouldValidate: 'onInput',
    // リバリデーションのタイミング(送信ボタンを押した後)。入力時にリバリデーションを行う
    shouldRevalidate: 'onInput',
  })

  console.log('field',fields.userName)

  return (
    <form action={action} id={form.id} className="m-4">
      <div>
        <label htmlFor={fields.userName.id}>Username</label>
        <input
          id={fields.userName.id}
          type="text"
          name={fields.userName.name}
          aria-invalid={fields.userName.errors ? true : undefined}
          aria-describedby={fields.userName.errors ? fields.userName.errorId : undefined}
          className="border-2 border-gray-300 rounded-md p-2 w-full"
        />
        <div id={fields.userName.errorId} className="text-red-500">{fields.userName.errors}</div>
      </div>
      <div>
				<label htmlFor={fields.email.id}>Email</label>
				<input
					id={fields.email.id}
					type="email"
					name={fields.email.name}
					aria-invalid={fields.email.errors ? true : undefined}
					aria-describedby={
						fields.email.errors ? fields.email.errorId : undefined
					}
          className="border-2 border-gray-300 rounded-md p-2 w-full"
				/>
				<div id={fields.email.errorId} className="text-red-500">{fields.email.errors}</div>
			</div>

      <button className="px-2.5 py-2 bg-blue-500 rounded-md text-white">送信</button>
    </form>
  )
}

myttymytty

現状、自力で属性を書いているが、getFormProps や getInputProps といったヘルパー関数を利用することで、アクセシビリティ関連の属性を自動で追加することができるみたい。

'use client'

import { useFormState } from "react-dom"
import { userRegister } from "../actions"
import { getFormProps, getInputProps, useForm } from "@conform-to/react"

export const Form = () => {
  const [lastResult, action] = useFormState(userRegister, undefined)

  const [form, fields] = useForm({
    lastResult,
    // バリデーションのタイミング。入力時にバリデーションを行う
    shouldValidate: 'onInput',
    // リバリデーションのタイミング(送信ボタンを押した後)。入力時にリバリデーションを行う
    shouldRevalidate: 'onInput',
  })

  console.log('field',fields.userName)

  return (
    <form {...getFormProps(form)} action={action} className="m-4">
      <div>
        <label htmlFor={fields.userName.id}>Username</label>
        <input
          {...getInputProps(fields.userName, { type: 'text'})}
          className="border-2 border-gray-300 rounded-md p-2 w-full"
        />
        <div id={fields.userName.errorId} className="text-red-500">{fields.userName.errors}</div>
      </div>
      <div>
				<label htmlFor={fields.email.id}>Email</label>
				<input
          {...getInputProps(fields.email, { type: 'email' })}
          className="border-2 border-gray-300 rounded-md p-2 w-full"
				/>
				<div id={fields.email.errorId} className="text-red-500">{fields.email.errors}</div>
			</div>

      <button className="px-2.5 py-2 bg-blue-500 rounded-md text-white">送信</button>
    </form>
  )
}

しっかりアクセシビリティ関連の属性が追加されている。

myttymytty

現状入力フォームに文字を入力するたびにサーバー側にリクエストがいき、サーバー側でバリデーションが実行されている状態なのでよろしくない。また、サーバーでバリデーションを行うと多少の遅延があるという問題もある。
どうやら、クライアントでバリデーションを実行する方法もあるみたい。
onValidate を定義するという方法。
以下のように onValidate を定義することで、送信ボタンを押した場合のみサーバー側に送信することができる。

'use client'

import { useFormState } from "react-dom"
import { userRegister } from "../actions"
import { getFormProps, getInputProps, useForm } from "@conform-to/react"
import { parseWithZod } from "@conform-to/zod"
import { schema } from "../schema"

export const Form = () => {
  const [lastResult, action] = useFormState(userRegister, undefined)

  const [form, fields] = useForm({
    lastResult,
    onValidate({ formData }) {
      return parseWithZod(formData, { schema: schema })
    },
    // バリデーションのタイミング。入力時にバリデーションを行う
    shouldValidate: 'onInput',
    // リバリデーションのタイミング(送信ボタンを押した後)。入力時にリバリデーションを行う
    shouldRevalidate: 'onInput',
  })

  console.log('field',fields.userName)

  return (
    <form {...getFormProps(form)} action={action} onSubmit={form.onSubmit} className="m-4">
      <div>
        <label htmlFor={fields.userName.id}>Username</label>
        <input
          {...getInputProps(fields.userName, { type: 'text'})}
          className="border-2 border-gray-300 rounded-md p-2 w-full"
          defaultValue={'aaa'}
        />
        <div id={fields.userName.errorId} className="text-red-500">{fields.userName.errors}</div>
      </div>
      <div>
				<label htmlFor={fields.email.id}>Email</label>
				<input
          {...getInputProps(fields.email, { type: 'email' })}
          className="border-2 border-gray-300 rounded-md p-2 w-full"
				/>
				<div id={fields.email.errorId} className="text-red-500">{fields.email.errors}</div>
			</div>

      <button className="px-2.5 py-2 bg-blue-500 rounded-md text-white">送信</button>
    </form>
  )
}
myttymytty

チュートリアルは以上でおしまい。

getFormProps と getInputProps を利用することで、 a11y に関連する属性を自動で設定してくれるのめちゃくちゃ良かった。

myttymytty
myttymytty

アクセシビリティ
ヘルパーは以下の6種類存在する

  • getFormProps
  • getFieldsetProps
  • getInputProps
  • getSelectProps
  • getTextareaProps
  • getCollectionProps

上記のヘルパーは、Radix UI などのカスタムUIコンポーネントを使用している場合は、すでに考慮されているので、必要ない。