はじめに
Vuetifyを使用していて、後述のユースケースがあり、バリデーションのタイミングの制御方法を調査したので記事にします。
ユースケース
- テキストを入力して、フォーカスアウトした時、以下を実行する
- APIでデータを取得する
- バリデーションで入力したテキストがAPIで取得したデータの中にあるか確認する(なければエラーとする)
問題点
順番としては、APIでデータを取得した後、バリデーションが実行されることが理想なのですが、下記のコードでは、以下の二つのイベントが同時に実行されるので必ずバリデーションエラーになります。
-
@Blur="handleBlur"
(データを取得するためのフォーカスアウトのイベント) -
:rules="textRules"
(バリデーション)
詳細に書くと以下のようになります
- TextFieldにapple(appleはhandleBlurで取得するデータリストの中に含まれるデータ)を入力し、フォーカスアウト
- handleBlurが先に実行、その後、handleBlurの終了を待たずにrulesが実行
- handleBlurでデータを取得中に、rulesでバリデーションされて、データがないのでエラーになる
- handleBlurが完了し、appleを含むデータが取得される
<template>
<v-app>
<v-container>
<v-text-field
v-model="msg"
@blur="handleBlur"
:rules="textRules"
validate-on="lazy blur"
/>
<!-- ログ用のテキスト -->
<div style="white-space: pre-wrap; word-wrap: break-word">
{{ msgListLabel }}
</div>
</v-container>
</v-app>
</template>
<script setup>
import { ref, computed } from 'vue'
// TextFieldのモデル
const msg = ref('')
// バリデーション
const textRules = [
(v) => {
msgList.value.push("rules start");
if(dataList.value.includes(v)){
return true
}
return "入力したフルーツはありません";
}
];
// 入力できるデータリスト(fetchDataで取得する)
const dataList = ref([]);
// ログとして表示するようの文字配列
const msgList = ref([])
const msgListLabel = computed(
() => msgList.value.join('\n')
)
// TextFieldからフォーカスアウトした時のイベント
const handleBlur = async () => {
msgList.value.push("handleBlur Start");
const data = await fetchData();
dataList.value = data;
msgList.value.push("fetched data");
msgList.value.push("handleBlur End");
}
// 擬似的なAPIからデータを取得する処理
const fetchData = async () => {
await sleep(1000);
return ["apple", "banana", "cherry"]
};
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
}
</script>
デモ
改善
改善ポイントの解説は下記です
- v-text-fieldのバリデーションのタイミングvalidate-onをlazy submitに変更して、フォーカスアウト時はバリデーションしないようにする
- v-text-fieldにrefを渡しておく
- handleBlurの中で、refを使ってバリデーションを実行する
<template>
<v-app>
<v-container>
<v-text-field
+ ref="textFieldRef"
v-model="msg"
@blur="handleBlur"
:rules="textRules"
- validate-on="lazy blur"
+ validate-on="lazy submit"
/>
<!-- ログ用のテキスト -->
<div style="white-space: pre-wrap; word-wrap: break-word">
{{ msgListLabel }}
</div>
</v-container>
</v-app>
</template>
<script setup>
import { ref, computed } from 'vue'
+ const textFieldRef = ref()
// TextFieldのモデル
const msg = ref('')
// バリデーション
const textRules = [
(v) => {
msgList.value.push("rules start");
if(dataList.value.includes(v)){
return true
}
return "入力したフルーツはありません";
}
];
// 入力できるデータリスト(fetchDataで取得する)
const dataList = ref([]);
// ログとして表示するようの文字配列
const msgList = ref([])
const msgListLabel = computed(
() => msgList.value.join('\n')
)
// TextFieldからフォーカスアウトした時のイベント
const handleBlur = async () => {
msgList.value.push("handleBlur Start");
const data = await fetchData();
dataList.value = data;
msgList.value.push("fetched data");
+ textFieldRef.value?.validate()
msgList.value.push("handleBlur End");
}
// 擬似的なAPIからデータを取得する処理
const fetchData = async () => {
await sleep(1000);
return ["apple", "banana", "cherry"]
};
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
}
</script>
改良版