お問い合わせ

ブログ

これまでに経験してきたプロジェクトで気になる技術の情報を紹介していきます。

vee-validateをvue3 composition apiで使用する

okuda Okuda 2 years

vee-validate for composition api

vee-validateをvue3のcomposition apiで使用してみました。
composition apiはVue3 compositon api よく使うところ一覧を参照してください。
説明はコメントで書いています。
翻訳にvue-i18nを使用しています。
バリデーション自体はyupで統一しています。

間違いや、もっと良い方法があればコメント欄で教えてくれたら嬉しいです!

install

npm i vee-validate@next --save
npm i yup --save

sample code

composition apiのためのvee-validateはコメント内で説明
コードが長くなっているので「composableパターン」できれいにする必要あり

<template>
  <div>
    <h1>vee-validation for composition api</h1>
    <div>{{ t("hello") }}</div>
    <!-- すべてのエラーを表示してみる -->
    <div>{{ errors }}</div>

    <!-- フォームはそのまま使える -->
    <form @submit="onSubmit">
      <!-- テキストのバリデーション -->
      <h2 class="header2">name</h2>
      <input type="text" v-model="name" />
      <!-- バリデーションエラー -->
      <div class="validation-notice">{{ errors.name }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ name }}</pre>
      <hr />

      <!-- emailのバリデーション -->
      <h2 class="header2">email</h2>
      <input type="text" v-model="email" />
      <!-- バリデーションエラー -->
      <div class="validation-notice">{{ errors.email }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ email }}</pre>
      <hr />

      <!-- パスワードのバリデーション -->
      <h2 class="header2">password</h2>
      <input type="password" v-model="password" />
      <input type="password" v-model="confirmation" />
      <!-- バリデーションエラー -->
      <div class="validation-notice">{{ errors.password }}</div>
      <div class="validation-notice">{{ errors.confirmation }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code"> {{ password }} : {{ confirmation }}</pre>
      <hr />

      <h2 class="header2">message</h2>
      <textarea v-model="message" />
      <!-- バリデーションエラー -->
      <div class="validation-notice">{{ errors.message }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ message }}</pre>
      <hr />

      <!-- 数値のバリデーション -->
      <h2 class="header2">age</h2>
      <input type="number" v-model="age" />
      <!-- バリデーションエラー -->
      <div class="validation-notice">{{ errors.age }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ age }}</pre>
      <hr />

      <!-- シングルチェックボックスのバリデーション -->
      <h2 class="header2">is_active</h2>
      <input type="checkbox" v-model="is_active" />
      <!-- バリデーションエラー -->
      <div class="validation-notice">{{ errors.is_active }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ is_active }}</pre>
      <hr />

      <!-- シングルチェックボックスのバリデーション コンポネントを使う場合 -->
      <h2 class="header2">is_public</h2>
      <Toggle v-model="is_public">
        <template v-slot:on>{{ $t("public") }}</template>
        <template v-slot:off>{{ $t("private") }}</template>
      </Toggle>
      <!-- バリデーションエラー -->
      <div class="validation-notice">{{ errors.is_public }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ is_public }}</pre>
      <hr />

      <!-- セレクトのバリデーション -->
      <h2 class="header2">item</h2>
      <select v-model="item">
        <option :value="null">---</option>
        <option value="coffee">Coffee</option>
        <option value="tea">Tea</option>
        <option value="coke">Coke</option>
      </select>
      <!-- バリデーションエラー -->
      <div class="validation-notice">{{ errors.item }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ item }}</pre>
      <hr />

      <!-- ラジオボタンのバリデーション -->
      <h2 class="header2">gender</h2>
      <label><input type="radio" value="male" v-model="gender" />male</label>
      <label><input type="radio" value="female" v-model="gender" />female</label>
      <label><input type="radio" value="other" v-model="gender" />other</label>
      <!-- バリデーションエラー -->
      <div class="validation-notice">{{ errors.gender }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ gender }}</pre>
      <hr />

      <!-- チェックボックスのバリデーション -->
      <h2 class="header2">categories</h2>
      <label><input type="checkbox" value="111" v-model="categories" />category1</label>
      <label><input type="checkbox" value="222" v-model="categories" />category2</label>
      <label><input type="checkbox" value="33333" v-model="categories" />category3</label>
      <!-- バリデーションエラー 項目をまとめて表示 -->
      <div v-for="(error, index) in errors" :key="index" class="validation-notice">
        <span v-if="index.startsWith('categories')">{{ error }}</span>
      </div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ categories }}</pre>
      <hr />

      <!-- 配列のバリデーション -->
      <h2 class="header2">tags</h2>
      <input v-model="tags[0]" type="text" />
      <!-- バリデーションエラー オブジェクトごとに -->
      <div class="validation-notice">{{ errors["tags[0]"] }}</div>
      <input v-model="tags[1]" type="text" />
      <!-- バリデーションエラー オブジェクトごとに -->
      <div class="validation-notice">{{ errors["tags[1]"] }}</div>
      <!-- バリデーションエラー オブジェクト全体 -->
      <div class="validation-notice">{{ errors.tags }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ tags }}</pre>
      <hr />

      <!-- オブジェクトのバリデーション -->
      <h2 class="header2">sites</h2>
      <input v-model="sites.twitter" type="url" />
      <!-- バリデーションエラー オブジェクトごとに -->
      <div class="validation-notice">{{ errors["sites.twitter"] }}</div>
      <input v-model="sites.github" type="url" />
      <!-- バリデーションエラー オブジェクトごとに -->
      <div class="validation-notice">{{ errors["sites.github"] }}</div>
      <!-- バリデーションエラー オブジェクト全体 -->
      <div class="validation-notice">{{ errors.sites }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ sites }}</pre>
      <hr />

      <!-- 増減可能な配列のバリデーション -->
      <h2 class="header2">products</h2>
      <div v-for="(product, index) in products" :key="product.key">
        <Field :name="`products[${index}]`" type="text" />
        <button type="button" @click="productRemove(index)">Remove</button>
        <!-- バリデーションエラー 配列ごとに -->
        <div class="validation-notice">{{ errors[`products[${index}]`] }}</div>
      </div>
      <button type="button" @click="productPush('')">Add</button>
      <!-- バリデーションエラー 配列全体 -->
      <div class="validation-notice">{{ errors.products }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ products }}</pre>
      <hr />

      <!-- 増減可能な配列の中のオブジェクトのバリデーション -->
      <h2 class="header2">users</h2>
      <div v-for="(user, index) in users" :key="user.key">
        <Field :name="`users[${index}].name`" type="text" />
        <Field :name="`users[${index}].age`" type="number" />
        <label>
          <Field :name="`users[${index}].gender`" type="radio" value="male" />male
        </label>
        <label>
          <Field :name="`users[${index}].gender`" type="radio" value="female" />female
        </label>
        <label>
          <Field :name="`users[${index}].gender`" type="radio" value="other" />other
        </label>
        <button type="button" @click="userRemove(index)">Remove</button>
        <!-- バリデーションエラー 配列のキーごとに -->
        <div class="validation-notice">{{ errors[`users[${index}].name`] }}</div>
        <div class="validation-notice">{{ errors[`users[${index}].age`] }}</div>
        <div class="validation-notice">{{ errors[`users[${index}].gender`] }}</div>
      </div>
      <button type="button" @click="userPush({ name: '', age: null, gender: '' })">
        Add
      </button>
      <!-- バリデーションエラー 配列全体 -->
      <div class="validation-notice">{{ errors.users }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ users }}</pre>
      <hr />

      <h2 class="header1">image</h2>
      <!-- <Field name="image" type="file" /> -->
      <input
        type="file"
        name="image"
        @change="imageChange($event), showPreview(imagePreviews, $event)"
      />
      <!-- バリデーションエラー 配列全体 -->
      <div class="validation-notice">{{ errors.image }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ image }}</pre>

      <figure class="preview">
        <img
          v-for="(imagePreview, index) of imagePreviews"
          :key="index"
          class="preview__image"
          :src="imagePreview"
        />
      </figure>
      <hr />

      <h2 class="header1">photos</h2>
      <input
        type="file"
        multiple
        name="photos"
        @change="photosChange, showPreview(photoPreviews, $event)"
      />
      <!-- バリデーションエラー 配列全体 -->
      <div class="validation-notice">{{ errors.photos }}</div>
      <!-- 値を表示 -->
      <pre class="validation-code">{{ photos }}</pre>

      <figure class="preview">
        <img
          v-for="(photoPreview, index) of photoPreviews"
          :key="index"
          class="preview__image"
          :src="photoPreview"
        />
      </figure>
      <hr />

      <!-- バリデーションをパスしたかチェック -->
      <div v-show="meta.valid" class="validation-success">
        <FAIcon :icon="['far', 'check-circle']" size="sm" />
      </div>

      <div v-show="!meta.valid" class="validation-notice">
        <FAIcon :icon="['far', 'times-circle']" size="sm" />
      </div>

      <!-- 送信ボタン 送信中は非アクティブ化 -->
      <button :disabled="isSubmitting">
        送信 <span>{{ submitCount }}</span>
      </button>
      <hr />

      <div>
        <!-- フォームを触ったかチェック -->
        <p>
          touched: {{ meta.touched }}
          <span v-show="meta.touched"><FAIcon icon="check-circle" size="sm" /></span>
        </p>
        <!-- フォームを編集したかチェック -->
        <p>
          dirty: {{ meta.dirty }}
          <span v-show="meta.dirty"><FAIcon icon="check-circle" size="sm" /></span>
        </p>
        <p>
          <!-- バリデーションにパスしたかチェック -->
          valid: {{ meta.valid }}
          <span v-show="meta.valid"><FAIcon icon="check-circle" size="sm" /></span>
        </p>
      </div>
      <hr />

      <!-- デフォルトの値を表示 -->
      <h2 class="header2">initialValues</h2>
      <pre class="validation-code">initialValues: {{ meta.initialValues }}</pre>
      <hr />

      <!-- meta全体を表示 -->
      <h2 class="header2">meta</h2>
      <pre class="validation-code">{{ meta }}</pre>
    </form>
  </div>
</template>

<script>
// Field: 増減可能な配列を作成するときに使う
// useFieldArray: 増減可能な配列を作成するときに使う
// useField: バリデーションを行うフィールドを割り当てる
// useForm: バリデーションスキーマを割り当てる
// configure: vee-validationの設定を変更する
import { Field, useFieldArray, useField, useForm, configure } from "vee-validate";
// ここではバリデーションをyupで統一する
import * as yup from "yup";
// ここでは翻訳をvue-i18nに統一する
import { useI18n } from "vue-i18n";
import { ref, reactive } from "vue";

// インポートした「configure」を使用してトリガするイベントを変更
configure({
  validateOnBlur: true, // blurイベントで検証をトリガーする必要がある場合、デフォルトはtrue
  validateOnChange: true, // changeイベントで検証をトリガーする必要がある場合、デフォルトはtrue
  validateOnInput: true, // inputイベントで検証をトリガーする必要がある場合、デフォルトはfalse
  validateOnModelUpdate: true, // update:modelValue(v-model)イベントで検証をトリガーする必要がある場合、デフォルトはtrue
});

export default {
  name: "Test4",
  components: {
    // 増減可能な配列を作成するときにテンプレート内で使用する
    Field,
  },
  // セットアップ関数
  setup(props, context) {
    // vue i18n を使用するので「t」関数を呼び出す
    const { t } = useI18n();

    // validation schema
    // ここですべてのフィールドを「yup」を使用してバリデーションするスキーマを作成
    // 詳しい付き方は -> https://github.com/jquense/yup#readme
    const schema = yup.object({
      name: yup
        .string()
        .label(t("name"))
        .required("${label is a required field.")
        .min(1, t("${label} must be at least ${min} characters."))
        .max(20, t("${label} must be at most ${max} characters.")),
      email: yup
        .string()
        .label(t("email"))
        .required("${label} is a required field.")
        .email("${label} must be a valid email."),
      password: yup
        .string()
        .label(t("password"))
        .required("${label} is a required field.")
        .min(8, t("${label} must be at least ${min} characters.")),
      confirmation: yup
        .string()
        .label(t("confirmation"))
        .oneOf([yup.ref("password")], "${label} must match with password."),
      message: yup
        .string()
        .label(t("message"))
        .max(100, t("${label} must be at least ${max} characters.")),
      age: yup
        .number()
        .label(t("age"))
        .typeError("must specify a number.")
        .min(5)
        .max(10),
      is_active: yup
        .boolean()
        .label(t("active"))
        .oneOf([true], t("${label} must be checked.")),
      item: yup
        .string()
        .label(t("item"))
        .oneOf(
          ["coffee", "tea"],
          t("${label} must be one of the following values: ${values}.")
        ),
      gender: yup
        .string()
        .label(t("gender"))
        .oneOf(
          ["male", "female"],
          t("${label} must be one of the following values: ${values}.")
        ),
      categories: yup
        .array()
        .label(t("categories"))
        .length(2, "${label} must have ${length} items.")
        .of(
          yup
            .string()
            .label(t("category"))
            .max(3, "${label} must be at most ${max} characters.")
        ),
      tags: yup
        .array()
        .label(t("tags"))
        .length(2, "${label} must have ${length} items!")
        .of(
          yup
            .string()
            .label(t("tag"))
            .max(3, "${label} must be at most ${max} characters.")
        ),
      sites: yup
        .object()
        .typeError("must specify a object.")
        .shape({
          twitter: yup.string().label(t("twitter")).url("${label} must be a valid URL."),
          github: yup.string().label(t("github")).url("${label} must be a valid URL."),
        }),
      products: yup
        .array()
        .label(t("products"))
        .length(3, "${label} must have ${length} items.")
        .of(yup.string().label(t("product")).url("${label} must be a valid URL.")),
      users: yup
        .array()
        .label(t("users"))
        .max(5, "${label} field must have less than or equal to ${max} items.")
        .of(
          yup.object().shape({
            name: yup.string().label(t("name")).required("${label} is a required field."),
            age: yup
              .number()
              .label(t("age"))
              .typeError("must specify a number.")
              .min(5)
              .max(10),
            gender: yup
              .string()
              .label("gender")
              .required("${label} is a required field."),
          })
        ),
      image: yup
        .array()
        .label(t("image"))
        .nullable()
        .test("file required", "${label} is a required field.", (files) => {
          if (files) {
            return files.length >= 1;
          }
        })
        .test("file type", "${label} is unsupported file format.", (files) => {
          let valid = true;
          if (files) {
            files.map((file) => {
              if (!["image/gif", "image/jpeg", "image/png"].includes(file.type)) {
                valid = false;
              }
            });
          }
          return valid;
        })
        .test("file size", "${label} file size is too large.", (files) => {
          let valid = true;
          if (files) {
            files.map((file) => {
              const size = file.size / 1024 / 1024; // change to Gb
              if (size > 1) {
                valid = false;
              }
            });
          }
          return valid;
        }),
      photos: yup
        .array()
        .label(t("photos"))
        .nullable()
        .test("file required", "${label} is a required field.", (files) => {
          if (files) {
            return files.length >= 1;
          }
        })
        .test("file type", "${label} is unsupported file format.", (files) => {
          let valid = true;
          if (files) {
            files.map((file) => {
              if (!["image/gif", "image/jpeg", "image/png"].includes(file.type)) {
                valid = false;
              }
            });
          }
          return valid;
        })
        .test("file size", "${label} file size is too large.", (files) => {
          let valid = true;
          if (files) {
            files.map((file) => {
              const size = file.size / 1024 / 1024; // change to Gb
              if (size > 1) {
                valid = false;
              }
            });
          }
          return valid;
        }),
    });

    // initial values
    // 各フィールドのデフォルト値を設定する
    const initial_values = {
      name: "asdfg",
      email: "asdf@asdf.asdf",
      is_active: false,
      is_public: false,
      item: "coffee",
      gender: "",
      message: "",
      age: null,
      categories: [],
      tags: [],
      sites: {
        twitter: "",
        github: "",
      },
      products: ["http://asdf.com", "http://asdf.com", "http://asdf.com"],
      users: [{ name: "", age: null, gender: "" }],
    };

    // initial errors
    // 各フィールドのデフォルトエラーを設定する
    const initial_errors = {
      name: "required!!",
      email: "required!!",
      password: "required!!",
    };

    // validation
    // ここで作成したバリデーションスキーマ(validationSchema)と、デフォルトの値(initialValues)と、デフォルトのエラー(initialErrors)を「useForm」にわたす
    // バリデーションの結果を「errors」で受け取る
    // 「meta」には以下が含まれる
    // dirty: 編集したか
    // pending: 検証待ち
    // touched: 触ったか
    // valid: すべてパスしたか
    // initialValue: デフォルト値
    // 「handleSubmit」はサブミットのファンクションに使用する
    // 「isSubmitting」で送信中を検知する
    // 「submitCount」で送信した回数を取得
    const { errors, meta, handleSubmit, isSubmitting, submitCount } = useForm({
      validationSchema: schema,
      initialValues: initial_values,
      initialErrors: initial_errors,
    });

    // フィールドを割り当てて、「value」をそれぞれの変数に代入する
    const { value: name } = useField("name");
    const { value: email } = useField("email");
    const { value: password } = useField("password");
    const { value: confirmation } = useField("confirmation");
    const { value: message } = useField("message");
    const { value: age } = useField("age");
    const { value: is_active } = useField("is_active");
    const { value: is_public } = useField("is_public");
    const { value: item } = useField("item");
    const { value: gender } = useField("gender");
    const { value: categories } = useField("categories");
    const { value: tags } = useField("tags");
    const { value: sites } = useField("sites");
    // 増減可能なフィールドは「useFieldArray」で割り当てて、remove関数とpush関数も受け取る
    const { remove: productRemove, push: productPush, fields: products } = useFieldArray(
      "products"
    );
    const { remove: userRemove, push: userPush, fields: users } = useFieldArray("users");
    const { value: image, handleChange: imageChange } = useField("image");
    const { value: photos, handleChange: photosChange } = useField("photos");

    // image and photo previews
    const imagePreviews = reactive([]);
    const photoPreviews = reactive([]);
    const showPreview = (previews, event) => {
      console.log(previews);
      if (event.target.files) {
        Object.values(event.target.files).forEach((file) => {
          // create preview
          const fileReader = new FileReader();
          fileReader.onload = () => {
            previews.push(fileReader.result);
          };
          fileReader.readAsDataURL(file);
        });
      }
    };

    // サブミットメソッド
    const onSubmit = handleSubmit(async (values, actions) => {
      // test check values
      // 中身を確認
      console.log(values);
      // 色々使える関数を確認
      console.log(actions);

      // test dummy api
      // apiの代わりに3秒ウェイトさせる
      await new Promise((resolve) => setTimeout(resolve, 3000));

      // test validation error
      // サーバ側でバリデーションエラーがでたことにする
      const status = 422;

      // reset form on success
      // 成功した場合は「actions.resetForm()」でフォームをクリアする
      // 「actions.resetForm()」すると設定したデフォルト値に戻る
      if (status === 200) {
        // reset form
        actions.resetForm();
      }

      // サーバ側でバリデーションに引っかかった場合
      if (status === 422) {
        // サーバから受け取ったエラー全体を「actions.setErrors([エラーオブジェクト])」でセットする
        // setFieldErrorと併用する場合はこちらを先に設定
        actions.setErrors({ password: ["The password is too short"] });

        // サーバから受け取ったエラーフィールドごとに「actions.setFieldError([キー]: [メッセージ])」でセットする
        actions.setFieldError("email", "This email is already taken! -> from server");
      }
    });

    // テンプレートで使用するものを返す
    return {
      // i18nをテンプレートでも使用する場合
      t,
      // 各フィールドの値
      name,
      email,
      password,
      confirmation,
      message,
      age,
      is_active,
      is_public,
      item,
      gender,
      categories,
      tags,
      sites,
      productRemove, // 配列を削除する関数う
      productPush, // 配列を追加する関数う
      products,
      userRemove, // 配列を削除する関数う
      userPush, // 配列を追加する関数う
      users,
      imageChange,
      image,
      photosChange,
      photos,
      // useFormからの返り値
      errors,
      meta,
      isSubmitting,
      submitCount,
      onSubmit,
      // other methods
      showPreview,
      imagePreviews,
      photoPreviews,
    };
  },
};
</script>

<style scoped>
.validation-notice {
  color: var(--color-caution);
}
.validation-success {
  color: var(--color-success);
}
.validation-code {
  color: var(--color-back);
  background-color: var(--color-dark);
  padding: 0.5rem 1rem;
  border-radius: 3px;
}
.preview {
  display: flex;
  align-items: center;
}
.preview__image {
  height: 10rem;
}
</style>
vee-validateをvue3 composition apiで使用する 2022-01-07 17:24:56

コメントはありません。

4829

お気軽に
お問い合わせください。

お問い合わせ
gomibako@aska-ltd.jp