ProblemYou want users to be able to rearrange the order of elements in a list by dragging the items into different positions. When an item is dropped into its new position, the application needs to save the new position of each element. SolutionDefine a database schema containing the items that are to be sorted. In this case, the chapters of a book should be in a definite order. Thus, the chapters table contains a position column to store the sort order. The following migration sets up books and chapters tables, and populates them with data from the MySQL Cookbook: db/migrate/001_build_db.rb: class BuildDb < ActiveRecord::Migration def self.up create_table :books do |t| t.column :name, :string end book = Book.create :name => 'MySQL Cookbook' create_table :chapters do |t| t.column :book_id, :integer t.column :name, :string t.column :position, :integer end Chapter.create :book_id => book.id, :name => 'Using the mysql Client Program', :position => 1 Chapter.create :book_id => book.id, :name => 'Writing MySQL-Based Programs', :position => 2 Chapter.create :book_id => book.id, :name => 'Record Selection Techniques', :position => 3 Chapter.create :book_id => book.id, :name => 'Working with Strings', :position => 4 Chapter.create :book_id => book.id, :name => 'Working with Dates and Times', :position => 5 Chapter.create :book_id => book.id, :name => 'Sorting Query Results', :position => 6 Chapter.create :book_id => book.id, :name => 'Generating Summaries', :position => 7 Chapter.create :book_id => book.id, :name => 'Modifying Tables with ALTER TABLE', :position => 8 Chapter.create :book_id => book.id, :name => 'Obtaining and Using Metadata', :position => 9 Chapter.create :book_id => book.id, :name => 'Importing and Exporting Data', :position => 10 end def self.down drop_table :books drop_table :chapters end end Now, set up a one-to-many Active Record association (e.g., one book has many chapters and every chapter belongs to a book): app/models/chapter.rb: class Chapter < ActiveRecord::Base belongs_to :book end app/models/book.rb: class Book < ActiveRecord::Base has_many :chapters, :order => "position" end Your layout includes the default JavaScript libraries and also defines the style of the sortable list elements: app/views/layouts/book.rhtml: <html> <head> <title>Book</title> <%= javascript_include_tag :defaults %> <style type="text/css"> body, p, ol, ul, td { font-family: verdana, arial, helvetica, sans-serif; font-size: 13px; line-height: 18px; } li { position: relative; width: 360px; list-style-type: none; background-color: #eee; border: 1px solid black; margin-top: 2px; padding: 2px; } </style> </head> <body> <%= yield %> </body> </html> The Book controller defines an index method to set up the initial display of a book and its chapters. The order method responds to the XMLHttpRequest calls and updates the sort order in the model. app/controllers/book_controller.rb: class BookController < ApplicationController def index @book = Book.find(:first) end def order order = params[:list] order.each_with_index do |id, position| Chapter.find(id).update_attribute(:position, position + 1) end render :text => "updated chapter order is: #{order.join(', ')}" end end The view iterates over the book object that is passed in and displays its the chapters. A call to the sortable_element helper acts on the DOM element containing the chapter list, making its contents sortable via dragging: app/views/book/index.rhtml: <h1><%= @book.name %></h1> <ul > <% for chapter in @book.chapters -%> <li style="cursor:move;"><%= chapter.name %></li> <% end -%> </ul> <p ></p> <%= sortable_element 'list', :update => 'order', :complete => visual_effect(:highlight, 'list'), :url => { :action => "order" } %> DiscussionThe call to sortable_element takes the id of the list you want sorted. The :update option specifies which element, if any, is to be updated by the action that's called. The :complete option specifies the visual effect that indicates when a sort action is complete. In this case, we highlight the list element with yellow when items are dropped into a new position. The :url option specifies that the order action is called by the XMLHttpRequest object. The script.aculo.us library does the heavy lifting of making the list items draggable. It's also responsible for producing an array of position information based on the final, numeric part of the id of each element. This array is passed to the Book Controller to update the model with the latest element positions. The Book controller's order action saves the updated positions, which are in the params hash, into the order array. each_with_index is called to iterate over the order array, passing the contents and position of each element into the code block. The block uses the contents of each element (id) as an index to find the Chapter object to be updated, and each Chapter object's position attribute is assigned the position of that element in the order array. With all the chapter position information updated, the order action renders a message about the new positions, as text, for display in the view. Figure 8-7 shows the chapter list before and after some reordering. Figure 8-7. A sortable list of chapters that uses drag and dropSee Also
|