Ember.js drag and drop file upload - the right(ish) way

Updated on August 13th 2015

Every journey starts with a beginning

Not so long time ago, I set out on a journey to try to accomplish the best possible way to do file uploads with Ember.js with no other libraries, but the ones what the framework already provides. After some experimentation and many, many, new updates released with some changes on the way to do things, I come to this article.

Which leads to the right(ish) part, in my opinion the right way to use a library/framework is to use it in it's full. As Ember.js provides a great range of tools and APIs to help us develop our applications I always take a deep dive in the Ember Guides before developing something new.

For this particular subject, file upload, there is one thing missing there though, which is Ember.RSVP API does not have support for progress.

So, after a long discussion with myself, I came to the conclusion that this time I would go a little bit around the framework to accomplish the task. So without any further ado, lets get to this, quite long, journey.

Creating the model

For this article I will not be covering the possible solutions for a backend part for the file upload, as there are many articles out there to get you to a good solution. Let's pretend we have a Rails api that will have a /assets resource which will give us the following response to the file upload request:

{
  asset: {
    id: 1,
    image: {
      url: "/path/to/the/file.jpg"
    },
    title: ''
  }
}

The idea is for the API to receive a model asset with a given image property that represents the file (to be) uploaded. Once the file is uploaded, this image property will then contain a url value to the file uploaded. If you are using Ruby for your backend I suggest using the gem Carrierwave to accomplish that.

Notice: I am using ember-cli to generate and manage my project.

The question you would ask at this stage could be but how do I represent the file (to be) uploaded in my model? As there is no support for that currently on Ember Data (only for string, number, boolean and date)? Well, it is actually simpler than you would expect.

You can create your very own DS.Transform implementation to represent other types of data in case of need. But for this case all we want is to pass and receive an object to/from the API, so you can just map it to DS.attr() and Ember Data will take care of the rest. With that, let's create the model asset to represent our file to be uploaded:

import DS from 'ember-data';  
import Ember from 'ember';

export default DS.Model.extend({  
  title: DS.attr('string'),
  image: DS.attr(),
  imageUrl: Ember.computed.alias('image.url'),
  imageName: DS.attr(),
  imageNameObserver: function(){
    /*
      This computed property is simply to when we receive the file from our
      servers on a store.find('asset', id) query we are still able to isolate
      it's file name correctly.
      If you api returns the imageName on the response you do not need this observer
    */
    var url,
        imageName;

    url = this.get('fileUrl');
    imageName = this.get('imageName');

    if(Ember.isPresent(url) && Ember.isNone(imageName)){
      return url.split('/').find(function(urlPart){
        return !!urlPart.match(/\.(?:jpg|gif|png)$/) ? urlPart : null;
      });
    }

    else{
      return "";
    }
  }.observes('imageUrl'),
  progress: 0
});

You can notice that for easy access on our template we create a imageUrl alias to our image.url property and a imageNameObserver observer to extract the image.jpg part of the url and set to our imageName to be displayed (for any control/display purposes).

We also have a unmapped progress property, that will come in handy later in this article.

It's time to upload something, but first...

Let's create a component, called file-uploader to manage the drag and drop features of our file upload. The idea, for this article, is to allow only one file to be uploaded at the time.

import Ember from 'ember';

export default Ember.Component.extend({  
  tagName: 'div',
  classNames: 'uploader dropzone'.w(),
  classNameBindings: 'isDragging isDisabled:is-disabled'.w(),
  attributeBindings: 'data-uploader'.w(),
  'data-uploader': 'true',
  isDisabled: false,


  dragOver: function(event){
    // this is needed to avoid the default behaviour from the browser
    event.preventDefault();
  },

  dragEnter: function(event){
    event.preventDefault();
    this.set('isDragging', true);
  },

  dragLeave: function(event){
    event.preventDefault();
    this.set('isDragging', false);
  },

  drop: function(event){
    var file;

    if(!this.get('isDisabled')){
      event.preventDefault();
      this.set('isDragging', false);

      // only 1 file for now
      file = event.dataTransfer.files[0];
      this.set('isDisabled', true);
      this.sendAction('fileInputChanged', file);
    } else{
      console.error('you can only upload on file at the time');
    }
  }
});

For this component there is no need for a template file, as all it's features are described in this file, and we will do no further special markup on it. The idea behind it is pretty simple: a box where you can drag and drop your file to be uploaded.

It will have the dragOver, dragEnter, dragLeave and drop events to get the file and pass it to be uploaded.

The dragEnter and dragLeave event listeners are simply to toggle isDragging, part of our classNameBindings, so you can show some visual feedback to the user.

The drop event listener is where we get the file object and pass it over to the controller so the file upload process can begin.

While the upload process is handled by the controller and store, isDisabled control property is set to true, so we avoid future drop events to proceed with another upload task while this upload is in progress.

To proceed with the upload task we call for the fileInputChanged action passing the file by running this.sendAction('fileInputChanged', file).

Creating the necessary markup

Now that we can make use of that file-uploader component, lets create our index template to use it.

<div class="row">  
  <div class="col-md-12">
    {{file-uploader fileInputChanged="receiveFile"}}
  </div>
</div>  

Notice that we map the fileInputChanged to call our controller's receiveFile.

Proceeding with the upload action

Now we create our index controller we can create the receiveFile action to receive the file object and push it to our severs.

import Ember from 'ember';

export default Ember.Controller.extend({  
  actions:{
    receiveFile: function(file){
      var asset;

      asset = this.store.createRecord('asset', {
        image:  file,
        imageName: file.name,
        title: 'something'
      });

      asset.save().then(function(asset){
        console.info(asset.get('imageUrl'));
      }, function(error){
        console.debug('Upload failed', error);
      }, 'file upload');
    }
  }
});

This step is pretty straight forward, we receive the file object and with it we create a asset record to be saved and persisted to our servers.

But for that feature to work properly we have to work with the FormData API to correctly serialise and send the file object to our servers.

You could, very easily, create a controller function that receives the file object, parses into FormData object and send it with a Ember.$.ajax call. But as we are speaking of the right(ish) way, we want to be able to simply use the framework at it's full. So for that the DS.Model.save call (asset.save) seems a better, cleaner, and more reusable way of handling it.

Creating your own adapter (mixin)

To make that possible we need to modify the standard DS.RESTAdapter implementation, to parse the received model into FormData. Let's create the form-adapter mixin:

import Ember from 'ember';

export default Ember.Mixin.create({  
  // Overwrite to change the request types on which Form Data is sent
  formDataTypes: ['POST', 'PUT', 'PATCH'],

  ajaxOptions: function(url, type, options){
    var data;

    if (options && 'data' in options){
      data = options.data;
    }

    var hash = this._super.apply(this, arguments);

    if(typeof FormData === 'function' &&
       data &&
       this.formDataTypes.contains(type)){
      var formData,
          root;

      formData = new FormData();
      root = Ember.keys(data)[0];

      Ember.keys(data[root]).forEach(function(key){
        if(Ember.isPresent(data[root][key])){
          formData.append(root + "[" + key + "]", data[root][key]);
        }
      });

      hash.processData = false;
      hash.contentType = false;
      hash.data = formData;
    }

    return hash;
  }
});

For that, we need to change our adapter's ajaxOptions function to parse the header properties using the FormData api. Now you can overwrite the AssetAdapter to extend DS.RESTAdapter, or your application's adapter for better reusability of a common endpoint configuration, passing this mixin.

import ApplicationAdapter from './application';  
import FormDataAdapterMixin from '../mixins/form-data-adapter';

export default ApplicationAdapter.extend(FormDataAdapterMixin, {  
});

With this you can already successfully handle file uploads with Ember.js, but this still does not feel complete, does it? Where is our progress handling? What if the user tries to upload a file type that I do not (want to) support/allow?

And now your other journey begins

your princess is in another castle!

I confess, the optimal way to introduce progress handling with Ember.js would be to have it built in the Ember.RSVP.Promise API. It would be just great to have something like:

// progress is last for backwards compatibility
model.save().then(successFunction, errorFunction, 'label', progressFunction);  

I even set out to do that in the beginning, but it turns out to be little gain for too much headache. So the plan B was to get a even fired to a specific target with a pre-determined attribute. This target is our file-uploader component, and the attribute to be targeted is [data-uploader].

In order to do that we must overwrite the DS.RESTAdapter.ajax call to 'forward' the progress event to our target somehow. So let's add that to our mixin.

import Ember from 'ember';  
import DS from 'ember-data';

var InvalidError = DS.InvalidError;

export default Ember.Mixin.create({  
  // ...

  ajax: function(url, type, options){
    var adapter = this;

    return new Ember.RSVP.Promise(function(resolve, reject){
      var hash = adapter.ajaxOptions(url, type, options);

      hash.success = function(json, textStatus, jqXHR){
        json = adapter.ajaxSuccess(jqXHR, json);
        if (json instanceof InvalidError){
          Ember.run(null, reject, json);
        } else {
          Ember.run(null, resolve, json);
        }
      };

      hash.error = function(jqXHR, textStatus, errorThrown){
        Ember.run(null, reject, adapter.ajaxError(jqXHR, 
                                                  jqXHR.responseText,
                                                  errorThrown));
      };

      hash.xhr = function(){
        var xhr = new window.XMLHttpRequest();
        //Upload progress
        xhr.upload.addEventListener("progress", function(evt){
          if (evt.lengthComputable){
            var percentComplete = evt.loaded / evt.total;
            Ember.$('[data-uploader]').trigger({
              type:"uploadProgress",
              progress:percentComplete
            });
          }
        }, false);
        //Download progress
        xhr.addEventListener("progress", function(evt){
          if (evt.lengthComputable){
            var percentComplete = evt.loaded / evt.total;
            Ember.$('[data-uploader]').trigger({
              type:"downloadProgress",
              progress:percentComplete
            });
          }
        }, false);
        return xhr;
      };

      Ember.$.ajax(hash);
    }, 'DS: RESTAdapter#ajax ' + type + ' to ' + url);
  }
});

In here almost everything is straight out from the source code (as you can see on the link above), except for the extra hash.xhr option. That is to enable us to add the xhr.upload and xhr (download) progress event listeners to our requests. When we receive those events callbacks, we then trigger on the [data-uploader] (our file-uploader component) the uploadProgress or downloadProgress accordingly.

Are you listening to it?!

Now we must add the event listeners on our file-uploader component. It will then forward this to the controller, as the component does not have control over the model that is being saved. So in your file-uploader component add on the didInsertElement hook the event watchers.

import Ember from 'ember';

export default Ember.Component.extend({  
  // ...

  didInsertElement: function(){
    var _this = this;

    this.$().on('uploadProgress', function(evt){
      if(evt.progress === 1){
        _this.set('isDisabled', false);
      }
      _this.sendAction('uploadProgress', evt.progress);
    });
  }
});

And now we can update the index template to map the action to be forwarded.

<div class="row">  
  <div class="col-md-12">
    {{file-uploader fileInputChanged="receiveFile" uploadProgress="uploadProgress" }}
  </div>
</div>  

Show your progress

Now let's make use of that progress information, first let's create a list of images, or assets: [], (being) uploaded. Whenever we receive our call for uploadProgres, we grab our firstObject, which will link to the file being uploaded at the moment, and update it's progress. Here's the updated index controller:

export default Ember.Controller.extend({  
  assets: [],
  actions:{
    receiveFile: function(file){
      var asset;

      asset = this.store.createRecord('asset', {
        image:  file,
        imageName: file.name,
        title: 'something'
      });

      this.get('assets').pushObject(asset);

      asset.save().then(function(asset){
        console.info(asset.get('imageUrl'));
      }, function(error){
        console.debug('Upload failed', error);
      }, 'file upload');
    },

    uploadProgress: function(progress){
      this.set('assets.lastObject.progress', progress);
    }
  }
});

With this structure in place, let's create a new component to handle this assets array with it's different states (progress/download/show). Let's create the uploaded-file component with the following logic:

import Ember from 'ember';

export default Ember.Component.extend({  
  tagName: 'div',
  classNames: 'asset'.w(),
  file: {},
  progress: function(){
    return 'width:'+(this.get('file.progress')*100)+'%';
  }.property('file.progress'),
  isUploading: function(){
    return this.get('file.progress') !== 1;
  }.property('file.progress')
});

This component's logic is very simple: receive a image object; map it's progress for easy access with the progress computed property; and verify if it's uploading with the isUploading computed property. A note that I leave here is that, depending on the size of your application, you will want to reduce your computed properties down to a minimum. So instead of two, or more, computed properties watching the same property(ies) for changes, you will want to have one observer setting multiple variables based on it's computation.

Now let's create the markup for our uploaded-file component:

{{#if file.isNew}}
  {{#if isUploading}}
    <div class="progress">
      <div class="bar" {{bind-attr style=progress}}></div>
    </div>
  {{else}}
    <span>uploading...</span>
  {{/if}}
{{else}}
  <img class="asset-image" {{bind-attr src=file.imageUrl}}/>
  <span>{{file.imageName}}</span>
{{/if}}

On our template file for the uploaded-file component we verify the model's isNew state to show either if it shows the upload progress bar or it's uploaded image. Let's now use the uploaded-image component on our index template:

<div class="row">  
  <div class="col-md-12">
    {{file-uploader fileInputChanged="receiveFile" uploadProgress="uploadProgress" }}
  </div>
</div>  
<div class="row">  
  <div class="col-md-12">
    <div class="uploaded-images">
    {{#each asset in controller.assets}}
      <div class="col-md-3">
        {{uploaded-file file=asset}}
      </div>
    {{/each}}
    </div>
  </div>
</div>  

Validate before sending

So now it comes to the asset model again. For this part you can choose to use any of the existing validation libraries, and party down.

party down

My goal however, as said in the beginning, was to use no external libraries for this. As the validation libraries out there supports some situations from the alpha times of Ember Data, when devs opted to stay away of it, and validations outside of models too.

My goal is simple here, validate my model using the pre-existing DS.Errors support for validation in the DS.Model instances. Using Ember's own structure in it's favour, we are able to reduce our code significantly in size and complexity. I will though use a validations object mapping similar to DockYard's ember-validations for compatibility at least when it comes to mapping the validations.

import DS from 'ember-data';  
import Ember from 'ember';  
import Validation from '../mixins/validation';

export default DS.Model.extend(Validation, {  
  // ...

  // our validations object
  validations:{
    // this is a property to be observed and validated accordingly
    imageName: {
      // this is our validator intended to be executed
      file: {
        // this is the restrictions that we want
        // to apply to our imageName property
        accept: ['png', 'jpg', 'gif'],
        // this is the message to display in case of a faulty value
        message: 'Accepts only images'
      }
    }
  }
});

Create your validation mixin

Let's create the mixin that will receive our validations object, and map the appropriate validators to be used when there's a change in the targeted property. Our validation mixin requires only a few steps.

import Ember from 'ember';

export default Ember.Mixin.create({  
  validators: Ember.inject.service(),
  init: function(){
    var properties,
        validations,
        _this;

    this._super.apply(this, arguments);

    properties = Ember.keys(this.get('validations'));
    validations = this.get('validations');
    _this = this;

    properties.forEach(function(property){
      var modelProperty,
          validationsToExecute;

      modelProperty = validations[property];
      validationsToExecute = Ember.keys(modelProperty);

      validationsToExecute.forEach(function(validation){
        var validationToExecute,
            validator;

        validationToExecute = validations[property][validation];
        validator = _this.get('validators').findBy('name', validation);

        _this.addObserver(property, _this, function(model){
          validator.get('executor')
                 .execute(model, property, validationToExecute);
        });
      });
    });

  },

});

And here we also target our support for Ember 1.10 and forward, because of the usage of Ember.inject.service. It's a more declarative way of dependency injection that Ember.js introduced on it's 1.10 release. But more importantly, allows us to actively decide where to use our dependencies, through the Ember.Service API, also allowing for some previously tricky places to access dependencies, like mixins specific dependencies :D.

Basically, by creating a Ember.Service instance, you be able to inject it either through a initialiser, by mapping service:yourService, or with the new Ember.inject.service call. So, when we call validators: Ember.inject.service(), we are mapping a service called validators, or service:validators if you inject through a initialiser, to our validators property in our validation mixin.

On the model's init call we will get our validations object and map our model's properties to be observed from it. And then map our appropriate validation(s) to execute from our modelProperty into validationsToExecute.

With that validationsToExecute array we will extract from our validators service our validator instance. And then call our model's addObserver to watch our model's property to call our validator's execute function, which will receive our model's state and our validationToExecute and determine either it's valid or invalid. This will be the default signature for any of our validators instances to implement.

How does this validators service looks like, you may wonder? It's actually nothing but an array that will map our validator instances, like our file validation. Here's our validators service in full:

import Ember from 'ember';

export default Ember.ArrayProxy.extend({  
  content: []
});

The idea is that we will create a validator that will simply look for that service instance and append itself to it, so our models can utilise it.

Creating our File validation

I decided that our validators would then be injected into our validators service through a initialiser. So let's go ahead and create our file validator, which I will call validator-upload.

import Ember from 'ember';

var UploadValidator = Ember.Object.create({  
  execute: function(model, property, restrictions){
    var isValid,
        acceptedExtensions;

    isValid = false;
    acceptedExtensions = new RegExp("\\.(?:"+restrictions.accept.join('|')+")$");

    isValid = !!model.get(property).toLocaleLowerCase().match(acceptedExtensions);

    if(!isValid){
      model.get('errors').add(property, restrictions.message);
    }
  }
});

export function initialize(container) {  
  var validators = container.lookup('service:validators');
  validators.pushObject(Ember.Object.create({
    name: 'file',
    executor: UploadValidator
  }));
}

export default {  
  name: 'validator-upload',
  initialize: initialize
};

The idea is for all validators to share a common signature, so you can even create a Validator.Base object from that same Ember.Object signature passed to be able to extend your validators library! In this case we will create just one validator, so we will use our pre-determined signature into it, where it will have an execute function. This function will be responsible to receive the model and check if the property passed is valid or not, based on the restrictions property.

In this case we are verifying if our imageName contains one of the extensions mapped on the accept array. If it does not, we will then add our model's property to it's error array by calling model.get('errors').add.

This validator will be injected into our service:validators instance with the validators.pushObject call. Using our pre-determined signature, where it will have a name attribute, that will be it's public alias to be used on the model's validations object mapping, and the executor property, that will then receive our UploadValidator object.

Updating our controller to watch for our (in)valid model

With all those validations going on, we want to make sure to send a request to our server only if our model is valid. That is made easy with our model's isValid state, that will check if we have added any error to our model's errors array. Here is our updated controller:

import Ember from 'ember';

export default Ember.Controller.extend({  
  assets: [],
  actions:{
    receiveFile: function(file){
      var asset;

      this.set('uploadDisabled', true);
      asset = this.store.createRecord('asset', {
        image:  file,
        imageName: file.name,
        title: 'something'
      });

      if(asset.get('isValid')){
        this.get('assets').pushObject(asset);

        asset.save().then(function(asset){
          console.info(asset.get('imageUrl'));
        }, function(error){
          console.debug('Upload failed', error);
        }, 'file upload');
      }

      else{
        // our model is invalid...
        this.set('uploadDisabled', false);
      }
    },

    uploadProgress: function(progress){
      this.set('assets.lastObject.progress', progress);
    }
  }
});

Our controller will also update the isDisabled state of our file-uploader, if any validation error occurs and we interrupt the upload process, through it's internal uploadDisabled property. Our asset model will get automatically validated upon calling this.store.createRecord passing it's intended property values, and then we should be able to check if our model is valid or not with our isValid state.

We now need to do a small update on our index template to map the file-uploader component's isDisabled property to our index controller's uploadDisabled, so our component gets re-enabled after a invalid file is discarded from our upload process.

{{file-uploader isDisabled=controller.uploadDisabled fileInputChanged="receiveFile" uploadProgress="uploadProgress" }}

And now we have a fully working Ember.js drag and drop file upload o/

celebrate

The ember-cli addon

Using the contents of this article I generated the ember-drag-n-drop-upload ember-cli addon. Install it by simply running the following command:

ember install:addon ember-drag-n-drop-upload  

And be happy ;)

References

Update, August 13th 2015

Hello boys and girls :), I know that an update to this article is anticipated by some folks. The reason why I have not done so is both for very busy times, and also to wait for the 2.0 version of Ember to be out in the wild.

As we finally have our 2.0 version delivered, read Ember.js 2.0 Released. I can now focus on a deserved update to file upload with Ember.js :).

That's it for now, till next time o/