The useLiveUpload composable provides a Vue-friendly API for handling Phoenix LiveView file uploads. It manages the required DOM elements, upload state, and integrates seamlessly with LiveView’s upload system.
useLiveUpload
function useLiveUpload (
uploadConfig : MaybeRefOrGetter < UploadConfig >,
options : UploadOptions
) : UseLiveUploadReturn
Parameters
uploadConfig
MaybeRefOrGetter<UploadConfig>
required
Reactive reference to the upload configuration from LiveView (typically () => props.upload). The UploadConfig is created on the server using allow_upload/3 in your LiveView.
Configuration options for the upload Optional event name for file validation. This event is sent when files are selected, allowing the server to validate files before upload starts.
Required event name for upload submission. This event is sent when files are ready to be uploaded (for manual uploads) or when the upload form is submitted.
Returns
Reactive list of current entries coming from the server patch. Each entry contains upload progress and metadata.
Overall progress 0-100 derived from all entries
inputEl
Ref<HTMLInputElement | null>
The underlying hidden <input type="file"> element
Whether the selected files are valid (no errors in the upload config)
Opens the native file-picker dialog Manually enqueue external files (e.g., from drag-and-drop) ( files : ( File | Blob )[] | DataTransfer ) => void
Submit all currently queued files to LiveView (for non-auto uploads) Cancel a single entry by ref or every entry when omitted Clear local queue and reset hidden input (post-upload cleanup)
Type definitions
The upload configuration object provided by LiveView’s allow_upload/3. interface UploadConfig {
ref : string
name : string
accept : string | false
max_entries : number
auto_upload : boolean
entries : UploadEntry []
errors : { ref : string ; error : string }[]
}
Unique identifier for this upload configuration
The name of the upload (matches the allow_upload call)
Accepted file types (MIME types or extensions)
Maximum number of files that can be uploaded
Whether files should automatically upload when selected
Current upload entries with progress
Represents a single file in the upload queue. interface UploadEntry {
ref : string
client_name : string
client_size : number
client_type : string
progress : number
done : boolean
valid : boolean
preflighted : boolean
errors : string []
}
Unique identifier for this upload entry
Whether the upload is complete
Whether the file passed validation
Whether the file has been preflighted (validated on the server)
Validation errors for this entry
Basic usage
< script setup >
import { useLiveUpload } from 'live_vue'
import type { UploadConfig } from 'live_vue'
interface Props {
upload : UploadConfig
}
const props = defineProps < Props >()
const { entries , showFilePicker , submit , cancel , progress , valid } = useLiveUpload (
() => props . upload ,
{
changeEvent: "validate" ,
submitEvent: "save"
}
)
</ script >
< template >
< div class = "upload-area" >
<!-- File picker button -->
< button @click = "showFilePicker" class = "btn-primary" >
Select Files
</ button >
<!-- Upload button for manual uploads -->
< button
v-if = "!upload.auto_upload && entries.length > 0"
@click = "submit"
:disabled = "!valid"
class = "btn-success"
>
Upload {{ entries.length }} file(s)
</ button >
<!-- Overall progress -->
< div v-if = "entries.length > 0" class = "progress-bar" >
< div class = "progress-fill" :style = "{ width: progress + '%' }" ></ div >
< span > {{ progress }}% </ span >
</ div >
<!-- File list -->
< div class = "file-list" >
< div v-for = "entry in entries" :key = "entry.ref" class = "file-item" >
< div class = "file-info" >
< span class = "file-name" > {{ entry.client_name }} </ span >
< span class = "file-size" > {{ formatBytes(entry.client_size) }} </ span >
</ div >
<!-- Entry-specific progress -->
< div class = "progress-bar-small" >
< div class = "progress-fill" :style = "{ width: entry.progress + '%' }" ></ div >
</ div >
<!-- Entry status -->
< span v-if = "entry.done" class = "status-done" > ✓ </ span >
< span v-else-if = "entry.errors.length > 0" class = "status-error" >
{{ entry.errors.join(', ') }}
</ span >
<!-- Cancel button -->
< button
v-if = "!entry.done"
@click = "cancel(entry.ref)"
class = "btn-cancel"
>
Cancel
</ button >
</ div >
</ div >
</ div >
</ template >
< script >
function formatBytes ( bytes : number ) : string {
if ( bytes === 0 ) return '0 Bytes'
const k = 1024
const sizes = [ 'Bytes' , 'KB' , 'MB' , 'GB' ]
const i = Math . floor ( Math . log ( bytes ) / Math . log ( k ))
return Math . round ( bytes / Math . pow ( k , i ) * 100 ) / 100 + ' ' + sizes [ i ]
}
</ script >
Drag and drop
< script setup >
import { useLiveUpload } from 'live_vue'
import { ref } from 'vue'
const props = defineProps <{ upload : UploadConfig }>()
const { addFiles , entries , showFilePicker , progress } = useLiveUpload (
() => props . upload ,
{ submitEvent: "save" }
)
const isDragging = ref ( false )
const handleDrop = ( event : DragEvent ) => {
event . preventDefault ()
isDragging . value = false
if ( event . dataTransfer ) {
addFiles ( event . dataTransfer )
}
}
const handleDragOver = ( event : DragEvent ) => {
event . preventDefault ()
isDragging . value = true
}
const handleDragLeave = () => {
isDragging . value = false
}
</ script >
< template >
< div
@drop = "handleDrop"
@dragover = "handleDragOver"
@dragleave = "handleDragLeave"
:class = "{
'border-blue-500 bg-blue-50': isDragging,
'border-gray-300': !isDragging
}"
class = "border-2 border-dashed rounded-lg p-8 text-center transition-colors"
>
< div v-if = "entries.length === 0" >
< p class = "text-gray-600" > Drop files here or </ p >
< button @click = "showFilePicker" class = "text-blue-600 underline" >
browse
</ button >
</ div >
< div v-else >
< div class = "mb-4" >
< div class = "text-sm text-gray-600" > Uploading {{ entries.length }} file(s) </ div >
< div class = "text-2xl font-bold" > {{ progress }}% </ div >
</ div >
< div class = "space-y-2" >
< div v-for = "entry in entries" :key = "entry.ref" class = "text-left" >
< div class = "flex items-center justify-between" >
< span class = "text-sm" > {{ entry.client_name }} </ span >
< span class = "text-sm text-gray-500" > {{ entry.progress }}% </ span >
</ div >
</ div >
</ div >
</ div >
</ div >
</ template >
Image preview
< script setup >
import { useLiveUpload } from 'live_vue'
import { ref , watch } from 'vue'
const props = defineProps <{ upload : UploadConfig }>()
const { entries , showFilePicker , inputEl } = useLiveUpload (
() => props . upload ,
{
changeEvent: "validate" ,
submitEvent: "save"
}
)
const previews = ref < Map < string , string >>( new Map ())
// Generate preview URLs for image files
watch ( inputEl , ( input ) => {
if ( input ?. files ) {
Array . from ( input . files ). forEach (( file , index ) => {
if ( file . type . startsWith ( 'image/' )) {
const entry = entries . value [ index ]
if ( entry ) {
const url = URL . createObjectURL ( file )
previews . value . set ( entry . ref , url )
}
}
})
}
})
// Clean up preview URLs when entries are removed
watch ( entries , ( newEntries , oldEntries ) => {
const newRefs = new Set ( newEntries . map ( e => e . ref ))
oldEntries ?. forEach ( entry => {
if ( ! newRefs . has ( entry . ref )) {
const url = previews . value . get ( entry . ref )
if ( url ) {
URL . revokeObjectURL ( url )
previews . value . delete ( entry . ref )
}
}
})
})
</ script >
< template >
< div >
< button @click = "showFilePicker" class = "btn" >
Select Images
</ button >
< div class = "grid grid-cols-3 gap-4 mt-4" >
< div v-for = "entry in entries" :key = "entry.ref" class = "relative" >
< img
v-if = "previews.get(entry.ref)"
:src = "previews.get(entry.ref)"
:alt = "entry.client_name"
class = "w-full h-32 object-cover rounded"
/>
<!-- Progress overlay -->
< div
v-if = "!entry.done"
class = "absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center rounded"
>
< span class = "text-white font-bold" > {{ entry.progress }}% </ span >
</ div >
<!-- Done indicator -->
< div
v-if = "entry.done"
class = "absolute top-2 right-2 bg-green-500 text-white rounded-full w-6 h-6 flex items-center justify-center"
>
✓
</ div >
</ div >
</ div >
</ div >
</ template >
Error handling
< script setup >
import { useLiveUpload } from 'live_vue'
import { computed } from 'vue'
const props = defineProps <{ upload : UploadConfig }>()
const { entries , showFilePicker , valid } = useLiveUpload (
() => props . upload ,
{ submitEvent: "save" }
)
// Check for upload-level errors
const uploadErrors = computed (() => props . upload . errors || [])
// Check if any entries have errors
const hasEntryErrors = computed (() =>
entries . value . some ( entry => entry . errors . length > 0 )
)
</ script >
< template >
< div >
<!-- Upload-level errors -->
< div v-if = "uploadErrors.length > 0" class = "alert alert-error" >
< div v-for = "error in uploadErrors" :key = "error.ref" >
{{ error.error }}
</ div >
</ div >
<!-- Validation status -->
< div v-if = "!valid" class = "alert alert-warning" >
Some files are invalid. Please check the errors below.
</ div >
< button @click = "showFilePicker" > Select Files </ button >
<!-- Entry-specific errors -->
< div v-for = "entry in entries" :key = "entry.ref" class = "file-item" >
< span > {{ entry.client_name }} </ span >
< div v-if = "entry.errors.length > 0" class = "text-red-600" >
< div v-for = "(error, index) in entry.errors" :key = "index" >
{{ error }}
</ div >
</ div >
</ div >
</ div >
</ template >
Server-side setup
In your LiveView, configure the upload using allow_upload/3:
defmodule MyAppWeb . UploadLive do
use MyAppWeb , :live_view
def mount ( _params , _session , socket) do
socket =
socket
|> allow_upload ( :avatar ,
accept: ~w(.jpg .jpeg .png) ,
max_entries: 1 ,
max_file_size: 5_000_000 ,
auto_upload: true
)
{ :ok , socket}
end
def handle_event ( "validate" , _params , socket) do
# Validation happens automatically
{ :noreply , socket}
end
def handle_event ( "save" , _params , socket) do
uploaded_files =
consume_uploaded_entries (socket, :avatar , fn %{ path: path}, entry ->
dest = Path . join ( "priv/static/uploads" , entry.client_name)
File . cp! (path, dest)
{ :ok , "/uploads/ #{ entry.client_name } " }
end )
{ :noreply , assign (socket, uploaded_files: uploaded_files)}
end
def handle_event ( "cancel-upload" , %{ "ref" => ref}, socket) do
{ :noreply , cancel_upload (socket, :avatar , ref)}
end
end
Then pass the upload config to your Vue component:
< .vue
v - component = "FileUploader"
upload = { @uploads .avatar}
v - socket = { @socket }
/>