Everyone loves Gmail’s async-drag-and-drop-with-progress bar attachment UI. While I’d heard that HTML5 supports this type of upload, I found myself sticking with the nasty old form submission model.
While writing a media manager for eridu, I decided to finally look into it. My research into the progress
element, the drop
event, and the FileReader
object bore a tiny ~180 line Sinatra app. Download it, run ruby dropbox.rb
, and you have a complete reference implementation of a Gmail-like uploader.
A few similar libraries already existed, but I wanted to fully understand the HTML5 elements involved. While it’s not too complicated, it’s not too obvious either. So here’s a breakdown of the critical pieces.
Outline
- Create dropbox HTML elements
- Bind event handlers to drop
- Read the file contents
- POST the file contents with Ajax, update progress bar
- Receive the file on the server
Create dropbox HTML elements
Two regular divs – one to serve as the dropbox, the other as a place for upload progress bars.
<div id="dropbox">Drop files here...</div>
<div id="log"></div>
Bind event handlers to drop
Functions to accept files and pass them off to a handler.
var dropbox = document.getElementById('dropbox')
// Handle each file that was dropped (you can drop multiple at once)
function drop(e) {
noop(e)
var files = e.dataTransfer.files
for ( var i=0; i < files.length; i++ )
handle_file(files[i])
}
function handle_file(file) {
// We'll write this later
}
// Prevent event from bubbling up
function noop(e) {
e.stopPropagation()
e.preventDefault()
}
// Bind event listeners
dropbox.addEventListener("drop", drop, false);
dropbox.addEventListener("dragleave", noop, false);
dropbox.addEventListener("dragexit", noop, false)
dropbox.addEventListener("dragover", noop, false);
Read the file contents
Read the file contents and pass it off to the uploader.
function handle_file(file) {
var reader = new FileReader();
reader.onload = function(e) {
// A base64 encoded string
var file_contents = e.target.result.split(',')[1]
// Upload it
upload_file(file, file_contents)
}
// Read the file
reader.readAsDataURL(file);
}
POST the file contents with Ajax, update progress bar
Put the file contents into a form, send the form over ajax, and periodically update a progress bar.
var log = document.getElementById('log')
function upload_file(file, file_contents) {
// Make a progress bar
var label = document.createElement('div')
label.innerHTML = '<progress></progress> ' + file.name
log.insertBefore(label, null)
// Build a form for the data
var data = new FormData()
data.append('filename', file.name)
data.append('mimetype', file.type)
data.append('data', file_contents)
data.append('size', file.size)
// Create a new XHR object and assign its callbacks
var xhr = new XMLHttpRequest()
// Periodically update progress bar
if ( xhr.upload )
xhr.upload.addEventListener('progress', function(e) { update_progress(e, label) }, false)
xhr.open('POST', '/ajax-upload')
xhr.onreadystatechange = function(e) {
if ( xhr.readyState === 4 ) {
if ( xhr.status === 200 ) {
// Success! Response is in xhr.responseText
} else {
// Error! Look in xhr.statusText
}
}
}
// Send the ajax request
xhr.send(data)
}
// Update the progress bar
function update_progress(e, label) {
if ( e.lengthComputable ) {
var progress = label.getElementsByTagName('progress')[0]
progress.setAttribute('value', e.loaded)
progress.setAttribute('max', e.total)
}
}
Receive the file on the server
Here’s a simple Sinatra app that receives the data and writes it to disk. Note that this is very different than a traditional upload, where a tmpfile is written to disk for you. Instead, you simply receive the file contents (as a base64 string) through a form field, just like any other form data.
require 'sinatra'
require 'base64'
post '/ajax-upload' do
File.open("/tmp/upload-#{params[:filename]}", 'w') do |f|
f.puts Base64.decode64(params[:data])
end
'huzzah!'
end
That’s it. My example app adds a few bells and whistles, but these basics are the meat of it. (Of course a production-ready implementation would need to provide some kind of fallback for older browsers.) Now go forth and make uploads better for everyone!