RESTful Drag & Drop with Rails and Multiple Models

For a client project we recently had to implement a sortable list using jQuery UI’s sortable plugin. …but with a nasty twist: The collection being sorted consisted of two unrelated models.

The technique not only does this in a highly-RESTful manner, but also has the added benefit of pushing all of your jQuery sorting needs into a single controller, no matter what models your working with. Here’s how we did it.

The problem

In our situation, we have a model that has many Foos and Bars.

class Container < ActiveRecord::Base
  has_many :foos
  has_many :bars
  #...

We want to render these Foos and Bars to the user as though they were all part of the same collection, and we want to allow the user to sort the Foos and Bars together.

drag-n-drop

Normal jQuery UI Sortable Usage

The well-documented way of using the jQuery UI sortable module is to have it serialize the DOM and send that in via a POST to your backend. This works handily with a single model, but the serialization loses the position information when dealing with multiple models.

The traditional DOM (generated by div_for) looks like this:

<div class="sortable">
  <div id="foo_1"  class="foo">…</div>
  <div id="bar_1"  class="bar">…</div>
  <div id="foo_6"  class="foo">…</div>
  <div id="bar_3"  class="bar">…</div>
  <div id="foo_23" class="foo">…</div>
</div>

The URL-encoded data comes in like such:

foos[]=1&amp;bars[]=1&amp;foos[]=6&amp;bars[]=3&amp;foos[]=23

…where the order of the params represents the new sorted order. Once Rails unencodes it, we end up with two arrays of IDs:

params = {
  foos: [1, 6, 23],
  bars: [1, 3]
}

As you can see, we now no longer know how to mingle the Foos and Bars. What we want is something more like this:

params = {
  0 => [“foo”, 1],
  1 => [“bar”, 1],
  2 => [“foo”, 6],
  3 => [“bar”, 3],
  4 => [“foo”, 23]
}

Let’s see how we can make that happen.

Sorting the Output

First of all, let’s render the DOM such that the two collections are intermingled, and sorted according to the #position attributes. We’ll add a method on the model to do this:

class Container < ActiveRecord::Base
  has_many :foos
  has_many :bars

  def foos_and_bars
    [*foos,*bars].sort_by(&:position)
  end     
  #…
end

Normally, sorting in ruby would be a huge performance no-no. In this case we know we’re dealing with at most a dozen items on any given page load, which gives us the ability to do things the easy way.

We then loop over this pseudo-collection in the view:

Foos and Bars

<% container.foos_and_bars.each do |model| %> <%= render model %> <% end %>

The DOM

Next, we need to add some identifying data to the rendered DOM, which we can send back in the POST. Like we saw earlier, using div_for and serialize doesn’t do what we need. We’ll use HTML5 data- attributes to be a bit more explicit.

Foos and Bars

<% container.foos_and_bars.each do |model| %> <%= div_for(model, class:"element", data: {id: model.id.to_i, model: model.class.to_s}) do %> <%= render model %> <% end %> <% end %>

The nested data hash in there produces well-formed HTML5 data attributes:

...

The Javascript

We add an update: entry to the normal sortable() call, which does two things:

  1. Constructs a new_order array that contains the [id, class]pairs.
  2. Posts that array as sorting to a single RESTful Sortings controller

For example:

$(function() {
  $( “.sortable” ).sortable({
    axis: ‘x’,
    opacity: 0.4,
    update: function() {
      var new_order = [];
      $(this).find(“.element”).each(function(index, element) {
        new_order.push([ \$(element).data().id, $(element).data().model ]);
      });
      $.post(“/sortings”, { sorting: new_order })
    }
  });
  $( “.sortable” ).disableSelection();
});

The Controller

Our controller is a normal RESTful resource, which fronts a Sorting class.

class SortingsController < ApplicationController
  load_and_authorize_resource

  def create
    sorting.save
    render nothing: true, status: 200
  end
end

We're using CanCan in this application, and we dry up our controllers with load_and_authorize_resource. If you weren't using CanCan, you'd do something like sorting = Sorting.new(params[:sorting]) inside the create method.

Now we hook up the routes:

MyApp::Application.routes.draw do
  resource :sortings
  # …

And the rest of the magic happens in the model.

The Model

The Sorting model is a PORC (plain old ruby class), which quacks like an ActiveModel.

class Sorting
  attr_accessor :new_order

  def save
    ActiveRecord::Base.transaction do
      new_order.each do |position, id_model_pair|
        update_position(position, id_model_pair)
      end
    end
  end

  def update_position(position, id_model_pair)
    instance(id_model_pair).update_attributes(position: position)
  end

  def instance(id_model_pair)
    model_id, model_class = *id_model_pair
    model_class.constantize.find(model_id)
  end

  # Quack like a model…
  def initialize(new_order = {})
    self.new_order = new_order
  end

  extend ActiveModel::Naming
  include ActiveModel::Conversion

  def persisted?
    false
  end
end

The point of this model is to coordinate the sorting of the multiple models. There are arguments about what kind of GoF pattern this is. I personally don’t care if you call it a Decorator, Composite or Presenter. It’s clean, easily understood, and gets the job done.

Bonus

Note that the SortingController and the Sorting model both have no knowledge of the Foo and Bar models. They simply work in concert to change the sorting order of any models that come their way. This means we can use this single route (/sortings) for all of our sorting needs! That’s a pretty big win on its own.

Security Concern?

The astute reader will note that there’s a potential security risk here. By using #constantize (a wrapper around #eval) on a user-supplied string, we’re giving the user a chance to execute arbitrary code. This is important to be aware of, but is mitigated by three facts:

  1. Only authenticated and trusted users are able to access this method for our particular use case.
  2. The only method being called on the generated class is #position=, meaning at most, a user can change the position of an item.
  3. If the user attempts to inject a class that doesn’t respond to #position=, they’ll get a 500 due to a MethodMissing exception. Raising an exception is absolutely the right thing to do here.

Discuss this post on Hacker News


*Keep it real, we'll erase shit we don't like.*
comments powered by Disqus


Thunderbolt Labs offers Engineering, Mentoring, and CTO Advising services for those who demand the best.

Follow us on twitter or email us to discuss your next project.


Thunderbolt Labs
Thunderbolt Labs

…The myth and the legend itself