Uploader with Alpine.js
A file uploader with progress using Alpine.js.
Here is the slimmed down markup, an file input to populate our alpine store with the selected files and a template to iterate through the files.
We are using a div to show progress by binding to the width property, note the absolute positioning and the z-index to ensure the progress div is behind the image.
<div >
<input type="file" id="file-input" multiple accept="image/*" x-on:change="$store.uploads.addFiles($event.target.files);" />
<ul id="preview">
<template x-for="file in $store.uploads.items">
<li class="relative">
<div class="absolute inset-y-0 left-0 bg-gradient-to-r from-green-500 to-emerald-500 transition-all duration-300 ease-out z-0 opacity-30" x-bind:style="`width: ${file.progress}%`"></div>
<img x-bind:src="URL.createObjectURL(file.f)" class="w-20 h-20 object-cover rounded-lg shadow z-10" />
<div>
<p x-text="file?.f.name"></p>
<p x-text="Humanize.fileSize(file?.f.size)"></p>
</div>
<button x-on:click="$store.uploads.remove(file)">x</button>
</li>
</template>
</ul>
</div>
Using a store we can group our data and functions together.
The addFiles method is called when files are selected on the file input and added to the store, this calls uploadFile which creates the http request using XMLHttpRequest which allows you to listen to the progress event and update the UI, fetch does not support this.
(function (window) {
document.addEventListener('alpine:init', () => {
Alpine.store('uploads', {
items: [],
remove(item) {
this.items = this.items.filter((f) => f !== item)
},
addFiles(files) {
files = [...files].map((f) => ({ progress: 1, status: 'Uploading...', f: f }))
this.items = [...this.items, ...files]
for (var file of files) {
this.uploadFile(file)
}
},
updateProgress(file, progress) {
this.items = this.items.map((f) => {
if (f.f === file) {
f.progress = progress
}
return f
})
},
async uploadFile(item) {
file = item.f
const formData = new FormData();
formData.append("file", file);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", ((event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
this.updateProgress(file, percentComplete);
}
}).bind(this));
xhr.addEventListener("load", (() => {
if (xhr.status >= 200 && xhr.status < 300) {
console.log("Upload successful:", xhr.responseText);
this.updateProgress(file, 100);
setTimeout(() => {
this.items = this.items.filter((f) => f.f !== file)
}, 1000);
} else {
console.error("Upload failed with status:", xhr.status);
}
}).bind(this));
xhr.open("POST", "/upload");
xhr.send(formData);
}
})
})
})(window);
You could store the xhr request and abort it if the user cancels the upload.