Rails, jQuery UI (Sortable), and Ordering of Slides

I am partly sharing this issue and solution to the world, but mostly just recording somewhere what I came up with. Now, onto the quick read.

A web design client recently asked me to build a web site for him that would allow him to create slide shows of his art work. One of the criteria was to create a way to set the order and, at a later date, re-arrange slides in the slide shows. Turning to jQuery UI, specifically the Sortable (doc), and a simple rails controller this task was pretty trivial. My initial concern was that a slide show could consist of hundreds of slides and doing any sort of ajax updating of the slides would be too slow. Turns out, this was a real concern and after reading all kinds of blogs I was unable to find a work-able ajaxively awesome solution. So, I turned back to the non web2.0 design of just having a Save Order button. The methodology of my solution is straight forward: allow the user to drag and drop to any order configuration they please and then save that order. Upon clicking save via the user interface, invoke the following javascript command that will build the query string and do a window relocation. I’d rather use GET than POST for no real reason outside of having to hack around the authenticity token. After all, what’s the point of having an auth token if you’re just going to override it in a members-only section. Here is the complete code for the view, broken into pieces for clarity.

CSS:

  #sortable { list-style-type: none; margin: 0; padding: 0; width: 100%; }
  #sortable li { margin: 0 3px 3px 3px; padding: 0.4em; padding-left: 1.5em; font-size: 1.4em; height: 18px; }
  #sortable li span { position: absolute; margin-left: -1.3em; }

JS:

<script type="text/javascript">
// Initialize sortiable on #sortable div
$(function() {
	$("#sortable").sortable();
	$("#sortable").disableSelection();
});
// Prepare and go-to proper url for updating order of slides.
// Called via html anchor tag by user
function update_order() {
  window.location.href = "/slides/order?"+$('#sortable').sortable('serialize');
}

Sample #sortable:

<ul id="sortable">
	<li id="slide_2" class="ui-state-default full-width"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span>Lack of a better Title</li>
	<li id="slide_3" class="ui-state-default full-width"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span>A fireplace for two</li>
	<li id="slide_1" class="ui-state-default full-width"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span>My Super Sweet Villa</li>
	<li id="slide_4" class="ui-state-default full-width"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span>A view from above</li>
</ul>

Anchor link:

<a href="#" onClick='update_order(); return false()'>Save Order</a>

Controller:

  def order
    if params[:slide]
      params[:slide].each_with_index { |slide, index| Slide.update(slide.to_i, :slide_position => (index +1)) }
      flash[:notice] = "Slide order has been updated."
      redirect_to(slides_path)
    else
      @slides = Slide.ordered
    end
  end

Now, for some disclaimers… You should add in error handling to all of these pieces! For brevity, I’ve included the main pieces of the task and not my error handling code. You can also use post and add in (instead of looking for params[:slide]) if request.post?… Just don’t forget to put in the authenticity_token to your ajax or form post (forms do this automatically in rails if protect_from_forgery is enabled).

Efficiency: This is a O(n) algorithm, because it has to iterate over each element being sorted and do three things: fetch from the database, update the attribute, and save. The last two steps can be combined. I’m using the handy ActiveRecord::Base extension in Rails called update to do this. Here’s that code so you don’t have to look it up:

     # File vendor/rails/activerecord/lib/active_record/base.rb, line 744
744:       def update(id, attributes)
745:         if id.is_a?(Array)
746:           idx = -1
747:           id.collect { |one_id| idx += 1; update(one_id, attributes[idx]) }
748:         else
749:           object = find(id)
750:           object.update_attributes(attributes)
751:           object
752:         end
753:       end

I was unable to locate any kind of conditional MySQL update that would allow different values to be updated for different rows all in one swoop. I’ll admit my research on this topic was about 30 minutes of blog and StackOverflow reading… If you have something more efficient please reply.

Here are the link(s) to relevant information:
jQuery UI Demos (and download) — I’m using the latest code base as of this post. The styles you see within the bullet list items are from the jQuery UI theme.