Drag and drop file uploads with Ember.js
As I’ve mentioned countless times before Ember.js is really hard to understand and to get it’s concepts right. Without some deeper knowledge knowing how to do trivial things can get pretty tricky. Uploading files with Ember.js using drag and drop is no exception here. Common task to solve which using vanilla stack will only require to include any available JavaScript library for drag and drop file upload may look impossible to solve for some using Ember.js.
Ember.js provides us with View Objects which we will use to solve this particular problem. First thing you need to know is that View Object has it’s own template which can be defined in two different ways.
- By creating it’s own separate template and providing View Object with a reference to the template.
- By defining a region marked by a {{#view}} block expression.
First technique is fairly simple and is well known. It could look something like this:
<script type="x/handlebars" id="viewTemplate">
<!-- Your awesome HTML codes go in here -->
</script>App.SomeView = Ember.View.extend({
templateName: 'viewTemplate
});
Second one is a little bit tricky and not so popular among developers I’ve talked to. What you do is you wrap a piece of existing HTML markup (which is a part of another template) into the {{#view}} block expression:
<script type="x/handlebars" id="viewTemplate">
<h1>I am a list of all Files</h1> {{#each}}
{{#view App.SomeView}}
<p>{{name}}</p>
{{/view}}
{{/each}}
</script>
This way you won’t have to define a separate template for your view. Now that you know some basic theory behind views lets move to our primary goal — creating a drag and drop file upload with Ember.js.
Lets upload all the files!
As you’ve guessed so far we are going to use View Object to make file upload possible. We will create UploadView with three methods:
App.UploadView = Ember.View.extend({
dragEnter: function(event) {
event.preventDefault();
}, dragOver: function(event) {
event.preventDefault();
}, drop: function(event) {
event.preventDefault();
}
})
If we won’t preventDefault() event on these three methods then all the hell will break loose and it won’t work.
Next we need a template. We are going to define it in one that’s already existing:
<script type="x/handlebars" id="someTemplate">
{{#view App.UploadView}}
<h1>I am a list of all Files</h1> {{#each}}
<p>{{name}}</p>
{{/each}}
{{/view}}
</script>
Now whenever you will drag/drop file anywhere on someTemplate the methods of View Object will be triggered which at this point will cause default events of the browser not to fire.
First two methods (dragEnter() and dragOver()) are up to you — here you probably want to implement some style changes or some other UI transformation effects to let your user know that he can drop file to start upload. You can select any element inside the view using the following syntax (this particular example will select first paragraph and remove it):
App.UploadView = Ember.View.extend({
dragEnter: function(event) {
event.preventDefault();
this.$('p:first').remove();
}
})
To make uploads possible we are going to extend our drop() method and use XMLHttpRequest. Lets start by defining the backbone of the drop() method:
App.UploadView = Ember.View.extend({
drop: function(event) {
event.preventDefault(); xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
}
})
We’d probably want to send files to our server instead of some hardcoded data. We can easily extract files from the event parameter and then send them to the server using xhr.send() method:
App.UploadView = Ember.View.extend({
drop: function(event) {
event.preventDefault(); var files = event.dataTransfer.files;
var formData = new FormData(); for (var i = 0; i < files.length; i++) {
formData.append('file', files[i]);
} xhr = new XMLHttpRequest();
xhr.open('POST', '/upload'); xhr.send(formData);
}
})
After you dispatch your data to the server, you will probably create a new record there. But what about the client side of our application? We can easily solve that by adding xhr.onload() method which will fire after the xhr.send() method finishes sending the data and receives answer from the server:
App.UploadView = Ember.View.extend({
drop: function(event) {
event.preventDefault(); var files = event.dataTransfer.files;
var formData = new FormData(); for (var i = 0; i < files.length; i++) {
formData.append('file', files[i]);
} xhr = new XMLHttpRequest();
xhr.open('POST', '/upload'); xhr.onload = function() {
App.File.store.push('file', {
id: 1,
name: 'Name'
}
} xhr.send(formData);
}
})
Note that inside xhr.onload() method you have xhr.responseText available which contains the response from your server. You can parse it to set attributes of the record you push to the store. Also note that you can push the record to the store in different ways. Here is one of them:
this.get('controller').get('store').push('file', {
id: 1,
name: 'Name'
})
Adding progress bar
Lets expand our View Object a little bit by adding a progress bar to indicate the progress of file upload. First we need to add the progress bar to our template:
<script type="x/handlebars" id="someTemplate">
{{#view App.UploadView}}
<h1>I am a list of all Files</h1> <progress min="0" max="100" value="0"></progress> {{#each}}
<p>{{name}}</p>
{{/each}}
{{/view}}
</script>
And to update our <progress> element we will use xhr.upload.onprogress() method which will first make sure that event has length that can be calculated and then it will update the value attribute of <progress> element:
App.UploadView = Ember.View.extend({
drop: function(event) {
event.preventDefault(); var files = event.dataTransfer.files;
var formData = new FormData(); for (var i = 0; i < files.length; i++) {
formData.append('file', files[i]);
} xhr = new XMLHttpRequest();
xhr.open('POST', '/upload'); xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
var complete = (event.loaded / event.total * 100 | 0);
this.$('progress').value = complete;
}
} xhr.onload = function() {
App.File.store.push('file', {
id: 1,
name: 'Name'
});
} xhr.send(formData);
}
})
Now if you initiate upload by dropping any file on the view you will see the progress bar advancing as the file is uploaded to the server.
Final thoughts
As you’ve seen the only difficult part here was to understand Ember’s concepts. Everything else was common stuff. The only caveat here can be the fact that XMLHttpRequest is supported by IE 10+. But in my opinion the time has come to forget the browser which was released three years ago; things changed since then. And the sooner you drop support for
pre-historic browsers, sooner your users will be forced to update their software.
Shameless self-promotion
If you are still struggling with Ember.js you can try the book I’ve
wrote — Ambitious Ember Applications which is a comprehensive Ember.js tutorial. During the course of the book you will build a simple application to help you understand concepts behind Ember.js. After finishing this book you should have enough knowledge to start building complex Ember.js applications.