Drag and drop sorting lists of records in an Ember 2 application, using JQuery UI’s sortable plugin! Working example up on GitHub.
I’ve been rebuilding Three D Radio‘s internal software using Ember JS. One aspect is to allow announcers to create playlists to log what they play on air. I wanted announcers to be able to reorder tracks using simple drag-n-drop. In this post I’ll explain how to do it.
Firstly, this post is based on the work by Benjamin Rhodes. However, I found that his solution didn’t work out of the box. Whether that is API changes from Ember 1.11 to Ember 2.x I’m not sure. So what I’m going to do here is bring his technique up to date for 2016 and Ember 2.6.
Starting an Ember Project
I’ll build this from scratch so we have a complete example. You shouldn’t have problems integrating this into an existing project though. So we’ll create a new Ember CLI project called sortable, and install JQuery UI:
ember new sortable-demo
cd sortable-demo
bower install --save jqueryui
We need to add JQuery UI to our build as well.
// ember-cli-build.js
var EmberApp = require("ember-cli/lib/broccoli/ember-app")
module.exports = function (defaults) {
var app = new EmberApp(defaults, {
// Add options here
})
app.import("bower_components/jquery-ui/jquery-ui.js")
return app.toTree()
}
Models
We are going to need a model for the data we are going to sort. Here’s something simple
ember generate model note
Inside the note model we’ll have two attributes, the content of the note, and an index for the sorted order:
// app/models/note.js
import Model from "ember-data/model"
import attr from "ember-data/attr"
export default Model.extend({
index: attr("number"),
content: attr("string"),
})
Fake data with Mirage
For the sake of this example, we’ll use Mirage to pretend we have a server providing data. Skip this bit if you have your REST API done.
ember install ember-cli-mirage
And provide some mock data:
// app/mirage/config.js
export default function () {
this.get("/notes", function () {
return {
data: [
{
type: "notes",
id: 1,
attributes: {
index: "1",
content: "Remember to feed dog",
},
},
{
type: "notes",
id: 2,
attributes: {
index: "2",
content: "You will add tests for all this, right?",
},
},
{
type: "notes",
id: 3,
attributes: {
index: "3",
content: "Learn React Native at some point",
},
},
],
}
})
}
A Route
We will need a route for viewing the list of notes, and a template. Here’s something simple that will do for now:
ember generate route list
And in here we will simply return all the notes:
// app/routes/list.js
import Ember from "ember"
export default Ember.Route.extend({
model() {
return this.store.findAll("note")
},
})
A template? No, a component!
We are going to display our notes in a table, but the Sortable plugin also works on lists if that’s what you’d like to do.
You may be tempted to just put your entire table into the list template that Ember created for you. However, you won’t be able to activate the Sortable plugin if you try this way. This is because we need to call sortable after the table has been inserted into the DOM, and a simple route won’t give you this hook. So, we will instead create a component for our table!
ember generate component sortable-table
We will get to the logic in a moment, but first let’s render the table:
// app/templates/components/sortable-table.hbs
<table>
<thead>
<tr><th>Index</th><th>Note</th></tr>
</thead>
<tbody class="sortable">
{{#each model as |note|}}
<tr>
<td>{{note.index}}</td>
<td>{{note.content}}</td>
</tr>
{{/each}}
</tbody>
</table>
The important part here is to make sure your table contains and components. We add the class sortable to the tbody, because that’s what we will make sortable. If you were rendering as a list, you add the sortable class to the list element.
Finally, in the template for our route, let’s render the table: We should have something that looks like this:
Let’s make this slightly less ugly with a quick bit of CSS. Which gives us a more usable table:
Moving over to our component’s Javascript file, we need to activate the sortable plugin. We do this in the didInsertElement hook, which Ember calls for you once the component has been inserted into the DOM. In this method, we will look for elements with the sortable class, and make them sortable! At this point we have a sortable table where users can drag and drop to re-order elements. However, this is purely cosmetic. You’ll see that when you reorder the table the index column shows the numbers out of order.
Open up Ember Inspector and you will see the models’ index is never being updated. We’ll fix this now. The first step is to store each note’s ID inside the table row that renders it. We will make use of this ID to update the index based on the order in the DOM. So a slight change to our component’s template: Next is to give sortable an update function. This gets called whenever a drag-drop is made. This function iterates over all the sortable elements in our table. Note that we get them from JQuery in their order in the DOM (ie the new sorted order). So, we create an array, and using the item’s ID store the index for each element. Note that I’m adding 1 to my indices to give values from 1 instead of 0. Next step is to use this array to update the records themselves: We update and save the record only if its index has actually changed. With long lists, this greatly reduces the number of hits to the server. (wish list: A method in ember that will save all dirty records with a single server request!)
And we’re done! A sortable list of Ember records, that persist those changes to the server*. Have a look on GitHub! (Note: If you’re using Mirage, you’ll get errors about saving records, because we need code to handle patches).{{sortable-table model=model}}
A quick detour to CSS
/* app/styles/app.css */
body {
font-family: sans-serif;
}
table {
width: 500px;
}
table th {
text-align: left;
}
tbody tr:nth-child(odd) {
background: #ddd;
}
tbody tr:nth-child(even) {
background: #fff;
}
Make Them Sortable
// app/components/sortable-table.js
export default Ember.Component.extend({
didInsertElement() {
Ember.$(".sortable").sortable()
Ember.$(".sortable").disableSelection()
},
})
Persisting the records
app/templates/components/sortable-table.hbs
<pre class="lang:xhtml decode:true" title="">
<tr data-id="">
<td></td>
<td></td>
</tr>
// app/components/sortable-table.js
didInsertElement: function() {
let component = this;
Ember.$('.sortable').sortable({
update: function(e, ui) {
let indices = {};
$(this).children().each( (index, item) => {
indices[$(item).data('id')] = index+1;
});
component.updateSortedOrder(indices);
}
});
Ember.$('.sortable').disableSelection();
}
// app/components/sortable-table.js
updateSortedOrder(indices) {
this.beginPropertyChanges();
let tracks = this.get('model').forEach((note) => {
var index = indices[note.get('id')];
if (note.get('index') !== index) {
note.set('index',index);
note.save();
}
});
this.endPropertyChanges();
},