Sortable Lists In Meteor using JQuery UI


by Bruce Hubbard

Most of the time in a Meteor project you want to avoid handling the DOM directly and instead rely on Meteor's reactivity to do the work for you. There are times though when you have to take matters into your own hands like handling element animations or in our case for this blog post: sorting a list of objects. This post is inspired by the Meteor.com blog post that does the same thing but no longer works due to API changes: (https://www.meteor.com/blog/2013/09/13/previewing-meteors-new-rendering-engine-reactive-sortable-lists)

First Some Setup

// create a meteor project
% meteor create meteor-sortable
% cd meteor-sortable
   
// add the jquery-ui package
% meteor add mrt:jquery-ui
   
// run our server
% meteor

This will create 3 files in the meteor-sortable folder: meteor-sortable.css, meteor-sortable.html and meteor-sortable.js. Normally we'd structure our app a little differently (taking advantage of the client and server folders) but for the sake of brevity we're just going to keep everything in these three files.

You can now pop open your web browser and go to http://localhost:3000

Next up we're going to create a new Collection in Mongo and seed it with some default data:

Creating Our Collection and Seed It

Add this to the javascript file that was created when you created the project. It creates a new Mongo collection named "items" and seeds it with some data.

meteor-sortable.js

Items = new Mongo.Collection('items')
 
//server check only required because this code
// is running on both client AND server
if(Meteor.isServer) {
  //Only seed on the server
  Meteor.startup(function() {
    //AND only seed if there are no items
    if(Items.find({}).count() == 0) {
      for(var i = 1; i <= 10; i++) {
        Items.insert({
            title: "Item " + i,
            rank: i
        })
      }
    }
  })
}

If you pop open the console in your web browser you can see the data (without refreshing your browser!!!) by running

console.table(Items.find().fetch())

Alt text

Add some html/css

Next we'll add some very basic html/css. If you're familiar at all with Meteor or at least Handlebars this should look pretty straightforward:

meteor-sortable.html

<head>
  <title>meteor-sortable</title>
</head>
 
<body>
  <h1>Meteor Sortable</h1>

 
  {{> items}}
</body>
 
<template name="items">
  <div id="items">
  {{#each items}}
    <div class="item">{{title}}</div>
  {{/each}}

  </div>
</template>

meteor-sortable.css

.item {
  display: block;
  padding: 5px;
  margin: 5px;
  background-color: #ccc;
  border: 1px solid #aaa;
  width: 200px;
  cursor: move;
}

Blaze.getData: The bridge between the DOM and your Data

In our example above our "items" template loops through our array of items and creates a div for each one. Did you know that Meteor remembers what data was in scope for every html element on the page?

There's a built in function called Blaze.getData that takes as a parameter an html element and returns the data that was in scope when it was created! That makes dealing with the DOM directly a LOT easier. Pop open the web console and play around with it for yourself:

Alt text

This will work for ANY html element, not just top level elements in our templates. It will even work when you have multiple templates rendering that have different data contexts!

Adding Sorting To Our Lists

Now that we know how to tie our DOM elements back to data tasks like integrating with JQuery UI become a lot easier. Since we're dealing directly with DOM elements within the JQuery code Blaze.getData saves us a lot of time/money/headaches by serving as a bridge between the DOM and our data:

meteor-sortable.js

...Code From Above omitted for succinctness...
 
//Only required because this code is running on
//  the client AND server
if(Meteor.isClient) {
 
  //Add some handlebars helpers to our Template.
  //  This one handily enough returns our Items in rank order
  //  Since Meteor is reactive, whenever our Items change Meteor
  //    will re-render our Template (putting them in the correct order)
  Template.items.helpers({
    items: function() {
      return Items.find({}, {sort: {rank: 1}})
    }
  })
 
  //Once the Template is rendered, run this function which
  //  sets up JQuery UI's sortable functionality
  Template.items.rendered = function() {
    this.$('#items').sortable({
        stop: function(e, ui) {
          // get the dragged html element and the one before
          //   and after it
          el = ui.item.get(0)
          before = ui.item.prev().get(0)
          after = ui.item.next().get(0)
 
          // Here is the part that blew my mind!
          //  Blaze.getData takes as a parameter an html element
          //    and will return the data context that was bound when
          //    that html element was rendered!
          if(!before) {
            //if it was dragged into the first position grab the
            // next element's data context and subtract one from the rank
            newRank = Blaze.getData(after).rank - 1
          } else if(!after) {
            //if it was dragged into the last position grab the
            //  previous element's data context and add one to the rank
            newRank = Blaze.getData(before).rank + 1
          }
          else
            //else take the average of the two ranks of the previous
            // and next elements
            newRank = (Blaze.getData(after).rank +
                       Blaze.getData(before).rank)/2
 
          //update the dragged Item's rank
          Items.update({_id: Blaze.getData(el)._id}, {$set: {rank: newRank}})
        }
    })
  }

The power of Meteor

Sorting lists might not seem like a big deal but let's take a step back and think about what we just accomplished. Our project consisted of:

  • 37 lines of js (after removing comments and half of those are end brackets)
  • 17 lines of html
  • 10 lines of css

That's not a lot of code but think about what we did:

This is a FULL STACK application. When you move an item it saves the change to a database (not just locally on your browser).

IT'S REACTIVE! If you open up two (or more) browser tabs side by side and make changes in one you should almost immediately see the change in the other browser!

That is a tiny amount of code but a tremendous amount of functionality and quite frankly revolutionary.

Try It Out

You can deploy this app out using Meteor's free hosting (meant for small/toy apps) using the command line:

  meteor deploy <MY_APP_NAME>.meteor.com
   
  #example: meteor deploy sortable-lists-jqueryui.meteor.com

I've deployed this out to sortable-lists-jqueryui.meteor.com

Tab 1 (iframe)

Tab 2 (iframe)

Share Share on Twitter Share on Facebook Share on LinkedIn

How Can We Help?

Reaching out doesn’t mean you’re ready to start a project, but we’d love to learn more about the challenge you’re facing, answer any questions, and see if we might be a good fit for working together.

Contact Us