The DUploadAvatar component provides a complete avatar upload experience with image preview, file selection, and customizable upload button. It handles both new uploads and existing images.
Props
The source URL of the previously uploaded avatar image. Displayed as the initial preview.
The size of the upload/change button
buttonColor
ButtonColor
default:"neutral"
The color variant of the upload/change button
Events
Emitted when an image file is selected for upload. Receives the File object as parameter.
Usage
Basic Avatar Upload
<template>
<DUploadAvatar @on-upload-image="handleUpload" />
</template>
<script setup>
function handleUpload(file: File) {
console.log('File selected:', file.name)
// Upload logic here
}
</script>
With Existing Avatar
<template>
<DUploadAvatar
:src="user.avatarUrl"
@on-upload-image="handleUpload"
/>
</template>
<script setup>
const user = ref({
avatarUrl: '/images/user-avatar.jpg'
})
function handleUpload(file: File) {
// Upload to server and update user.avatarUrl
}
</script>
Custom Button Styling
<template>
<DUploadAvatar
button-size="md"
button-color="primary"
@on-upload-image="handleUpload"
/>
</template>
Complete Profile Form
<template>
<form @submit.prevent="handleSubmit">
<div class="space-y-6">
<div>
<label class="block text-sm font-medium mb-2">
Profile Picture
</label>
<DUploadAvatar
:src="formData.avatar"
@on-upload-image="handleAvatarUpload"
/>
</div>
<UFormField label="Name" name="name">
<UInput v-model="formData.name" />
</UFormField>
<DActionButtons
primary-button-text="Save Profile"
secondary-button-text="Cancel"
@on-click-primary-button="handleSubmit"
/>
</div>
</form>
</template>
<script setup>
const formData = reactive({
avatar: '/images/default-avatar.jpg',
name: ''
})
function handleAvatarUpload(file: File) {
// Create object URL for immediate preview
formData.avatar = URL.createObjectURL(file)
// Upload to server
uploadToServer(file)
}
function uploadToServer(file: File) {
const formData = new FormData()
formData.append('avatar', file)
// API call
fetch('/api/upload-avatar', {
method: 'POST',
body: formData
})
}
</script>
With Loading State
<template>
<div class="relative">
<DUploadAvatar
:src="avatarUrl"
:button-color="isUploading ? 'neutral' : 'primary'"
@on-upload-image="handleUpload"
/>
<div v-if="isUploading" class="absolute inset-0 bg-black/50 flex items-center justify-center rounded-md">
<UIcon name="i-lucide-loader-2" class="animate-spin text-white" />
</div>
</div>
</template>
<script setup>
const isUploading = ref(false)
const avatarUrl = ref('')
async function handleUpload(file: File) {
isUploading.value = true
try {
const url = await uploadFile(file)
avatarUrl.value = url
} finally {
isUploading.value = false
}
}
</script>
With File Validation
<template>
<div>
<DUploadAvatar @on-upload-image="handleUpload" />
<p v-if="error" class="text-error text-sm mt-2">{{ error }}</p>
</div>
</template>
<script setup>
const error = ref('')
function handleUpload(file: File) {
error.value = ''
// Validate file size (2MB max)
if (file.size > 2 * 1024 * 1024) {
error.value = 'File size must be less than 2MB'
return
}
// Validate file type
if (!file.type.startsWith('image/')) {
error.value = 'File must be an image'
return
}
// Proceed with upload
uploadFile(file)
}
</script>
Multiple Upload Formats
<template>
<div class="grid grid-cols-2 gap-4">
<div>
<h3 class="text-sm font-medium mb-2">Profile Photo</h3>
<DUploadAvatar
:src="photos.profile"
button-size="sm"
@on-upload-image="(file) => handleUpload(file, 'profile')"
/>
</div>
<div>
<h3 class="text-sm font-medium mb-2">Cover Photo</h3>
<DUploadAvatar
:src="photos.cover"
button-size="sm"
@on-upload-image="(file) => handleUpload(file, 'cover')"
/>
</div>
</div>
</template>
<script setup>
const photos = reactive({
profile: '',
cover: ''
})
function handleUpload(file: File, type: 'profile' | 'cover') {
photos[type] = URL.createObjectURL(file)
// Upload logic
}
</script>
Implementation Details
Image Preview
The component uses computed property to determine which image to show:
const getImage = computed(() => {
if (fileName.value) {
// Show newly selected file
const imgUrl = globalThis.URL.createObjectURL(fileName.value)
return imgUrl
} else if (props.src) {
// Show existing image
return props.src
}
return null
})
File Input Handling
- Hidden file input with
type="file" and accept="image/*"
- Triggered programmatically via template ref
- Handles both regular file selection and drag-and-drop events
Default State
When no image is available:
- Displays a placeholder with photo icon
- Clickable area triggers file selection
- Primary color background (
bg-primary-50)
Styling
- Avatar Preview:
size-20 (80px) rounded square with object-cover
- Button Position: Aligned to bottom with
items-end
- Layout: Horizontal flex with
gap-x-6
- Button Text: “Cambiar” (Change in Spanish)
Type Exports
export type ButtonSize = InstanceType<typeof UButton>['$props']['size']
export type ButtonColor = InstanceType<typeof UButton>['$props']['color']
Source: /home/daytona/workspace/source/app/components/d/upload/d-upload-avatar.vue:73