見通しを改善！Vue.js でのバリデーション処理のリファクタリング

# 課題

フロントエンド開発において、フォームのバリデーションと入力欄の管理は一般的な作業です。
特に、多くの入力欄を持つフォームでは、コードが複雑になり、メンテナンスが困難になることがあります。

今回の課題は、複雑なフォームのコードをリファクタリングして、より管理しやすく、再利用可能なコードにすることです。

元々のコードは機能していましたが、見通しの悪さが目立ち、特にスキーマの部分とuseFieldの部分で同じ名称が度々出てくること、そしてuseFieldを何行も書かなければならない点が問題でした。

# 考えた方法

この課題に対処するために、以下の方法を考えました。

1. useFieldの部分をスキーマ定義を使って簡略化する。
1. 共通の処理を外部に定義して、どのページでも使えるようにする。
1. スキーマ設定を外部ファイルに定義する。

# 前提条件
まず最初に今回リファクタリング対象となっているコードや環境について記述しておきます。

## リファクタリングするコード
```javascript
<template>
  <div class="container">
    <form class="mt-4">
      <div class="mb-3">
        <label for="testName" class="form-label">名前</label>
        <input type="text" class="form-control" v-model="testName" />
        <small v-if="basicInfoErrors.testName" class="text-danger">{{
          basicInfoErrors.testName
        }}</small>
      </div>
      <!-- 以下省略 -->
    </form>
  </div>
</template>

<script setup>
import { useField, useForm } from "vee-validate";
import * as yup from "yup";

// 日付の形式をチェックするカスタムルール
yup.addMethod(yup.string, "datetime", function () {
  return this.test({
    name: "datetime",
    message: ({ label }) => {
      return `${label}はYYYY/MM/DD HH:MM:SS形式で入力してください。`;
    },
    test: (value) => {
      return value.match(/^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/);
    },
  });
});

/* 他のカスタムルールは省略 */

// バリデーション設定
const basicInfoSchema = yup.object({
  // 名前(必須、100文字以内)
  testName: yup.string().required().max(100).label("名前"),
  // 実施期間(必須、YYYY/MM/DD HH:MM:SS形式、開始日と終了日の関係チェック)
  testPeriodFrom: yup.string().required().datetime().label("開始日時"),
  testPeriodTo: yup
    .string()
    .required()
    .datetime() //　カスタムルール
    .startEnd(yup.ref("testPeriodFrom"))　//　カスタムルール
    .label("終了日時"),
// 以下、他の項目の設定
});

// 初期値設定
const initialValues = {
  testName: "サンプル",
// 以下、他の項目の設定
};

// フォーム設定
const { errors: basicInfoErrors, meta: basicInfoMeta } = useForm({
  validationSchema: basicInfoSchema,
  initialValues,
});

// フィールド設定
const { value: testName } = useField("testName");
const { value: testPeriodFrom } = useField("testPeriodFrom");
const { value: testPeriodTo } = useField("testPeriodTo");
// 以下、他の項目の設定
</script>

```

## 言語・フレームワーク
Vue.js: メインのフレームワークとして Vue.js を使用しています。
Nuxt3: サイト構築には Nuxt3 を使用。プラグインの作成などに関連する部分があります。
## ライブラリ
vee-validate: フォームバリデーションに vee-validate ライブラリを使用しています。
yup: スキーマバリデーションに yup ライブラリを使用。カスタムバリデーションメソッドの追加なども行っています。

# 手順・実装内容

## useFieldの部分の簡略化

まず、useFieldの部分を下記のように変更しました。
shemaのキーでループをさせて、そのキーを元にuseFieldをする事で、重複した記述を回避しています。
```javascript
const formItems = new Map();
Object.keys(schema).forEach(fieldName => {
  const {value} = useField(fieldName);
  formItems.set(fieldName, value);
});
```

これによってtemplate側のmodelの指定部分も下記のようにformItemsから取得するように変更しています。

```vue
<input
  type="text"
  class="form-control"
  v-model="formItems.get('testName').value"
/>
```

## 共通の処理の外部化

次に、先ほど簡略化をした部分など他のページでも使える共通の処理を切り出しました。
今回はNuxt3を使っているのでプラグインとして切り出しています。

```javascript
import {defineNuxtPlugin} from '#app';
import * as yup from 'yup';
import {useField, useForm} from 'vee-validate';

// 日付の形式をチェックする
yup.addMethod(yup.string, 'datetime', function () {
  return this.test({
    name: 'datetime',
    message: ({label}) => {
      return `${label}はYYYY/MM/DD HH:MM:SS形式で入力してください。`;
    },
    test: value => {
      return value.match(/^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/);
    },
  });
});

/* 他のカスタムルールの部分は省略 */

export default defineNuxtPlugin(nuxtApp => {
  return {
    provide: {
      yup: (schema, initialValues) => {
        const {errors: basicInfoErrors, meta: basicInfoMeta} = useForm({
          validationSchema: yup.object(schema),
          initialValues,
        });

        const formItems = new Map();

        Object.keys(schema).forEach(fieldName => {
          const {value} = useField(fieldName);
          formItems.set(fieldName, value);
        });

        return {
          formItems,
          basicInfoErrors,
          basicInfoMeta,
        };
      },
    },
  };
});

```

## スキーマ設定と初期値設定の外部ファイル化

最後にスキーマ設定を外部ファイルに定義しました（ここでは/validates/test.js）。
（本当は、json形式に変換をしたかったのですが、断念・・・）

```javascript
import * as yup from 'yup';

export const schema = {
  // テストの名前(必須、100文字以内)
  testName: yup.string().required().max(100).label('テストの名前'),
  // テストの実施期間(必須、YYYY/MM/DD HH:MM:SS形式)
  testPeriodFrom: yup.string().required().datetime().label('テストの開始日時'),
  testPeriodTo: yup.string().required().datetime().startEnd(yup.ref('testPeriodFrom')).label('テストの終了日時'),
};

export const initialValues = {
  testName: 'サンプル'
};

```

呼び出し側は下記のようにして、これを先ほどのプラグインに渡してあげればOKです。

```javascript
import {schema, initialValues} from '/validates/test.js';
```

## 最終的なコード

リファクタリング後のコードは以下の通りです。スッキリしましたね！

```vue
<template>
  <!-- 省略 -->
</template>

<script setup>
import {schema, initialValues} from '/validates/test.js';

const {$yup} = useNuxtApp();
const {formItems, basicInfoErrors, basicInfoMeta} = $yup(schema, initialValues);
</script>
```

# まとめ

このリファクタリングによって、script部分の可読性が向上し、プラグイン化によって他のページでも同様の効果を得ることができるようになりました。
今後の開発では、このようなアプローチを取ることで、効率的な開発が期待できると思います。

リファクタリングは単にコードを綺麗にするだけではなく、将来の変更に対する柔軟性を高め、チーム全体の生産性を向上させる重要なプロセスです。今回の経験は、その価値を改めて認識する良い機会でした。

もっと効率良くできるよ！とか、ここおかしくない？といったことがあればコメントでお知らせください！