この記事は、2021年に作成し未公開となっていたブログ記事のメタデータのみを書き換えて再掲載するものです。
この記事では、 Vuetify の <v-file-input>
コンポーネントを基に、ファイルサイズの上限設定を追加した <file-input>
コンポーネントを実装していきながら、 <v-form>
のバリデーション機構の理解、 VInput
を継承した入力コンポーネントの拡張を目指します。
TL;DR 完成済みソースコードへのリンク

Vuetify の <v-input>
系コンポーネントでは、 rules prop を利用して入力のバリデーションを行うことが可能ですが、
- 非同期処理を含むバリデーションを実装したい
- 複数の入力コンポーネントをまたいでバリデーションを実装したい
といった場合、 rules では実現が難しいケースが存在するのではないかと思います。
Vuetify のソースコードを読む
まずは <v-form>
のソースコードから、 error
や validate
といったキーワードを中心に探していきます。
手始めに、どのような流れで <v-form>
が子要素のバリデーションを行った結果を v-model
で通知しているかを調べてみましょう。
VForm.ts#L47-L65, #L111-L114
ソースコードの抜粋
export default mixins(/* ... */).extend({
// ...
watch: {
errorBag: {
handler (val) {
const errors = Object.values(val).includes(true)
this.$emit('input', !errors)
},
deep: true,
immediate: true,
},
},
methods: {
watchInput (input: any): Watchers {
const watcher = (input: any): (() => void) => {
return input.$watch('hasError', (val: boolean) => {
this.$set(this.errorBag, input._uid, val)
}, { immediate: true })
}
// ...
},
register (input: VInputInstance) {
this.inputs.push(input)
this.watchers.push(this.watchInput(input))
},
// ...
register()
でthis.inputs
に 入力コンポーネントを登録するwatchInputs()
で input 入力コンポーネントのエラー状態を監視する- input の
hasError
プロパティがエラー状態を表している - エラーがあれば
this.errorBag
に保持する
- input の
watch
で、this.errorBag
が空であればtrue
を、1 つでもエラーがあればfalse
を emit する
<v-form>
のバリデーションが実現されていることがわかりました。次に個々の入力コンポーネントがどのようにエラーを判定しているかを調べてみることにします。
いくつかの入力コンポーネントのソースコードを確認したところ、
VInput
を extends していることvalidatable
mixin を利用していること
がわかり、 hasError
は validatable
mixin (validatable/index.ts#L79-L85) 内で定義されていることがわかりました。
これらを踏まえ、 VInput
を extends し hasError
computed property を override することで、入力コンポーネントにエラーが存在すると判定されるようになるか確かめてみましょう。
App.vue
<template>
<v-app>
<v-main class="ma-8">
<v-form v-model="isValid">
<custom-validation />
</v-form>
<p>
Is form valid? <b :class="isValid || 'red--text'">{{ isValid }}</b>
</p>
</v-main>
</v-app>
</template>
<script lang="ts">
import Vue from 'vue';
import CustomValidation from './components/CustomValidation.vue';
export default Vue.extend({
name: 'App',
components: {
CustomValidation,
},
data() {
return {
isValid: false,
};
},
});
</script>
components/CustomValidation.vue
<template>
<v-text-field v-model="input" />
</template>
<script lang="ts">
import Vue, { PropType } from 'vue';
import VInput from 'vuetify/es5/components/VInput';
export default Vue.extend({
extends: (VInput as unknown) as typeof Vue,
data() {
return { input: '' };
},
computed: {
hasError(): boolean {
return !this.input; // 入力が空であれば true を返す (=入力エラーが存在する)
},
},
});
</script>
hasError() { return !this.input; }
とした結果、入力がなければ error
であると認識されるようになりました。

コンポーネントの作成
ようやくここで独自のバリデーションを実現した入力コンポーネントを実装する準備が整いました 🎉。
あとは、 hasError()
に、選択したファイルの容量が超過していないかチェックするコードを実装するだけです。
最終的には次のようなコードになりました。
App.vue
<template>
<v-app>
<v-main class="ma-8">
<v-form v-model="isValid">
<file-input ref="fileInput" :max-file-size="maxFileSize" />
</v-form>
<p>
Is form valid? <b :class="isValid || 'red--text'">{{ isValid }}</b>
</p>
</v-main>
</v-app>
</template>
<script lang="ts">
import Vue from 'vue';
import FileInput from './components/FileInput.vue';
export default Vue.extend({
name: 'App',
components: {
FileInput,
},
data() {
return {
isValid: false,
maxFileSize: 5 * 1024,
};
},
});
</script>
components/FileInput.vue
<template>
<div>
<v-file-input ref="fileInput" @change="handleFileChange" />
<p>
Current file size is <b>{{ JSON.stringify(fileSize) }}</b> bytes
</p>
<p>
Maximum file size is <b>{{ maxFileSize }}</b> bytes
</p>
<p>
Does my file exceed max file size? <b>{{ hasError }}</b>
</p>
</div>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue';
import { Component } from 'vue/types';
import VInput from 'vuetify/es5/components/VInput';
type FileEventTarget = EventTarget & { files: FileList };
export default Vue.extend({
extends: (VInput as unknown) as typeof Vue,
props: {
maxFileSize: {
type: Number,
required: true,
},
},
data() {
return {
fileSize: null as null | number,
};
},
computed: {
hasError(): boolean {
return (this.fileSize ?? 0) > this.maxFileSize;
},
},
methods: {
handleFileChange(file: File | undefined): void {
if (file) {
this.fileSize = file.size;
} else {
this.fileSize = null;
}
},
},
});
</script>
あとがき
今回の例のような単純なバリデーションであれば、単純に rules を利用するだけで実現できますが、Vuetify の他のコンポーネントを含む、任意のコンポーネントを拡張する際に今回の手法は役に立つのではないかと考えています。
直接 VFileInput を extends しなかったのは、 “Composition over inheritance” を念頭においています。 (場合によっては、直接対象のコンポーネントを extends したほうが良い場合もあるでしょう)
この記事中で紹介した Vuetify のソースコードは、 MIT ライセンス のもと配布されています。
利用したライブラリのバージョン
- Vue: 2.6.11
- Vuetify: 2.4.2