はじめに
画面のデザイン的に、Vuetify3のデフォルトのアップロードボタンでなく、いかにもボタンボタンしてる アップロードボタンが作りたくなりました。Vuetify2での例はありましたが Vuetify3での例がなかったのでそのままコピペできず、ちょっと調べることになりました。
開発環境
- Nuxt 3.0.0-rc.1
- Vuetify 3.0.0-beta.1
ほしいイメージ
これじゃなくて
これがほしい
結果
過程をいっぱい書いても 興味ない方も居ると思うので出来上がったものをまず置いときます
1: ボタンがある
2: クリックでクルクルアニメーション+選択ダイアログ
3: 選択後ファイル名が出る + emitされる
-: ドラッグアンドドロップもできる
<template>
<v-btn
@dragover.prevent="isDragged = true"
@dragleave.prevent="isDragged = false"
@drop.prevent="onFileDropped"
width="100%"
height="100%"
color="info"
class="text-none"
:variant="!isDragged ? 'contained' : 'outlined'"
depressed
@click="openFileSelect"
>
<v-progress-circular
v-if="isSelecting"
class="mx-12 my-6"
indeterminate
color="white"
/>
<v-row v-if="!isSelecting" class="text-center" justify="center">
<v-col class="mt-2 mb-n2" cols="10">
<v-icon size="40">
{{ props.buttonIcon }}
</v-icon>
</v-col>
<v-col class="text-truncate mb-2 mt-n2" cols="10">
{{ displayText }}
</v-col>
</v-row>
</v-btn>
<input
ref="uploader"
class="d-none"
type="file"
:accept="props.accept"
@change="onFileSelectChange"
>
</template>
<script setup lang="ts">
import {
mdiCloudUpload
} from '@mdi/js'
interface Props {
buttonTitle?: string,
buttonIcon?: string,
accept?: string
}
const props = withDefaults(defineProps<Props>(), {
buttonTitle: "ファイルを選択",
buttonIcon: mdiCloudUpload,
accept: "image/*"
})
interface Emits {
(e: "update:File", value: File): void;
}
const emits = defineEmits<Emits>()
const isSelecting = ref<boolean>(false)
const isDragged = ref<boolean>(false)
const selectedFile = ref<File | null>(null)
const displayText = computed(() => {
switch (true) {
case isDragged.value:
return "離すとアップロード"
case selectedFile.value != null:
return selectedFile.value!.name
default:
return props.buttonTitle
}
})
// ボタンクリックでファイル選択を開く
const uploader = ref<HTMLInputElement>()
const openFileSelect= () => {
isSelecting.value = true
window.addEventListener('focus', () => {
isSelecting.value = false
}, { once: true })
uploader.value?.click()
}
// ファイル選択確定時にEmit
const onFileSelectChange = (e: Event) => {
const target = e.target as HTMLInputElement
const files = target.files
const file = files![0]
selectedFile.value = file
emits("update:File", file)
}
// D&D時にEmit
const onFileDropped = (e: DragEvent) => {
isDragged.value = false
if (!e) {
return
}
if (!e.dataTransfer) {
return
}
if (e.dataTransfer.files.length === 0) {
return
}
const file = e.dataTransfer.files[0]
selectedFile.value = file
emits("update:File", file)
}
</script>
作ってみる
過程に興味ある人向け (様々な資料を参考にさせていただきました)
1: 既存のファイルアップロードボタンを見てみる
Googleで Vuetify file upload button
と検索すると Vuetify2の例が出てくる
どうやらv-btnが押されたら refsを経由して input type="file" の要素をクリックするという
処理をするということがわかった(そんなことできるのか...)
2: Vue3 の refsの仕様を調べる
Vue2でrefsは使った覚えがあったが Vue3のrefsの使い方がよくわからなかったのでCompositionAPIの記事を参考にさせていただいた
そして型についてはなんやかんや困ったが
どうやらHTMLInputElement
型を取ってくればいいということがわかった
Eventに関しては 正しい使い方がちょっとわからなかったが as で捻じ曲げて e.targetを HTMLInputElement として扱って とりあえずtypescript文法上は動くようにした
// ファイル選択確定時にEmit
const onFileSelectChange = (e: Event) => {
const target = e.target as HTMLInputElement
const files = target.files
const file = files![0]
selectedFile.value = file
emits("update:File", file)
}
3: Vue3.2 Setupでの Props/Emitを調べる
Nuxt3 RCすげ~!って始めたので 実はVue3.2自体知らない、よってこれも調べた。とても参考にさせていただいた。
4: ドラッグアンドドロップに対応させる
できればドラッグアンドドロップに対応させたいよねってことでこちらも参考にさせて頂いた。こちらはほぼそのままで動かすことができた。
5: ドラッグアンドドロップ時にボタンをアウトラインにする
Vuetify2の v-btn は outlined
をつけるとアウトラインになったが Vuetify3 では variant=outlined
を指定するようになっていた。これに気づかず若干ハマった。
6: サイズ調整
そのままだと絶妙に残念なサイズをしていたので、親要素にサイズを任せることにして
width="100%"
と height="100%"
をつけたら いい感じになった