The Source for Java Technology Collaboration
User: Password:



Brian Leonard's Blog

April 2008 Archives


Drag & Drop with Rails

Posted by bleonard on April 18, 2008 at 11:45 AM | Permalink | Comments (0)

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 <image_tag> after the <text_area>:

    <%= 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 <li> tag:

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

  3. 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

  4. 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 <div> solves this problem.

  5. Open _comment.html.erb and wrap the <li> in a <div>, also moving the class and id properties to the <div>:

    <% 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) %>

  6. 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


Autovalidation with Rails

Posted by bleonard on April 16, 2008 at 12:31 PM | Permalink | Comments (0)

Actually, I don't know what this feature is called, but I love it when a web application gives me instant feedback on the validity of my entry. For example:



This is especially true when I'm registering a new user account - submitting the page over and over again because the username I've selected is already taken or the password doesn't meet the requirements is always a drag. So I started looked for examples on how to do this in Rails and couldn't really find anything (possibly because I don't actually know what to call it) - I guess it's most similar to autocompletion, so I'm going with autovalidation. And here are the steps I've put together to get it to work...

Setting Things Up

  1. Download and install MySQL.
  2. Download and install NetBeans 6.1 RC1 or later.

Creating the Rails Project

  1. Create a new Ruby on Rails Application named autovalidation. You can select Finish after the first page of the New Ruby on Rails Application dialog:
  2. Right-click the project and select Run Rake Task > db > create:



  3. Right-click the project and select Generate. Generate a scaffold with the Model Name User and the Attribute Pairs username:string:
  4. Right-click the project and select Migrate Database > To Current Version.

  5. Open user.rb and type the following:

    vu[tab]
    - this will expand to:



    type username over the attribute and press enter.

  6. On the next line in user.rb type the following:

    vl[tab]
    this will expand to:



    type username over the attribute and press enter. Type 5..10 over the 3..20 and press enter. The completed code should look as follows:

    class User < ActiveRecord::Base
      validates_uniqueness_of :username
      validates_length_of :username, :within => 5..10
    end

Traditional Server Side Validation

  1. Run the project and browse to http://localhost:3000/users. Try to create a new user such as bill:



  2. Okay, create william.

  3. Try to create william again:



    Let's enhance this application with more user friendly "autovalidation"...

Lets Autovalidate

In addition to the server side validation that's being performed, we're going to add some client side validation using Ajax. We'll do this by displaying a message to the right of the field on the validity of what's been entered. Since we're validating against the database, we will need to make an XMLHTTPRequest back to the server. We can use Rails' observe_field helper method for this task.

  1. First we need to create a named placeholder for the message. Open new.html.erb and add a <span> tag after the text field as follows:

    <%= f.text_field :username %><span id="message"></span>
  2. Now we'll use Rails' observe_field method to watch changes to that field. Add the following before the closing <% end %> tag:

        <%= observe_field   :user_username,                         # The field to observe 
    :with => "username", # The input to validate
    :frequency => 0.25, # The frequency in seconds to watch for changes
    :url => {:action => :validate }, # The action to call when changes occur
    :update => :message # The DOM ID to update
    %>

  3. Create the validate action. Open users_controller.rb and add the following:

      def validate
    color = 'red'
    username = params[:username]
    if username.length < 5
    message = 'Too short'
    elsif username.length > 10
    message = 'Too long'
    else
    user = User.find_all_by_username(username)
    if user.size > 0
    message = 'Taken'
    else
    message = 'Available'
    color = 'green'
    end
    end
    @message = "<b style='color:#{color}'>#{message}</b>"
    render :partial=>'message'
    end

    You'll notice in the last line of the validate action above we're rendering a partial. This partial, which we'll create next, will be dynamically rendered inside the span tag we defined in step 1 above.

  4. Create a new ERB file named _message in Views > users. Replace its contents with the following:

    <%= @message %>

  5. Open users.html.erb and add the following before the closing </head> tag:

    <%= javascript_include_tag 'prototype'  %>
    All Rails applications come with the prototype and scriptaculous libraries ready to use, but you do need to tell the application that you want to use them.

Run with Autovalidate

  1. Switch to the browser and start creating a new user (you'll need to refresh the browser once to load the prototype JavaScript libraries):








Troubleshooting

Firebug is your friend for troubleshooting Rails applications. With Firebug enabled, you can see the requests as you type:


A couple of things that tripped me up

Forgetting to include the Prototype libraries:


Misspelling the field to observe:

 

The Completed Application

autovalidation.zip




Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds