Chapter 07

入力フォーム

Thirosue
Thirosue
2021.08.28に更新

Webアプリでは、入力フォームの管理、入力バリデーションなどの要素は必ずと言っていいほど必要になってきます。
TypeScript(フォームの型定義)+hookでも対応はできますが、React Hook Formを利用することで、より省力かつ高パフォーマンスなフォームを構築できます。

https://react-hook-form.com/jp/

React Hook Form単体でもHTML標準のバリデーションを適応できますが、スキーマバリデーションライブラリのyupを併用することで、項目間の関連チェックなども柔軟に対応できるため、サンプルではyupとReact Hook Formを併用しています。

https://react-hook-form.com/jp/get-started#Applyvalidation

React Hook Form 使い方

公式の動画を見るのが一番理解が早いです。

https://youtu.be/DN8v7_RbVlc

フォーム実装

公式のサンプルを参考にフォームを実装していきます。
以下サンプルでは、パスワード変更ダイアログの実装になります。

components/page/password-dialog.tsx
// 入力フォームの型定義
type FormValues = {
  password: string
  confirmPassword: string
}

export const PasswordDialog = ({
  onSubmit,
  onClose,
  onCancel,
}: {
  onSubmit: () => void
  onClose: (event: any) => void
  onCancel: (event: any) => void
}): JSX.Element => {
 // カスタムフックを定義
  const {
    register,
    handleSubmit,
    formState: { errors }, // エラーオブジェクト
  } = useForm<FormValues>({
    resolver: yupResolver(schema),
    defaultValues: { // 入力のデフォルト値
      password: 'Password1?',
      confirmPassword: 'Password1?',
    },
  })

  const doSubmit = (data: FormValues): void => {
    captains.log(data)
  }

  return (
    <>
      <Progress processing={mutation.isLoading} />
      <Confirm
        title={'パスワード変更'}
        onSubmit={handleSubmit(doSubmit)} <!-- フォームサブミット -->
        onClose={onClose}
        onCancel={onCancel}
        processing={mutation.isLoading}
      >
        <form className="px-6 mt-4 mb-4 w-full">
          <label className="block">
            <FormLabel>新しいパスワード</FormLabel>
            <input
              id="password"
              type={TextFieldType.Password}
              className={`mt-1 w-full border-gray-300 block rounded-md focus:border-indigo-600 ${
                errors.password ? 'border-red-400' : ''
              }`} <!-- バリデーションエラー時に入力フィールドの枠を赤くする -->
              {...register('password')}
            />
	    <!-- バリデーションエラー表示 -->
            <FormErrorMessage>{errors.password?.message}</FormErrorMessage>
          </label>

          <label className="block mt-5">
            <FormLabel>新しいパスワード(確認用)</FormLabel>
            <input
              id="confirmPassword"
              type={TextFieldType.Password}
              className={`mt-1 w-full border-gray-300 block rounded-md focus:border-indigo-600 ${
                errors.confirmPassword ? 'border-red-400' : ''
              }`}
              {...register('confirmPassword')}
            />
            <FormErrorMessage>
              {errors.confirmPassword?.message}
            </FormErrorMessage>
          </label>
        </form>
      </Confirm>
    </>
  )
}

スキーマバリデーション

公式のサンプルを参照にスキーマバリデーションを実施します。

https://react-hook-form.com/jp/get-started#SchemaValidation
components/page/password-dialog.tsx
// フォームのスキーマ定義
const schema = yup.object().shape({
  password: yup
    .string()
    .required('入力してください')
    .matches(
      /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/,
      'アルファベット(大文字小文字混在)と数字と特殊記号を組み合わせて8文字以上で入力してください'
    ),
  confirmPassword: yup
    .string()
    .required('入力してください')
    .matches(
      /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/,
      'アルファベット(大文字小文字混在)と数字と特殊記号を組み合わせて8文字以上で入力してください'
    )
    .oneOf([yup.ref('password'), null], '確認用パスワードが一致していません'),
})

export const PasswordDialog = ({
  onSubmit,
  onClose,
  onCancel,
}: {
  onSubmit: () => void
  onClose: (event: any) => void
  onCancel: (event: any) => void
}): JSX.Element => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({
    resolver: yupResolver(schema), // yupResolverを利用し、スキーマ定義を読み込む
    defaultValues: {
      password: 'Password1?',
      confirmPassword: 'Password1?',
    },
  })

項目間バリデーション

パスワードと確認用のパスワードが一致していることを確認します。
oneOfとyup.ref(passwordを参照)を併用し、入力値が一致していることを確認します。

components/page/password-dialog.tsx
const schema = yup.object().shape({
  password: yup
    .string()
    .required('入力してください')
    .matches(
      /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/,
      'アルファベット(大文字小文字混在)と数字と特殊記号を組み合わせて8文字以上で入力してください'
    ),
  confirmPassword: yup
    .string()
    .required('入力してください')
    .matches(
      /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/,
      'アルファベット(大文字小文字混在)と数字と特殊記号を組み合わせて8文字以上で入力してください'
    )
    .oneOf([yup.ref('password'), null], '確認用パスワードが一致していません'), // oneOfとrefで一致していることを確認
})

その他

最初は、Material-UITextField同様のコンポーネントを用意しようと考えましたが、利用に一手間かかるため、input要素に直接バインドするようにしました。

https://dev.classmethod.jp/articles/mui-with-rhf-v7/