Appearance
はじめに
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>
エラーメッセージの表示
バリデーションエラーが発生した場合のメッセージを表示します。
メッセージは、validatedUsername
とvalidatedEmail
から、最初の文字列を探して取得します。
また、isDirtyUsername
とisDirtyEmail
で入力フォームの入力値が変更されるまでは、メッセージは表示しないよう制御します。
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
を使いローディングを表示します。
提出ボタンは、isValid
がfalse
の場合は無効にしています。
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を使って、フォームのバリデーションとエラーメッセージの表示を行いました。
バリデーションのルールやエラーメッセージの表示タイミングなど細かな部分を調整することで、より使いやすいフォームを作成することができます。
また、ライブラリーを使うと煩雑になりがちなバリデーション処理をシンプルに実装できるので、ぜひ検討してみてください。