Skip to main content

Drag & Drop with Rails

Posted by bleonard on April 18, 2008 at 11:45 AM PDT

The ability to drag and drop has been a staple of desktop applications for years. With the advent of Ajax, the ability to drag and drop has now found its way to web applications. In this entry I spice up the blogging application we've been building with the ability to drag comments to the trash. This is an appropriate feature to add as our own blogs here on java.net have been prone to spam attacks via the comments which require cleanup.

Setting Things Up

I'm going to continue from where the tutorial Using Ajax with Ruby on Rails leaves off. However, I'll be using NetBeans 6.1 and Rails 2.0, so you may want to start with this updated version of the rubyweblog project which has been updated to Rails 2.0 (the entire tutorial series is in the process of being updated).

Test the Existing rubyweblog Project

  1. Open the rubyweblog project.
  2. Note, if you are starting with the provided rubyweblog project, you will also need to create the rubyweblog_development database (Run Rake Task > db > create) and run the migrations.
  3. Run the project and browse to http://localhost:3000/posts to verify that it works. Add at least one blog entry with a couple of comments.

The Plan

Currently, the application does not provide the means to delete a comment. Now, I know this application is severely lacking any sort of user model or administrative interface, but my intent is to show how to do cool things with Rails, not create a replacement for roller, so I squeeze the features in where I can. In this case, I'm going to add a trash can icon to the page and allow users to drag comments to the trash for deletion.



Step 1: Make the Comments Draggable

Here we'll employ the draggable_element helper, a Rails wrapper around the Scriptaculous Draggable object, to make the comments draggable.

  1. Open _comment.html.erb, which is the partial for displaying a single comment, and make it draggable by adding the draggable element helper to the bottom of the file as shown in bold below:



    <% comment_id = "comment_#{comment.id}" %>
    <li id=<%= comment_id %> >
      <%= h comment.comment %><br>
      <div style="color: #999; font-size: 8pt">
          Posted on <%= comment.created_at.strftime("%B %d, %Y at %I:%M %p") %>
      </div>
    </li>
    <%= draggable_element(comment_id, :revert=>true) %>



    Note, since comment_id is used twice, I pulled it out into a local variable to keep the code a bit cleaner. Revert returns the comment to its original location if we drop it anywhere but on the trash can (which we haven't added yet).


  2. Add a visual indicator that the comments are draggable. Open scaffold.css (Public > stylesheets) and add the following style:



    #comments {
        cursor: -moz-grab;
    }



  3. Test your changes. The comments are now draggable, but we haven't specified anywhere to drop them yet.

Step 2: Add the Trash Icon

  1. Save (trashfull.jpg) to the public/images directory of your rubyweblob project directory.


  2. Open show.html.erb and add the following after the :



    <%= text_area 'comment', 'comment' %>
    <%= image_tag "trash.jpg", :id=>'trash'%>


Step 3: Make the Trash Icon a Drop Receiver

Here we'll use the drop_receiving_element helper, a Rails wrapper around the Scriptaculous Droppables.add method to give the comment a drop destination and call the action to delete the comment.

  1. Open show.html.erb and add the drop_receiving_element to the bottom of the file as show below:



    <%= drop_receiving_element('trash',                             # The id of the receiving element
      :accept => "comment",                                         # The CSS class of the dropped element
      :with   => "'comment=' + (element.id.split('_').last())",     # The query string parameters
      :url    => {:action=>:trash_comment}                          # The action to call
    )%>



    Note the :with parameter - this is a piece of JavaScript code that extracts the comment ID form the DOM ID, which if you recall from above is something like "comment_123". We also still have some work to do: first, we don't have anything with a CCS class named "comment" and second we need to code the trash_comment action.


  2. Open _comment.html.erb and add class="comment" to the
  3. tag:


    <li class="comment" id=<%= comment_id %> >

  4. Open posts_controller.rb and add the following trash_comment action:



      def trash_comment
        comment_id = params[:comment]
        Comment.delete(comment_id)
       
        render :update do |page|
          page.replace_html "comment_#{comment_id}", ""
        end         
      end


  5. Test your changes. Dragging a comment to the trash can should delete it and clear it from the list, although the bullet still remains. That's because the replace_html method above is replacing the html inside of the bulleted list item. Wrapping the bulleted list items in a
    solves this problem.


  6. Open _comment.html.erb and wrap the
  7. in a
    , also moving the class and id properties to the
    :


    <% comment_id = "comment_#{comment.id}" %>
    <div class="comment" id=<%= comment_id %> >
      <li>
        <%= h comment.comment %><br>
        <div style="color: #999; font-size: 8pt">
           Posted on <%= comment.created_at.strftime("%B %d, %Y at %I:%M %p") %>
        </div>
    </li>
    </div>
    <%= draggable_element(comment_id, :revert=>true) %>


  8. Now the comments should delete completely.


Step 4: Adding Visual Clues That Something's Happening

  1. Save (trashfull.jpg) to the public/images directory of your rubyweblob project directory.


  2. Add a couple of JavaScript methods to show.html.erb to swap the source of the trash image from empty to full and back:



    <script>   
        function fill_trash() {
            $('trash').src = "/images/trashfull.jpg";
        }      
     
        function empty_trash() {
            $('trash').src = "/images/trash.jpg";
        }    
    </script>

  3. Add a couple of properties to the drop_receiving_element method, :onHover and :complete:



    <%= drop_receiving_element('trash',                             # The id of the receiving element
      :accept => "comment",                                         # The CSS class of the dropped element
      :with   => "'comment=' + (element.id.split('_').last())",     # The query string parameters
      :url    => {:action=>:trash_comment},                         # The action to call
      :onHover => "function() {fill_trash()}",                     
      :complete => "empty_trash()"

    )%>


    Note, I can't tell you why :onHover requires a function definition while :complete takes any arbitrary JavaScript, but that's the way it works.


  4. Test your changes. You should now see a full garbage can when you drag a comment over it and then empty again once the delete completes.


  5. There's still one minor problem - if you drag a comment over the trash but don't actually drop it, your trash can remains full. We can fix this using the onMouseOut event. Add it to the image_tag as follows:


    <%= image_tag "trash.jpg", :id=>'trash', :onMouseOut=>"empty_trash()"%>
    And that should pretty much complete the drag and drop functionality. Remember to refresh the browser to pick up changes to Javascript. Also, Firebug is your friend whenever you're working with Ajax and not seeing what you're expecting.

The Completed Application

RubyWeblogDND.zip

Comments

Genius!

First of all thank you for this extremely useful post! After following the tutorial I notice that with :revert => true, the item still reverts even when you've dropped it successfully. is there a way around that or have I missed something?