Skip to content
sumnail

Vue3のComposition APIを使ったフォームバリデーションの実装

created at : 2025/03/07

Vue3

はじめに

Vue3の仕組みを理解することを目的に、Composition APIを使ってフォームのバリデーションを実装します。

フォーム入力後、バリデーションを行いエラーメッセージが表示されることを目指します。

完成イメージ

Sample Form

実装

バリデーション

入力フォームそれぞれにバリデーションルールを設定し、算出プロパティを使ってバリデーションを行います。

Sample Code
vue
<script setup lang="ts">
import { ref, computed } from "vue";

// form values
const username = ref("");
const email = ref("");

// form validation rules
const usernameRules = [
  (v: string) => !!v || "Username is required",
  (v: string) => v.length >= 3 || "Username must be at least 3 characters",
];
const emailRules = [
  (v: string) => !!v || "Email is required",
  (v: string) => /.+@.+\..+/.test(v) || "Email must be valid",
];

// computed validation
const validatedUsername = computed(() =>
  usernameRules.map((rule) => rule(username.value)),
);
const validatedEmail = computed(() =>
  emailRules.map((rule) => rule(email.value)),
);
</script>

エラーメッセージの表示

バリデーションエラーが発生した場合のメッセージを表示します。
メッセージは、validatedUsernamevalidatedEmailから、最初の文字列を探して取得します。

また、isDirtyUsernameisDirtyEmailで入力フォームの入力値が変更されるまでは、メッセージは表示しないよう制御します。

Sample Code
vue
<template>
  <p
    class="flex w-full justify-center text-3xl font-semibold text-indigo-600 dark:text-indigo-400"
  >
    Sample Form
  </p>
  <form
    class="relative flex h-full w-full flex-col items-center"
    @submit.prevent="handleSubmit"
  >
    <div class="flex h-[110px] w-full max-w-md flex-col">
      <label for="username">Name : </label>
      <input
        type="text"
        name="username"
        v-model="username"
        @blur="updateIsDirtyUsername"
      />
      <div v-if="messageUsername">
        <p class="text-sm text-red-400 dark:text-red-700">
          {{ messageUsername }}
        </p>
      </div>
    </div>
    <div class="flex h-[110px] w-full max-w-md flex-col">
      <label for="email">Email : </label>
      <input
        type="email"
        name="email"
        v-model="email"
        @blur="updateIsDirtyEmail"
      />
      <div v-if="messageEmail">
        <p class="text-sm text-red-400 dark:text-red-700">
          {{ messageEmail }}
        </p>
      </div>
    </div>
  </form>
</template>
vue
<script setup lang="ts">
import { ref, computed } from "vue";

// form values
const username = ref("");
const email = ref("");
const isDirtyUsername = ref(false);
const isDirtyEmail = ref(false);

// form validation rules
const usernameRules = [
  (v: string) => !!v || "Username is required",
  (v: string) => v.length >= 3 || "Username must be at least 3 characters",
];
const emailRules = [
  (v: string) => !!v || "Email is required",
  (v: string) => /.+@.+\..+/.test(v) || "Email must be valid",
];

// computed validation
const validatedUsername = computed(() =>
  usernameRules.map((rule) => rule(username.value)),
);
const validatedEmail = computed(() =>
  emailRules.map((rule) => rule(email.value)),
);

const messageUsername = computed(() => {
  return (
    isDirtyUsername.value &&
    validatedUsername.value.find((v) => typeof v === "string")
  );
});
const messageEmail = computed(() => {
  return (
    isDirtyEmail.value &&
    validatedEmail.value.find((v) => typeof v === "string")
  );
});

const updateIsDirtyUsername = () => (isDirtyUsername.value = true);
const updateIsDirtyEmail = () => (isDirtyEmail.value = true);
</script>

フォームの提出

提出前にバリデーションのチェックを行い、問題ない場合にフォームを提出します。
提出処理中は、isPendingを使いローディングを表示します。

提出ボタンは、isValidfalseの場合は無効にしています。

Sample Code
vue
<template>
  <p
    class="flex w-full justify-center text-3xl font-semibold text-indigo-600 dark:text-indigo-400"
  >
    Sample Form
  </p>
  <form
    class="relative flex h-full w-full flex-col items-center"
    @submit.prevent="handleSubmit"
  >
    <div class="flex h-[110px] w-full max-w-md flex-col">
      <label for="username">Name : </label>
      <input
        type="text"
        name="username"
        v-model="username"
        @blur="updateIsDirtyUsername"
      />
      <div v-if="messageUsername">
        <p class="text-sm text-red-400 dark:text-red-700">
          {{ messageUsername }}
        </p>
      </div>
    </div>
    <div class="flex h-[110px] w-full max-w-md flex-col">
      <label for="email">Email : </label>
      <input
        type="email"
        name="email"
        v-model="email"
        @blur="updateIsDirtyEmail"
      />
      <div v-if="messageEmail">
        <p class="text-sm text-red-400 dark:text-red-700">
          {{ messageEmail }}
        </p>
      </div>
    </div>
    <div class="flex justify-center gap-2">
      <button type="button" @click="resetForm">Reset</button>
      <button type="submit" :disabled="!isValid">Submit</button>
    </div>
    <div
      v-if="isPending"
      class="loading absolute flex h-full w-full flex-col items-center justify-center"
    >
      <div class="spinner"></div>
      <p class="text-md">Loading...</p>
    </div>
  </form>
</template>
vue
<script setup lang="ts">
import { ref, computed } from "vue";

// form values
const username = ref("");
const email = ref("");
const isDirtyUsername = ref(false);
const isDirtyEmail = ref(false);
const isPending = ref(false);

// form validation rules
const usernameRules = [
  (v: string) => !!v || "Username is required",
  (v: string) => v.length >= 3 || "Username must be at least 3 characters",
];
const emailRules = [
  (v: string) => !!v || "Email is required",
  (v: string) => /.+@.+\..+/.test(v) || "Email must be valid",
];

// computed validation
const validatedUsername = computed(() =>
  usernameRules.map((rule) => rule(username.value)),
);
const validatedEmail = computed(() =>
  emailRules.map((rule) => rule(email.value)),
);

const messageUsername = computed(() => {
  return (
    isDirtyUsername.value &&
    validatedUsername.value.find((v) => typeof v === "string")
  );
});

const messageEmail = computed(() => {
  return (
    isDirtyEmail.value &&
    validatedEmail.value.find((v) => typeof v === "string")
  );
});

const isValid = computed(() => {
  return (
    isDirtyUsername.value &&
    isDirtyEmail.value &&
    validatedUsername.value.every((v) => v === true) &&
    validatedEmail.value.every((v) => v === true)
  );
});

const updateIsDirtyUsername = () => (isDirtyUsername.value = true);
const updateIsDirtyEmail = () => (isDirtyEmail.value = true);

const resetForm = () => {
  username.value = "";
  email.value = "";
  isDirtyUsername.value = false;
  isDirtyEmail.value = false;
};

const handleSubmit = async (e: Event) => {
  isPending.value = true;
  // 提出処理の待機を再現
  await new Promise((resolve) => setTimeout(resolve, 1000));

  if (isValid.value) {
    alert("Form is submitted");
    resetForm();
  } else {
    alert("Form is invalid");
  }

  isPending.value = false;
};
</script>
vue
<style scoped>
input {
  border: 1px solid gray;
  border-radius: 4px;
  padding: 8px;
}

button {
  border: 1px solid gray;
  border-radius: 4px;
  padding: 8px;
}

button:disabled {
  border: 1px solid gray;
  background-color: gray;
  cursor: not-allowed;
  color: white;
}

.loading {
  background-color: rgba(255, 255, 255, 0.8);
  z-index: 10;
}

.spinner {
  border: 4px solid rgba(0, 0, 0, 0.1);
  border-left-color: #3498db;
  border-radius: 50%;
  height: 50px;
  width: 50px;
  animation: spin 1s linear infinite;
}

.dark {
  .loading {
    background-color: rgba(0, 0, 0, 0.8);
  }
}
</style>

まとめ

Vue3のComposition APIを使って、フォームのバリデーションとエラーメッセージの表示を行いました。

バリデーションのルールやエラーメッセージの表示タイミングなど細かな部分を調整することで、より使いやすいフォームを作成することができます。

また、ライブラリーを使うと煩雑になりがちなバリデーション処理をシンプルに実装できるので、ぜひ検討してみてください。

関連記事