テックキャンプ卒の弱々エンジニア日記

エンジニアとして働く中での学びをちょっとでも記録していきます。

Yupを用いた相互依存フォームのバリデーション

Yupとは

Yupとは入力値のバリデーションを担当するライブラリです。 入力欄1つひとつにバリデーションルールを定義する作業は、通常であれば面倒なものですが、Yupを使うとスッキリと定義でき とても便利なライブラリなのです!

github.com

相互依存したフォームのバリデーション

さて、このYupを用いて、相互依存するフォームのバリデーションを仕掛けたいと思いました。

GithubIssueやブログ等で調べてみると色々あって、 結局何を使えばいいのかわからなくなったので、 一旦整理してみたいと思います。

github.com

実装の前提と方針

実装の前提

実装の前提としては下記の画像にあるよな 誕生日の月と日をそれぞれいれるようなフォームを作りたいとします。 (普通、年月日の入力であればカレンダーで指定したりするのが普通ですがそこは無視でお願いします笑)

フォームの画像
フォーム

仕様の要件は、

  • 月あるいは日は両方空白または両方選択済みの状態でなければならない
  • 片方だけが入力されている場合、空白のボックスの下にエラー文を出す

といったものです。

いわゆる相互依存したフォームのバリデーションです。

実装方針

結論から言えば、実装するにはwhenメソッドまたはtestメソッドを使う必要があります。 これら2つのメソッドについては、こちらのドキュメントを確認してください。

Doc: https://github.com/jquense/yup#yup

実装方針はざっくり3つあります。

  1. 月と日の両方のフィールドでwhenメソッドを使用した上で、noSortEdgeに依存関係を明示する
  2. 片方をwhenメソッド,もう片方のフィールドではtestメソッドを使うといったパターン
  3. 両方ともtestメソッドを使うパターン
実装1: 月と日の両方のフィールドでwhenメソッドを使用する

この実装はGithubIssueなどで盛んに提案されている方法です。

github.com

今回のパターンだと下記のような実装になります。

const schema = yupJp
  .object()
  .shape(
    {
      birthMonth: yupJp
        .string()
        .label("誕生月")
        .trim()
        .nullable()
        .defined()
        .when("birthDay", ([birthDay], schema) => {
          return birthDay
            ? schema.required("誕生日がある場合は、月が必須です")
            : schema;
        }),
      birthDay: yupJp
        .string()
        .label("誕生日")
        .trim()
        .nullable()
        .defined()
       .when('birthMonth', ([birthMonth], schema) => {
         return birthMonth
           ? schema.required('誕生月がある場合は、日が必須です')
           : schema
       }),
    },
    [["birthDay", "birthMonth"]]
  )
  .required();

実は結構簡単に実装できます。 普通にwhenメソッドをそれぞれ利用して、第二引数に依存関係のフィールド名を入れています。 この第二引数をnoSortEdgeというらしい.....

ちなみに、これについてのドキュメントがほとんどないのは 作者曰く

It's a ugly escape hatch for dealing with cyclical dependencies. といって、それでドキュメントには書かなかったとか.

このパターンは割とメジャーな気がしますが、 noSortEdgeというドキュメントにないようなものを使うかどうかはそれぞれの判断になるのかなといった感じです。

実装2: whenとtestをそれぞれ使う

こちらは実装1と違って、依存関係を明示する必要はありません。

const schema = yupJp
  .object()
  .shape(
    {
      birthMonth: yupJp
        .string()
        .label("誕生月")
        .trim()
        .nullable()
        .defined()
        .when("birthDay", ([birthDay], schema) => {
          return birthDay
            ? schema.required("誕生日がある場合は、月が必須です")
            : schema;
        }),
      birthDay: yupJp
        .string()
        .label("誕生日")
        .trim()
        .nullable()
        .defined()
        .test(
          "birthDay",
          "誕生月がある場合は、日が必須です",
          (value, testContext) => {
            const { birthMonth } = testContext.parent;

            return value !== null || birthMonth === null;
          }
        ),
    }
  )
  .required();

これはこれでいいのですが、 testメソッドではschemaオブジェクトをもってこれないので バリデーションルールの表現のためにコードが長くなりがちというデメリットがあります。

実装3: 両方testを使う

これは上記でも記載しましたが、 バリデーションルールの表現のためにコードが長くなりがちというデメリットがあるので 回避したほうが良さそうです。

結論

個人的な意見としては、実装1がわかりやすかったりしますが、 noSortEdgeが割とマイナーだったりするので無難に実装2のパターンでもいいかも。 動き的にも同じなので、。。。。。