Image Upload : Front End

A refined UI for uploading and displaying and uploading images responsively.

This post extends the backend functionality in Image Upload with an UI for displaying and uploading images.

Displaying the uploaded photo on a page

MyUser can have many Upload instances in our model. We can retrieve them using an include in the query resulting in an array of uploads that are associated with the user.

For convenience we'll define a small utility for iterating over the uploads and returning the one that matches the property we want such as "photo" or "background."

server/lib/getUploadForProperty.js

// this function expects
//  prop: property that was specified when the image was uploaded eg. "photo" "background" 
//  uploads: array of Uploads to scan
// if nothing is found it returns a slug graphic (/images/fpo.jpg)
// otherwise it returns the url of the desired upload
module.exports = function getUploadForProperty(prop, uploads) {  
    if (uploads && uploads.length) {
        for (var j = 0; j < uploads.length; j++) {
            if (uploads[j].property === prop) {
                return uploads[j];
            }
        }
    }
    return {
        url: '/images/fpo.jpg'
    };
};

Expose the getUploadForProperty utility to the template engine (jade) so we can call it within templates.

server/server.js

app.locals.getUploadForProperty = require('./lib/getUploadForProperty');  

Include the uploads in the getCurrentUser middleware MyUser lookup so that we can use them in the templates.

This is a small change to the middleware we built in the Tokens and Sessions tutorial.

server/middleware/context-currentUser

req.app.models.MyUser.findById(req.accessToken.userId, {  
  include: ['uploads'] <--- THIS
}, function (err, user) {
// user now has user.uploads()
});

Call the getCurrentUser middleware on the route to get the currentUser and its Uploads.

server/boot/routes.js

  router.get('/upload', getCurrentUser(), ensureLoggedIn(), function (req, res, next) {
    var ctx = server.loopback.getCurrentContext();
    var currentUser = ctx.get('currentUser');

    res.render('pages/upload', {
      user: currentUser
    });
  });

Now display the photo of the current user in the template

-var photo = getUploadForProperty('photo',user.uploads())

img(src= photo.url)  

This is a bit crude however. The user probably uploaded an image with an aspect ratio that does not match the design or a even a 12 megapixel file that would be larger than the browser window.

This page will need some finesse to display the upload properly. Ideally we would lazy load the image so it is not loaded until it is scrolled into view and we would place it in a "viewport" where we can scale it and crop it to fit the design. To do all that we need to connect some javascript to the layout elements.

A Quick Intro to digitopia.js controllers

Digitopia Controllers are Javascript objects that are closely associated with HTML elements which listen for and respond to external events such as those generated by window resizing, scrolling and page transitions. They can also implement complex UI functions such as formatting results of ajax calls or, in the case the example below, simply displaying a prompt.

The digitopia.js library automatically instantiates and starts controllers for html element that have data-jsclass defined.

<div data-jsclass="myController">This is in the markup.</div>  
// define myController
// elem: the html DOM element that we are controlling
// myController.start is called on page load
(function ($) {
    function myController(elem) {
        this.element = $(elem);
        var self = this;
        self.start = function() {
            self.element.append(' And this is the controller talking.');
        }
    }

    // this turns myController into a jQuery function
    $.fn.myController = GetJQueryPlugin('myController', myController);
})(jQuery);

When the page is loaded the element will contain:

This is in the markup. And this is the controller talking.

Using these controllers we can build a complex, responsive element that handles displaying the image with elegant drag and drop upload of an image.

Responsive, Lazy Loaded image with Dropzone upload

The UI for this will be boostrap's .col-sm-6 wide x 300 heigh. On xs screens it will be full width. The image will be centered, scaled and cropped in that zone. The uploader is revealed on hover.

server/views/pages/upload.jade

.row
    .col-sm-4
        - var endpoint = "/api/MyUsers/me/upload/photo"
        - var photo = getUploadForProperty('photo',user.uploads())
        .profile-photo-viewport.text-center(data-jsclass="digitopiaViewport" data-crop="true" data-blowup="true")
            img(data-jsclass="digitopiaLazyImg" data-lazy-src= photo.url)
            .dropzone(data-jsclass="dropzoneController" data-endpoint= endpoint)
                .dz-message.
                    Drop your photo here<br><small>(or click to upload)</small>
.profile-photo-viewport
    position:relative
    height:300px
    img
        transition: all 0.5s ease;
    .dropzone
        position:absolute
        top:0px!important
        width:100%
        height:300px
        opacity: 0
        transition: all 0.5s ease;
        paddding-top:40%
    .dropzone:hover
        opacity: 0.8
    .loading
        opacity: 1

Notice that there are several data-jsclass= in the jade markup.

digitopiaViewport is an element which will center, scale and position an image (or other elements) clipping it to the dimensions of the viewport.

digitopiaLazyImg is an image where the download of the file is delayed until it is scrolled into view which helps with page load time by delaying heavy downloads until the file is needed.

Defining the upload behavior with dropzoneController

dropzone.js provides a nice, clean method for handling file upload which supports click to upload and drag and drop upload with nice upload progress indicators.

Wrap the creation of the dropzone in a digitopiaController. The details of the dropzone configuration are documented on the dropzone.js site. In short we want the user to upload one and only one file (dropzone supports bulk upload) and set the endpoint for uploading the image /api/MyUsers/me/uploads/photo.

assets/js/dropzoneController.js

(function ($) {
    // disable dropzone auto instantiation
    Dropzone.autoDiscover = false;

    // define dropzoneController
    function dropzoneController(elem, options) {
        this.element = $(elem);

        var self = this;

        // get the upload endpoint from the html data-endpoint= tag
        this.settings = $.extend({
            endpoint: this.element.data('endpoint')
        }, options || {});

        this.dropzone = undefined;

        this.start = function () {

            this.dropzone = new Dropzone(this.element[0], {
                url: self.settings.endpoint,
                paramName: 'uploadedFile',
                uploadMultiple: false,
                maxFiles: 1,

                init: function () {

                    // only allow single file upload
                    this.on('maxfilesexceeded', function (file) {
                        this.removeAllFiles();
                        this.addFile(file);
                    });

                    // provide feedback that upload is processing
                    this.on("processing", function () {
                        self.element.addClass('loading');
                    });

                    // on success, update the current image in the markup
                    this.on("success", function (dzfile, body) {
                        var response = body.response;
                        var s3file = response.url;
                        var img = self.element.parent().find('img').first();
                        img.attr('src', '');
                        img.attr('src', s3file);
                        img.data('lazy-src', s3file);
                        self.element.removeClass('loading');
                    });

                    // remove the thumbnail from the dropzone UI
                    this.on("complete", function (file) {
                        this.removeFile(file);
                    });
                }
            });

        };

        this.stop = function () {};
    }

    $.fn.dropzoneController = GetJQueryPlugin('dropzoneController', dropzoneController);
})(jQuery);

To see this in action download the example project scaffolding from git (you will need AWS account for s3 credentials)


Photo: Leopard Cub, Tanzania (2009)
Document version 1.0