Project Website Part 4: Drag and Drop in jQuery

4BE91917-A3EB-4A6A-BB40-BC4A4F929926.jpg

Drag and drop is like those Nutty Bars snack things.

Allow me to explain.

I like Nutty Bars. But I never expect to like them. They are kind of funny looking, for one thing, what with that weird criss-cross pattern on the top, and the chocolate never quite covering the wafers. But when I get past that and actually eat one, it's actually kind of tasty.

Which brings me to drag and drop. Which I always expect is going to be an overwhelming pain in the neck (probably based on bad experiences using Java Swing). But whenever I manage to get over it and actually implement a web drag and drop, I'm always surprised at how easy it is using an Ajax framework.

jQuery is no exception.

The drag and drop functionality for the web site is relatively simple. The home page for this site displays the brands whose products are available via the site. The brands are displayed in one of two groups -- brands for which the client is an exclusive distributor, and brands for which the client is not (there are also some brands that don't show on the home page at all).

In the current Jython version, these lists are -- ugh -- maintained as values in the actual code base. Among the many problems with this plan is that it required the programmer -- me -- to be involved anytime a brand's status changed. For the new version, I wanted to give the client the opportunity to manage this by themselves.

A drag and drop interface showing the brands in roughly their home page positions and allowing the user to drag brands from state to state seemed the way to go.

The Rails Setup

The Rails code is all in helpers that define each section -- all the ERb does is call the following helper, which sets up a parent div named display_container, and calls a separate helper for each section:

  def display_container
    Html.div(:id => "display_container") do |div|
      %w(exclusive, other, none).each do |state|
        div << display_type_section(state.titlecase, @brands[state])
      end
    end
  end

The section helper is also pretty simple:

  def display_type_section(type, brands)
    brands ||= []
    Html.div(:class => :display_type,
        :id => "display_type_#{type.downcase}") do |div|
      div << Html.h2("Brands displayed as #{type}")
      div << table_into_cells(brands.map { |b| brand_span(b)}, 4)
    end
  end
 
  def brand_span(brand)
    Html.span(brand.name, :class => :brand_span,
        :id => dom_id(brand, :span))
  end

Each brand status gets its own div with an ID of "display_type_#{type.downcase}", inside of which the brands with that status are arranged in a table four across (using the table_into_cells helper, not shown). Each brand gets it's own span.

Dragging

That should give you the setup. Next is to make things draggable. Each brand item has a class of brand_span and they all need to be draggable. The jQuery for that is pretty easy. Note that this does require the jQuery UI plugin, which comes with jRails, but can also be downloaded from http://ui.jquery.com/.

$(function() {
  $(".brand_span").draggable({containment: "#display_container"});
});

As you may recall from the last jQuery post, the $(function() setup allows all the code in that function to be executed when the page DOM has been loaded but (usually) before the page displays.

The draggable method takes a jQuery selection set and makes all the items in that set draggable. It takes a boatload of options, which you can see in detail here. In this case, the containment option limits the range of the draggables to the item defined by the selection string, in this case the parent container of all the sections.

Dropping

At this point the brand elements can be moved willy-nilly around the screen, but they don't actually do anything. In order to get the page to do something you need to define a droppable element. Still inside the jQuery main function:

    $(".display_type").droppable({
            accept: ".brand_span",
            hoverClass: "active",
            drop: function(event, props) {
              $("#display_container").load("brand_display/change",
                  {drag_id: $(props.draggable).attr("id"),
                   drop_id: $(this).attr("id")});
            }
      });

This code is still pretty simple, but it has more options defined, here's the breakdown:

The droppable method makes all members of the selection set aware of draggable objects. Like draggable, the argument is an options hash. Unlike draggable, you actually need to specify some of the options to get actually useful behavior. A full list of options is available here. The options defined in this call are as follows:

  • accept: Defines what kind of draggable can be dropped on the element. In this case, I used a jQuery selector -- anything with a class of brand_span will work. In more complicated cases, you can use a function as the argument instead.
  • hoverClass
  • : A CSS class added to the droppable when a draggable is hovering over it, and removed when it's done. The class I defined turns the text in the droppable red. There is also activeClass, which acts whenever the droppable is a valid target of the current drag, regardless of where the element being dragged actually is.

  • drop: A function that is called when an element is actually dropped. Within the function $(this) represents the target droppable, and the props element contains the draggable object as $(props.draggable).

Inside the drop function, the code makes an Ajax call back to the server. This call makes the database change and returns the HTML for the entire display container, which is more than is strictly necessary to redraw, but much cleaner from a code perspective. The DOM IDs of the draggable and droppable elements are sent to the server to convert to the actual ID's -- here's the server side controller method, note that all the render does is do an inline ERb call to the same helper method.

  def change
    brand_id = params[:drag_id].split("_")[-1]
    @brand = Brand.find(brand_id)
    @brand.update_attribute(:display_status,
		params[:drop_id].split("_")[-1])
    @brands = Brand.by_name.all.group_into_hash(&:display_status)
    respond_to do |format|
      format.js { render
		:inline => "<%= display_container %>" }
    end
  end

Refactoring

There's only one problem with the code so far, and if you read last week's jQuery post, you may have guessed it already. The drag and drop status of each individual element is set by jQuery on DOM ready and is not re-invoked for the new HTML returned by the Ajax call. As a result, the code so far only works once -- after that, nothing is set to drag or drop.

After banging my head against this one for a bit, I realized it could be fixed with a bit of refactoring, coupled with a third argument to the load method that is a callback invoked after the Ajax call completes. The code in my project actually looks like this:

    function make_draggable() {
      $(".brand_span").draggable(
		{containment: "#display_container"});
    }
 
    function make_droppable() {
      $(".display_type").droppable({
            accept: ".brand_span",
            hoverClass: "active",
            drop: function(event, props) {
              $("#display_container").load("brand_display/change",
                  {drag_id: $(props.draggable).attr("id"),
			  drop_id: $(this).attr("id")},
                  function() {make_draggable(); make_droppable();});
            }
      });
    }
 
    $(function() {
      make_draggable();
      make_droppable();
    });

This code sets up the make_draggable and make_droppable segments of the jQuery setup as separate functions, called both by the jQuery ready function and then again by the callback of the Ajax call -- this works even though the callback calls make_droppable and is defined inside make_droppable. Works like a charm now, and it's cleaner and easier to read at the same time.

Next Time

The Joys of Morph.

Related Services: Ruby on Rails Development, Ajax Rich Internet Applications
, Custom Software Development

Related posts:

  1. Scriptaculous Drag and Drop with AJAX Update Fix for IE
  2. Project Website, Part Two: Simple jQuery With Rails
  3. Multiple Column Sorting with Drag and Drop using Scriptaculous
  4. Project Website, Part One: Migrating Data
  5. Project Website Part 0: Introduction

Comments: 3 so far

  1. You might want to look at LiveQuery: http://brandonaaron.net/docs/livequery/

    It handles the apply-to-new-elements bit that you were struggling with manually.

    Comment by Jamie, Friday, July 25, 2008 @ 2:18 pm

  2. Good write up!

    I’m a few weeks away from finishing a rails 2 rewrite of an bookmark app I made a few years ago called HomeMarks. I’d say I’m in the middle of nested drag and drop hell now. If you care to take a look at some of the solutions I have, please check out the project page on Github http://github.com/metaskills/homemarks/tree/master

    One thing I ended up creating was a SortuableUtils JavaScript module that I use to serialize and dynamically add/remove sortables/draggables/droppables. The serialization is the nicest. The default way Scriptaculous is to send a sorted aray of IDs which can make for nasty controller actions. Currently I have the sortable objects not only find what was added, but at what position and even what scope (ie… chaning boxes, etc.).

    Comment by Ken Collins, Sunday, July 27, 2008 @ 12:47 pm

  3. I would like to select multiple items (using control-key), and then drag all selected items to a droppable category and update all on server .

    How can we achieve this in Rails !

    Comment by srinath, Friday, August 14, 2009 @ 4:07 am

Leave a comment

Powered by WP Hashcash

Launch: Pathfinder Newsletter

    Get a monthly update on best practices for delivering successful software.

    Subscribe via email


    Subscribe via RSS      RSS icon

Topics

Search

WordPress

Comments about this site: info@pathf.com