Dugan Chen's Homepage

Various Things

Progressively Enhancing Upload Forms

This is how you progressively enhance an upload form.

Start with your average run-of-the-mill upload form. The following example takes a file whose maximum size is two placekittens:

<form method="POST" action="/upload" enctype="multipart/form-data">
    <input type="hidden" name="MAX_FILE_SIZE" value="32722">
    <input type="file" name="image">
    <input type="submit">
</form>

We can use this form in page like the following, where the user is expected to upload an image. After the form is submitted, the server-side code sends back the page with the image embedded between the div id=”image” tags. The server-side code to handle this is obvious. Furthermore, the form will work with any browser, including a browser without Javascript:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Form Demo</title>
    </head>
    <body>
        <form method="POST" action="/upload" enctype="multipart/form-data">
            <p>
                <input type="hidden" name="MAX_FILE_SIZE" value="32722">
            </p>
            <p>
                <input type="file" name="image">
            </p>
            <p>
                <input type="submit">
            </p>
        </form>
        
        <div id="image"></div>
    </body>
</html>

(Please note that in this example, we’ll ignore obvious security measures such as CSRF protection).

Modern browsers, however, have features that can greatly enhance the user experience. We’ll take advantage of them if they’re there. To do so, we’ll start by loading the Javascript libraries that we’ll need:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Form Demo</title>
    </head>
    <body>
        <form method="POST" action="/upload" enctype="multipart/form-data">
            <p>
                <input type="hidden" name="MAX_FILE_SIZE" value="32722">
            </p>
            <p>
                <input type="file" name="image">
            </p>
            <p>
                <input type="submit">
            </p>
        </form>
        
        <div id="image"></div>
       
        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
        <script src="http://cdnjs.cloudflare.com/ajax/libs/modernizr/2.0.6/modernizr.min.js"></script>
        <script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.1.7/underscore-min.js"></script>
        <script src="demo.js"></script>
    </body>
</html>

In the “demo.js” file, we’ll detect whether or not the browser supports the features that we need. If so, then we add two event handlers.

The first is for the file input’s change event. When the event fires, it immediately uses AJAX to upload the file.

The second is for the form’s drop event. If you drag a file from another application (Windows Explorer, for example) and drop it onto the form, then this event fires and uploads the file via AJAX.

Both event handlers will, on success, execute a callback that refreshes the image on the page. They also validate the file’s size (no larger than MAX_FILE_SIZE) and type (JPEGs only).

We’ll also remove the “submit” button, as we don’t need it for this particular form.

The code is as follows:

var jQuery = jQuery || undefined,
    FormData = FormData || undefined,
    _ = _ || undefined,
    FileReader = FileReader || undefined,
    $ = $ || undefined,
    Modernizr = Modernizr || undefined;

(function ($) {
    'use strict';
    
    var sendFiles = function (options) {
        var formData = new FormData(),
            size = parseInt($('input[name=MAX_FILE_SIZE]').val(), 10),
            validFiles = _(options.files).select(function (file) {
                return file.size < size && file.type.match(options.filter);
            }),
            inputName = options.name;
        if (options.multiple) {
            inputName += '[]';
        }
        
        if (validFiles.length === 0) {
            return;
        }

        _(validFiles).each(function (file) {
            var reader = new FileReader();
            reader.readAsDataURL(file);
            formData.append(inputName, file);
        });
        $.ajax({
            url: options.url,
            type: 'POST',
            data: formData,
            processData: false,
            contentType: false,
            success: options.success
        });
    };
    
    $.fn.uploadForm = function (options) {

        var fileInput = this.find('input[type=file'),
            cancel = function (event) {
                event.preventDefault();
                event.stopPropagation();
            },
            settings = {
                url: '/',
                success: function () {},
                filter: new RegExp(''),
                multiple: fileInput.attr('multiple') !== undefined,
                name: fileInput.attr('name')
            };
        
        if (options) {
            $.extend(settings, options);
        }
        
        this.bind('dragenter', cancel);
        this.bind('dragover', cancel);
        
        this.bind('drop', function (event) {
            sendFiles($.extend({
                files: event.originalEvent.dataTransfer.files
            }, settings));
        });
        
        this.find('input[type=file]').change(function () {
            sendFiles($.extend({
                files: this.files
            }, settings));
        });
        return this;
    };

}(jQuery));

$(function () {
    'use strict';

    if (Modernizr.draganddrop && !!FileReader) {
        this.find('input[type=submit]').remove();
        $('form').uploadForm({
            url: '/upload/ajax/',
            filter: /image\/jpeg/,
            success: function () {
                var src;
                if ('#image').find('img').length === 0:
                    $('<img>').appendTo('#image');
                src = $('img').attr('src').split('?')[0];
                src += '?v=' + _.uniqueId();
                $('img').attr('src', src);
            }
        });
    }
});