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.

jQuery & Navigation: sliding sub navs

We’ve all seen those web sites that have navigation links that, when clicked, slide down a sub navigation. I recently put one together for thatwasmean.com and thought I would take a moment to clean up, generalize, and make easily viewable an example. You can find the working example in my examples directory, here.

To make this functionality possible I utilized jQuery’s slideUp and slideDown functionality. This is a neat little function that changes the display property from displaying the full element to slowly hiding the top part of it until the whole element is hidden, at which point the display CSS attribute is set to none.

I am going to put the main parts of the example here in this point, but keep in mind you will have to set up the remaining html tags (like body and head) as well as include the jQuery framework. It is encouraged to view the working example to see some of the added styling to make things look a little prettier (since in this post scope I am only including the necessities)

Let’s begin with the CSS styles to set things up. We have three sub navigation items and so we need to set those three sub nav items with a display property set to none. You will also note I have modified text alignment in here, since each sub nav requires different aligning. The ‘flash_about’ is the container for the navigation as well as the sub navigation items. This is easy to change if you wish, but I stuck it all in one place as it suited my needs.

#flash_about {
  text-align:right;
}
.sub_nav_box {
  font-weight: normal;
  padding:0.3em 1em 1em;
  border-top:1px dotted #C1CDCD;
}
#add_new_thing {
  display: none;
  text-align: left;
}
#about {
  display: none;
  text-align: left;
}
 
#browse_by {
  display: none;
  text-align: center;
}
#browse_by &gt; .link {
  margin-left: 0.2em;
}
.nav_link {
  margin-left: 1em;
}

Next we need our JavaScript… using the jQuery functions mentioned above and a little jQuery helper magic we have a toggle_div function that takes a div ID and will toggle that div. This function is cool in the sense that it will hide all other opened sub nav items and only show the one you are selecting. Further, if you click a navigation link and its corresponding sub navigation item is already being displayed then the function will slide up that sub navigation item. (The $ symbol represents a call to jQuery.)

function toggle_div(div_id) {
  var div = $('#'+div_id);
  var divs = $('#flash_about').children('div').not(div);
 
  if(div.css('display') == 'none')
  {
    divs.slideUp(function() {
      div.slideDown();
    });
  }
  else
  {
    div.slideUp();
  }
}

Lastly, let’s get the HTML in here to make all of this mean something to the viewer. I have left some content in the sub navigation items to give the overall example some meaning. (I strongly dislike examples where everything is named ‘example 1, example 2, examples 3– show me some content good sir!)

<div id="flash_about">
 
  <a class="nav_link" onclick="return toggle_div('about');" href="#">About</a>
  <a class="nav_link" onclick="return toggle_div('browse_by');" href="#">Categories</a>
  <a class="nav_link" onclick="return toggle_div('add_new_thing');" href="#">Submit something</a>
<div id="add_new_thing" class="sub_nav_box">
<div style="margin-bottom: 0.4em; margin-top: 0.4em;">
      <span id="add_new_thing_title" style="color: #333; font-size: 16px;">Type your thing here... </span></div>
<textarea id="add_new_thing_body" class="thing_input" style="border: 2px solid #666;" cols="40" rows="10"></textarea>
<div id="new_thing_buttons">
      <a class="button" href="#">add new thing</a> or <a onclick="toggle_div('add_new_thing');" href="#">cancel</a></div>
</div>
<div id="about" class="sub_nav_box">
    Welcome to my web site. This is the about us... it is awesome. Feel free to browse the site and check out some of our cool features.</div>
<div id="browse_by" class="sub_nav_box">
    <a class="link" href="#">link</a>
    <a class="link" href="#">link</a>
    <a class="link" href="#">link</a>
    <a class="link" href="#">link</a>
 
    <a class="link" href="#">link</a>
    <a class="link" href="#">link</a>
    <a class="link" href="#">link</a></div>
</div>

And there we have it. Go check out the example to see it in action. I hope you enjoyed this post. If you have any requests on JavaScript/jQuery examples let me know… As I find more time I will put up more examples of common uses of JS & jQuery.