はじめに
こんにちは🧑💻 ラボルのUIデザイナー/フロントエンドエンジニアの寺岡です。
早速ですがフロントエンドのみなさん、フォームのリアルタイムバリデーションってどのように実装されてますでしょうか?
リアルタイムバリデーションとは、入力時やフォーカスアウト時に入力内容のチェックを行い、OKかNGかの結果をリアルタイムに表示する以下のようなフォームのことですね。
「あるフォームの400エラーが著しく多い」などの問題がある場合、UX改善として実装する機会があるかと思います。 というわけで、今回はリアルタイムバリデーションの実装方法についての記事になります!
目次
準備
仕様
今回、仕様は以下のようにしました。
- 概要: 入力するたびに入力値のバリデーションチェックを行い、もし入力値がNGパターンであればエラーを表示する。
- バリデーション:
- hogeフォーム: 入力値が未入力ではないこと
- fugaフォーム: 5文字以上入力されていること
完成イメージ
上記の仕様で実装したものを先にお見せします👀 バリデーションルールがNGだと、エラーメッセージが表示されます。
実装手順
今回はVueで実装しました。 コードの全てが見たい方は、codesandboxで確認してください。
では順番に説明していきます。
1. formの入力値を用意
いつも通り、リアクティブな値を用意します。
後ほど、input
イベント時に実行する処理を書きたいので、v-model
ではなく:value
+@input
の書き方をしています。
// App.vue <template> <div> <label for="hoge"></label> <input id="hoge" type="text" :value="formState.hoge" @input="onInputForm('hoge', $event.target.value)" /> </div> </template> <script setup> import { reactive } from "vue"; // 入力値の用意 const formState = reactive({ hoge: "", }); // input時に入力値を更新 const onInputForm = (id, value) => { formState[id] = value; }; </script>
2. errorMessageをformごとに用意
hoge
に表示する用のエラーメッセージをArrayで用意します。
template内にv-for
でリストレンダリングしていますが、今はまだ何も表示されなくてOKです。
// App.vue <template> <div> <!-- 省略 --> <ul> <li v-for="message in errorMessagesState.hoge" :key="message"> {{ message }} </li> </ul> </div> </template> <script setup> // エラーメッセージをformごとに用意 const errorMessagesState = reactive({ hoge: [], }); </script>
3. 空文字orエラー文のStringを返すFunctionを定義
正しくは関数を返す関数という説明になりますが、中身は入力値を受け取ったらStringを返すバリデーションルール用の関数です。Result型のようなものを返してもいいかもしれません。
あとでVue Component側で呼び出すのでexport
しておきましょう。
// validatorOptions.js export const notBlank = () => { // 入力値がemptyかスペースのみの場合、エラー文を返す return (v) => { if (!v || !v.match(/\S/g)) { return "入力してください。"; } return ""; }; }; // あとで他のバリデーションも追加する想定
4. 各formのバリデーションルールを追加
3で定義したバリデーションルールFunctionをvalidatorsState
という変数内でArrayの中に追加していきます。
hogeのバリデーションルールはnotBlank
のみなので、以下のようになります。
// App.vue <template> <!-- 省略 --> </template> <script setup> import { notBlank } from "./validatorOptions"; const validatorsState = { hoge: [notBlank()], }); </script>
5. errorMessageの更新
2で用意していたerrorMessagesState
を、input
イベント発火時に更新します。
validatorsState.hoge
を.map
でバリデーションチェックを実行し、.filter
でエラーメッセージのStringのみをerrorMessagesState
に格納します。
これで、フォームの入力を空にするとエラーメッセージが表示されるはずです!
// App.vue <template> <!-- 省略 --> </template> <script setup> const onInputForm = (id, value) => { // valueの更新 formState[id] = value; // errorMessageの更新 errorMessagesState[id] = validatorsState[id] .map((validate) => { return validate(value); }) .filter((msg) => msg !== ""); }; </script>
6. エラー時の表示を作る
errorMessagesState[id].length
で、エラーが一つでも出ているかの判定を行い、classをbindさせます。
あとは.input-error
の時に背景を赤色にするなど、わかりやすいエラー用の表示をCSSで作るだけです!
<template> <input :class="[ 'input', { 'input-error': errorMessagesState.hoge.length }, ]" type="text" id="hoge" :value="formState.hoge" @input="onInputForm('hoge', $event.target.value)" /> </template> <script setup> // 省略 </script>
完成!
fugaフォームも同様に実装した結果、コード全体は以下になります。 ちゃんと以下のバリデーションチェックが行われていますね。
- hogeフォーム: 入力値が未入力ではないこと
- fugaフォーム: 5文字以上入力されていること
// App.vue <template> <div class="formGroup"> <label class="formGroup__label" for="hoge">HOGE</label> <input :class="[ 'formGroup__input', { 'formGroup__input-error': errorMessagesState.hoge.length }, ]" type="text" id="hoge" placeholder="なんでもいいので入力" :value="formState.hoge" @input="onInputForm('hoge', $event.target.value)" /> <ul class="formGroup__errorMessages"> <li v-for="message in errorMessagesState.hoge" :key="message"> {{ message }} </li> </ul> </div> <div class="formGroup"> <label class="formGroup__label" for="fuga">FUGA</label> <input :class="[ 'formGroup__input', { 'formGroup__input-error': errorMessagesState.fuga.length }, ]" type="text" id="fuga" placeholder="5文字以上で入力" :value="formState.fuga" @input="onInputForm('fuga', $event.target.value)" /> <ul class="formGroup__errorMessages"> <li v-for="message in errorMessagesState.fuga" :key="message"> {{ message }} </li> </ul> </div> </template> <script setup> import { reactive } from "vue"; import { notBlank, atLeast } from "./validatorOptions"; const formState = reactive({ hoge: "", fuga: "", }); const errorMessagesState = reactive({ hoge: [], fuga: [], }); const validatorsState = ({ hoge: [notBlank()], fuga: [atLeast(5)], }); const onInputForm = (id, value) => { // valueの更新 formState[id] = value; // errorMessageの更新 errorMessagesState[id] = validatorsState[id] .map((validate) => { return validate(value); }) .filter((msg) => msg !== ""); }; </script>
// validatorOptions.js export const notBlank = () => { return (v) => { if (!v || !v.match(/\S/g)) { return "入力してください。"; } return ""; }; }; export const atLeast = (digit) => { return (v) => { if (v.length < digit) { return `${digit}文字以上で入力してください。`; } return ""; }; };
実装ポイントまとめ
今回のようなリアルタイムバリデーションの実装ポイントをまとめます。
1. 高階関数をバリデーションルール定義用のArrayに格納
const validatorsState = ({ id: [notBlank(), atLeast(5)], });
上記のvalidatorsState
に、フォームごとのバリデーションルールをArrayに格納していましたね。
console.log(validatorsState.id)
するとわかりやすいのですが、(2) [ƒ (), ƒ ()]
のように「Functions in Array」の構造になっています。
以下のように、notBlank
は「v
を受け取って何らかの処理を行う無名関数」を返していますよね。
export const notBlank = () => { return (v) => { if (!v || !v.match(/\S/g)) { return "入力してください。"; } return ""; }; };
2. inputイベントで入力のたびにバリデーションチェックを実行
// inputイベント発火のたびにこの処理が動く const onInputForm = (id, value) => { errorMessagesState[id] = validatorsState[id] .map((validate) => { return validate(value); }) .filter((msg) => msg !== ""); };
@input="onInputForm(id, value)"
で、入力するたびに上記のバリデーションチェックの処理が動き、最終的にエラーメッセージをArrayに格納するようにしています。
今回の実装ではバリデーションチェックのタイミングは「inputした時」でしたが、もし「blurした時」に変えたければ、@blur
で発火させるように変更するだけですね。
なぜこの実装方法か
今回はnotBlank()
やatLeast(digit)
など、バリデーションチェック用のロジックをvalidatorOptions.js
にまとめて置いておく設計でした。
このやり方をとった理由をまとめます。
1. バリデーションロジックの再利用性が高い
本来、フォームは複数箇所で使う場合がほとんどだと思います。 会員登録フォーム・ログインフォーム・お問い合わせフォームなど、色々なフォームにリアルタイムバリデーションを実装したいですよね。
共通のjsファイルにバリデーションロジックをまとめておけば、同じロジックを複数コンポーネントで使えるので、再利用性が高いと言えます。
2. テストがしやすい
今回のバリデーションロジックはjsファイルに切り分けされているので、自動テストで完結します。 つまり、いちいちフォームの入力を試す手動テストの手間が省けます。
以下のように、期待通りのエラーメッセージが返ってくるかのテストを書けば完璧ですね。
// validatorOptions.test.js import { notBlank } from "./validatorOptions"; // エラーメッセージ取得 const getErrorMessages = (validators, value) => { return validators .map((validate) => { return validate(value); }) .filter((msg) => msg !== ""); } // notBlank()のテスト test('未入力どうかの判定', () => { const validators = [ notBlank() ]; expect(getErrorMessages(validators, "")).toEqual([ "入力してください。" ]); expect(getErrorMessages(validators, "テスト")).toEqual([]); });
まとめ
リアルタイムバリデーションの実装方法は色々な方法があると思いますが、以下のメリットを感じたい方は、今回紹介した方法がオススメです🍻
- バリデーションロジックの再利用性が高い
- テストがしやすい
また、他にみなさんのオススメの実装方法があれば是非教えてください!!
それでは!👋
最後に
ラボルでは、エンジニアを積極採用中です。1、2年目のエンジニアから経験豊富なテックリードやエンジニリングマネージャーまで、興味がある方はぜひご応募ください!!