O'Reilly logo

Ajax on Rails by Scott Raymond

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Drag and Drop

The ability to directly manipulate on-screen objects is often taken for granted in desktop applications, but web interfaces have been slow to follow—largely due to the complex DOM manipulation it requires. script.aculo.us changes that equation, and provides surprisingly easy and powerful support for drag-and-drop interfaces. That means that web developers can decide to use drag and drop based primarily on usability concerns, rather than technical ones. As with visual effects, it’s important to remember that drag and drop is often not the best solution to an interface problem. But when it is, script.aculo.us makes it painless.

Draggables

script.aculo.us provides a Draggable class that’s used to add draggability to DOM elements. To get started, create a new template file, draggables.rhtml. In it, add this:

<div id="dragDIV" class="green box">drag</div>
<%= javascript_tag "new Draggable('dragDIV')" %>

When the page is loaded (http://localhost:3000/chapter4/draggables), the JavaScript statement causes a new instance of the Draggable class to be created, tied to the given element ID. From then on, you can drag the element around the page. Notice how it becomes slightly transparent while it is dragged—it uses the same Opacity effect we explored earlier. The Draggable constructor takes an optional second parameter for options, which will be detailed later.

Rails provides the draggable_element helper to create draggables. Just like Draggable.initialize, the first argument is the ID of an element, and the second is a hash of options. For example:

<div id="helperDIV" class="green box">helper</div>
<%= draggable_element :helperDIV %>

The output of draggable_element is a <script> element with a new Draggable statement. If you just need the JavaScript statement without the <script> tags, use draggable_element_js instead. For example:

<div id="clickDIV" class="green box">
    <%= button_to_function "Make draggable",
         draggable_element_js(:clickDIV) %>
</div>

For usability, it’s often a good idea to change the cursor when it’s over a draggable element. The CSS cursor property makes it easy. For example:

<div class="green box" style="cursor:move">drag</div>

When the user mouses over this element, the cursor will change to a “move” icon (as in Figure 4-1), indicating that the element is draggable. Of course, the CSS doesn’t need to be inline—it could easily be part of the external stylesheet.

Using the CSS cursor property

Figure 4-1. Using the CSS cursor property

Draggable options

As with the Effect.* methods, Draggable.initialize takes a JavaScript hash of options to customize their behavior. The draggable_element helper takes a Ruby hash and converts it to JavaScript.

revert, if set to true, causes the element to return back to its original location after being dragged. The value can also be a function, which will get called when a drag ends, to determine whether the element should be reverted. For example:

<div id="revertDIV" class="green box">revert</div>
<%= draggable_element :revertDIV, :revert => true %>

<div id="functionRevertDIV" class="green box">function revert</div>
<%= draggable_element :functionRevertDIV, 
     :revert => "function(el){
        return Position.page(el)[0] > 100; }" %>

In the second example, :revert is a function that is passed a reference to the element when the dragging stops. In this case, it reverts the drag only if the position of the element is more than 100 pixels from the left edge of the window.

ghosting, if set to true, will clone when a drag starts, leaving the original in place until the drag ends. For example:

<div id="ghostingDIV" class="green box">ghosting</div>
<%= draggable_element :ghostingDIV, :ghosting => true %>

handle allows for a subelement to be used as the handle—the part that can be clicked on to start the drag. The value should be a JavaScript expression that will evaluate to an element ID, or an element reference. For example:

<div id="handleDIV" class="green box">
  <span id="myHandle">handle</span>
</div>
<%= draggable_element :handleDIV, :handle => "'myHandle'" %>

Note that myHandle is in two sets of quotes—that’s because it’s a JavaScript expression that needs to evaluate to a string.

change can be set to a function that will be called every time the draggable is moved while dragging. The callback function gets the draggable as a parameter. For example:

<div id="changeDIV" class="green box">change</div>
<%= draggable_element :changeDIV, :change => "function(draggable) {
  draggable.element.innerHTML=draggable.currentDelta();
}" %>

constraint, if set to horizontal or vertical, will constrain the element to that dimension. It is evaluated as a JavaScript expression, so specifying a DOM element ID requires two sets of quote marks. For example:

<div id="constraintDIV" class="green box">constraint</div>
<%= draggable_element :constraintDIV, :constraint => 'vertical' %>

snap allows you to snap the draggable to a grid. If snap is false (the default), no snapping occurs. If the value is an integer n, the element will jump to the nearest point on a grid of n pixels. The value can also be an array of the form [ x , y ], so that the horizontal and vertical axis can be constrained differently. Finally, the value can be a function that will be passed the current [ x , y ] coordinates of the element (as offsets from its starting position, not absolute coordinates), returns the snapped coordinates. For example:

<div id="snapDIV_50" class="green box">snap to 50</div>
<%= draggable_element :snapDIV_50, :snap => 50 %>

<div id="snapDIV_50_100" class="green box">snap to 50,100</div>
<%= draggable_element :snapDIV_50_100, :snap => '[50,100]' %>

<div id="snapDIV_function" class="green box">snap to function</div>
<%= draggable_element :snapDIV_function, :snap => "function(x, y) {
  new_x = (x > 100) ? 100 : ((x < 0) ? 0 : x);
  new_y = (y > 100) ? 100 : ((y < 0) ? 0 : y);
  return [ new_x, new_y ];
}" %>

The last example demonstrates the power of defining a function for the snap option. For both the x and y dimensions, it limits the value to between 0 and 100. The result is that the draggable is constrained to a small box on the screen.

Droppables

Droppables are DOM elements that can receive dropped draggables and take some action as a result, such as an Ajax call. To create a droppable with JavaScript, use Droppables.add:

<div id="dropDIV" class="pink box">drop</div>
<%= javascript_tag "Droppables.add('dropDIV', {hoverclass:'hover'})" %>

The second argument is a hash of options, which are detailed in the “Droppable options” section. The Rails helpers for creating droppables are drop_receiving_element and drop_receiving_element_js. For example:

<div id="dropHelperDIV" class="pink box">drop here.</div>
<%= drop_receiving_element :dropHelperDIV, :hoverclass => 'hover' %>

The drop_receiving_element_js helper does exactly the same thing, except that it outputs plain JavaScript, instead of JavaScript wrapped in <script> tags.

A droppable doesn’t necessarily accept every draggable; several of the options below can be used to determine which draggables are accepted when.

Droppable options

hoverclass is a class name that will be added to the droppable when an accepted draggable is hovered over it, indicating to the user that the droppable is active. We’ve already seen a couple examples of this in the previous section.

accept can be a string or an array of strings with CSS classes. If provided, the droppable will only accept draggables that have one of these CSS classes. For example:

<div id="dragGreen" class="green box">drag</div>
<%= draggable_element :dragGreen, :revert => true %>

<div id="dragPink" class="pink box">drag</div>
<%= draggable_element :dragPink, :revert => true %>

<div id="dropAccept" class="pink box">drop here (green only).</div>
<%= drop_receiving_element :dropAccept, :hoverclass => "hover",
     :accept => 'green' %>

containment specifies that the droppable will only accept the draggable if it’s contained in the given elements or array of elements. It is evaluated as a JavaScript expression, so specifying a DOM element ID requires two sets of quotation marks. For example:

<div id="one">
    <div id="dragGreen2" class="green box">drag</div>
    <%= draggable_element :dragGreen2, :revert => true %>
</div>

<div id="two">
    <div id="dragPink2" class="pink box">drag</div>
    <%= draggable_element :dragPink2, :revert => true %>
</div>

<div id="dropContainment" class="pink box">drop here.</div>
<%= drop_receiving_element :dropContainment, :hoverclass => "hover",
    :containment => "'one'" %>

onHover is a callback function that fires whenever a draggable is moved over the droppable, and the droppable accepts it. The callback gets three parameters: the draggable, the droppable, and the percentage of overlapping as defined by the overlap option. A simple example, without any parameters:

<div id="dropOnHover" class="pink box">drop</div>
<%= drop_receiving_element :dropOnHover, :hoverclass => "hover",
      :onHover => "function(){ $('dropOnHover').update('hover!'); }" %>

And here is an example using all three possible callback parameters:

<div id="dropOnHover" class="pink box">drop</div>
<%= drop_receiving_element :dropOnHover, :hoverclass => "hover", 
      :onHover => "function(draggable, droppable, overlap){ 
         $('dropOnHover').update('you dragged ' + draggable.id + 
              ' over ' + droppable.id + ' by ' + overlap + 
              ' percent'); }" %>

onDrop is called whenever a draggable is released over the droppable and it’s accepted. The callback gets two parameters: the draggable element and the droppable element. For example:

<div id="dropOnDrop" class="pink box">drop</div>
<%= drop_receiving_element :dropOnDrop, :hoverclass => "hover",
    :onDrop => "function(drag, drop){ 
       alert('you dropped ' + drag.id + ' on ' + drop.id) }" %>

Droppables with Ajax

All the options specified in the previous section are available whether you create your droppable with JavaScript (Droppables.add) or the Rails helpers (drop_receiving_element and drop_receiving_element_js). However, when created with the helpers, some additional options are available. Namely, all the link_to_remote options, such as update and url (described in Chapter 3), are also available, and will be used to create an onDrop callback function for doing Ajax calls with droppables. For example:

<div id="drag" class="green box">drag</div>
<%= draggable_element :drag, :revert => true %>

<div id="drop" class="pink box">drop</div>
<%= drop_receiving_element :drop, :hoverclass => "hover",
     :update => "status", :url => { :action => "receive_drop" } %>

<div id="status"></div>

Notice that the :url option points to a receive_drop action, so we’ll need to define that in chapter4_controller.rb:

def receive_drop
  render :text => "you dropped element id #{params[:id]}"
end

Unless overridden by the :with option, the drop_receiving_element Ajax call will automatically include the ID of the draggable as the id parameter of the request.

Sortables

Sortables are built on top of draggables and droppables so that with one fell swoop, you can give a group of elements advanced drag-and-drop behavior so that they can be reordered graphically.

Use Sortable.create to create a sortable from JavaScript. For example:

<ul id="list">
  <li>Buy milk</li>
  <li>Take out trash</li>
  <li>Make first million</li>
</ul>

<%= javascript_tag "Sortable.create('list')" %>

Of course, Rails provides helpers for this task as well: sortable_element and sortable_element_js. Just like the other drag-and-drop related helpers, the first argument is the target DOM element and the second is a hash of options used to affect the behavior. The other available options are:

hoverclass

Passed on to the droppables, so that the specified CSS class is added to the droppable whenever an acceptable draggable is hovered over it.

handle

Passed on to the draggable. This is especially useful when the sortable elements are interactive, such as links or form elements. For example:

<ul id="listHandle">
  <li><span class="handle">x</span> Buy milk</li>
  <li><span class="handle">x</span> Take out trash</li>
  <li><span class="handle">x</span> Make first million</li>
</ul>

<%= sortable_element :listHandle, :handle => 'handle' %>
ghosting

Passed on to the draggables as well. For example:

<ul id="listGhosting">
  <li>Buy milk</li>
  <li>Take out trash</li>
  <li>Make first million</li>
</ul>

<%= sortable_element :listGhosting, :ghosting => true %>
constraint and overlap

Work together to determine which direction the Sortable will operate in: either vertical (the default) or horizontal. constraint is passed on to the draggables—it restricts which direction the elements can be dragged. overlap is passed to the droppable, making it only accept the draggable element if it is more than 50 percent overlapped in the given dimension. For example:

<ul id="listHorizontal">
  <li style="display: inline; margin-right: 10px;">Buy milk</li>
  <li style="display: inline; margin-right: 10px;">Take out trash</li>
  <li style="display: inline; margin-right: 10px;">Make first million</li>
</ul>

<%= sortable_element :listHorizontal,
     :constraint => 'horizontal',
     :overlap    => 'horizontal' %>
tag

Sets the kind of tag that is used for the sortable elements. By default, this is LI, which is appropriate for UL and OL list containers. If the sortable elements are something else (such as paragraphs or DIVs), you can specify that here. For example:

<div id="listTag">
  <div>Buy milk</div>
  <div>Take out trash</div>
  <div>Make first million</div>
</div>

<%= sortable_element :listTag, :tag => 'div' %>
only

Restricts the selection of child elements to elements with the given CSS class or an array of classes. For example:

<ul id="listOnly">
  <li class="sortable">Buy milk</li>
  <li class="sortable">Take out trash</li>
  <li>Make first million</li>
</ul>

<%= sortable_element :listOnly, :only => 'sortable' %>
containment

Used to enable drag-and-drop between multiple containers. A container will only accept draggables whose parent element is in containment, which can be either an ID or an array of IDs. For example:

<ul id="list1">
  <li>Buy milk</li>
  <li>Take out trash</li>
</ul>

<ul id="list2">
  <li>Make first million</li>
</ul>

<%= sortable_element :list1, :containment => ['list1', 'list2'] %>
<%= sortable_element :list2, :containment => ['list1', 'list2'] %>
dropOnEmpty

Useful when you have two sortable containers, and you want elements to be able to be dragged between them. By default, an empty container can’t have new draggables dropped onto it. By setting dropOnEmpty to true, that’s reversed. For example:

<ul id="listFull">
    <li id="thing_1">Buy milk</li>
    <li id="thing_2">Take out trash</li>
    <li id="thing_3">Make first million</li>
</ul>

<ul id="listEmpty">
</ul>

<%= sortable_element :listFull,
     :containment => ['listFull', 'listEmpty'],
     :dropOnEmpty => true %>
<%= sortable_element :listEmpty,
     :containment => ['listFull', 'listEmpty'],
     :dropOnEmpty => true %>
scroll

Allows for sortables to be contained in scrollable areas, and dragged elements will automatically adjust the scroll. To accomplish this, the scrollable container must be wrapped in an element with the style overflow:scroll, and the scroll option should be set to that element’s ID. The value is evaluated as a JavaScript expression, so it’s necessary to put it in two sets of quotes. Scrolling in script.aculo.us must be explicitly enabled, by setting Position.includeScrollOffsets to true. For example:

<div id="container" style="overflow: scroll; height: 200px;">
  <ul id="listScroll">
    <% 20.times do |i| %>
      <li>Buy milk</li>
      <li>Take out trash</li>
      <li>Make first million</li>
    <% end %>
  </ul>
</div>

<%= javascript_tag "Position.includeScrollOffsets = true" %>
<%= sortable_element :listScroll, :scroll => "'container'" %>
onChange

Called whenever the sort order changes while dragging. When dragging from one sortable to another, the callback is called once on each sortable. The callback gets the affected element as its parameter. For example:

<ul id="listChange">
  <li>Buy milk</li>
  <li>Take out trash</li>
  <li>Make first million</li>
</ul>

<%= sortable_element :listChange,
     :onChange => "function(el) { alert(el.innerHTML); }" %>
onUpdate

Called when the drag ends and the sortable’s order has changed. When dragging from one sortable to another, onUpdate is called once for each sortable. The callback gets the container as its parameter. For example:

<ul id="listUpdate">
  <li>Buy milk</li>
  <li>Take out trash</li>
  <li>Make first million</li>
</ul>

<%= sortable_element :listUpdate,
     :onUpdate => "function(el) { alert(el.innerHTML); }" %>

Ajax-enabled sortables

As with droppables, the sortable_element helper also can take all the familiar Ajax options that link_to_remote provides. By default, when an Ajax call is created, the action called gets the serialized sortable elements as parameters. To work, the IDs of the sortable elements should follow the naming convention used by Sortable.serialize: the unique part of the ID should be at the end, preceded by an underscore. So item_1, person_2, and _3 would make good IDs, but item1, 2_person and 3 would not. For example:

<ul id="listAjax">
  <li id="item_1">Buy milk</li>
  <li id="item_2">Take out trash</li>
  <li id="item_3">Make first million</li>
</ul>

<%= sortable_element :listAjax,
     :url      => { :action => 'repeat' },
     :complete => "alert(request.responseText);" %>

In the example, reordering the list triggers an Ajax call to the repeat action, which gets a listAjax array parameter containing the IDs of the sortable elements, in the current order. To see this in action, define a repeat action to echo back the parameters it receives, like this:

def repeat
  render :text => params.inspect
end

For a real-world example of creating sortables and handling reordering on the server side, see the Review Quiz example application in Example A.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required