ProblemThe application has a model with a list that you want to sort. It is advisable to control the sorting order of the list in the database, and provide users with an attractive, modern drag-and-drop interface that allows you to set the order of the list items.
DecisionSuppose you create an application to manage the list of purchased products. Given the size of modern grocery supermarkets, before you get to the malls, it is important to develop a procurement strategy. Otherwise, you can waste precious hours of life, following non-optimal purchasing routes. The Active Record migration file for the procurement optimization application is as follows:
class CreateAddPersonAndGroceryListsAndFoodItemsTables <ActiveRecord :: Migration
def self.up
create_table: people do | t |
t.column: name,: string
end
')
create_table: grocery_lists do | t |
t.column: name,: string
t.column: person_id,: integer
end
create_table: food_items do | t |
t.column: grocery_list_id,: integer
t.column: position,: integer
t.column: name,: string
t.column: quantity, integer
end
end
def self.down
drop_table: people
drop_table: grocery_lists
drop_table: food_items
end
end this source code was highlighted with Source Code Highlighter .
From the code it is clear that we have tables with a list of people, lists of purchases and records of products that fall into these lists (along with the right amount of each product). All of them are linked by the standard has_many () relation in Active Record, with the exception of the position column in the food_items table. In a couple of minutes we will understand that this column plays a special role.
All the model files associated with the tables are equally short and unpretentious. Person models own many GroceryList objects:
class Person <ActiveRecord :: Base
has_many: grocery_lists
end this source code was highlighted with Source Code Highlighter .
And each GroceryList model has a list of Foodltem objects, which will be retrieved according to the value of the position column of the food_items table:
class GroceryList <ActiveRecord :: Base
has_many: food_items, order =>: position
belongs_to: person
end this source code was highlighted with Source Code Highlighter .
And finally, we have reached the most delicious. The Foodltem class contains an Active Record declaration of acts_as_list (), which allows the object contained in it (Grocery -List) to "automatically" control the order of its elements:
class FoodItem <ActiveRecord :: Base
belongs_to: grocery_list
acts_as_list: scope =>: grocery_list
end
* This source code was highlighted with Source Code Highlighter .
The: scope parameter tells acts_as_list () that the sort order applies to the contents of a single list, whose items have the same grocery_list_id. Thus, sorting one purchase list will not affect the order established in other lists.
The column name position plays a special role for acts_as_list (). By convention, when the model has a declaration of acts_as_list (), Rails will automatically use that column name to control the sort order. If you need to use a non-standard column name here, you can pass the parameter: column to the declaration, but the position name makes sense for our modest list of products, so we leave it alone.
After starting the migration and creating the model files, let's turn on the Rails console and test this new structure:
chad> ruby script / console
>> kelly = Person.create (: name => "Kelly")
=> # <Person: 0x26ec854 ... >>
>> list = kelly.grocery_lists.create (: name => "Dinner for Tibetan New Year Party")
=> # <GroceryList: 0x26b9788 ... >>
>> list.food_items.create (: name => "Bag of flour",: quantity => 1)
=> # <FoodItem: 0x26a8898 ... >>
>> list.food_items.create (: name => "Pound of Ground Beef",: quantity => 2)
=> # <FoodItem: 0x269b60c ... >>
>> list.food_items.create (: name => "Clove of Garlic",: quantity => 5)
=> # <FoodItem: 0x26937e0 ... >>
So, now in our database there is a man named Kelly, who seems to be planning a party to celebrate the New Year on the Tibetan calendar. So far, there are only three names on her list. Of course, under the list has not yet been summarized, but you can cook a Tibetan dish from these three ingredients. Let's take a look at what happened to the position column when creating these objects:
>> list.food_items.find_by_name ("Pound of Ground Beef"). position
=> 2
>> list.food_items.find_by_name ("Bag of flour"). position
=> 1
Wow! Active Record updated position column for us! In addition, the acts_as_list () declaration has resulted in the installation of a whole package of excellent and convenient methods for performing such tasks as selecting the next (in order) record in the list or moving the record position up or down. But let's still not be right now to understand everything that is in the model. We are already ready to get to the thing that interests us - dragging.
As always, when you are going to apply some fashionable thing related to Ajax, you need to include the necessary JavaScript libraries somewhere in the HTML. I usually create a standard layout in the app / views / layouts / standard.rhtml file, and then fill it with the following code:
<! HTML DOCTYPE PUBLIC "- // W3C // DTD HTML 4.01 // EN" "http://www.w3.org/TR/html4/strict.dtd" >
< html >
< head >
<% = javascript_include_tag: defaults %>
</ head >
< body >
<% = yield %>
</ body >
</ html >
* This source code was highlighted with Source Code Highlighter .
Further, having imagined that we already have a certain interface for creating a list and associating it with a specific person, let's create a controller and an action from which the order of the elements of the list will be changed. We will create an app / views / controllers / grocery_list_controller.rb controller containing an action called show ().
The beginning of the controller code should look like this:
class GroceryListController <ApplicationController
layout "standard"
def show
@grocery_list = GroceryList.find ( params [: id])
end
# ... * This source code was highlighted with Source Code Highlighter .
Notice that we have included the standard.rhtml layout and defined the main action, which will simply search for a list of products based on the parameters provided.
Then in the file app / views / grocery_list / show.rhtml we will create the corresponding view:
So far, nothing unusual is visible. This is a standard Action View, read-only material. Although it is worth noting that for tags <li> unique identifiers of elements are automatically generated. This is needed to go to the sorting code, so at this stage you need not to miss this circumstance. We can look at what this page looks like by running the application development server and pointing the browser (assuming the default port is used) to
localhost : 3000 / grocery_list / show / listid, where listid is the id of the Grocerytist model object created when working in console mode .
Now let's make the list available for sorting. To do this, at the end of the contents of the show.rhtml file, add the following code:
< h2 > <% = @ grocery_list.person.name %> 's Grocery List </ h2 >
< h3 > <% = @ grocery_list.name %> </ h3 >
< ul id = " grocery-list " >
<% @ grocery_list.food_items.each do | food_item | %>
< li id = " item_ & # 60 ;% = food_item . id % & # 62 ; >
<% = food_item.quantity %> units of <% = food_item.name %>
</ li >
<% end %>
</ ul >
<% = sortable_element 'grocery-list' ,
: url => {: action => "sort",: id => @grocery_list},
: complete => visual_effect (: highlight, 'grocery-list' )
%>
* This source code was highlighted with Source Code Highlighter .
This helper will generate the JavaScript needed to turn our unordered list into a dynamic, sorted by dragging and dropping form. The first parameter, the grocery-list, refers to the item identifier on the current HTML page, which must be converted to a sortable list. The: url parameter defines elements such as the controller and the action that will be used to compose a URL that is invoked after making changes to the sort. In it, we have defined the sort () action of the current controller, to which the identifier of the current product list has been added. Finally, the xomplete parameter sets the visual effect to be applied as soon as the sort () action is completed.
Let's turn the sort () action code into reality and see how it all works. Add a sort () action to the grocery_list_controller.rb file, which looks like this:
def sort
@grocery_list = GroceryList.find ( params [: id])
@ grocery_list.food_items.each do | food_item |
food_item.position = params [ 'grocery-list' ] .index (food_item.id.to_s) + 1
food_item.save
end
render: nothing => true
end
* This source code was highlighted with Source Code Highlighter .
First, a list of products is selected by the provided identifier. Then, the list entries are sequentially searched, and the position of each entry changes according to its index in the grocery-list parameter. This parameter is automatically generated by the sortable_element () helper, which creates an ordered array of list entry identifiers. Since the value of the position columns starts from one, and the array indexing starts from zero, before we save the position, we increase the index value by one.
Finally, we absolutely clearly indicate to Rails that the action should not send anything. Since the visual display of the list being sorted is the list itself (which is already displayed), we will allow the action to complete its work without external manifestations.
If we wanted to update the HTML page with the results of the action, we would need to add the update parameter to the call to sortable_element (), passing it the identifier of the HTML element filled in by these results.
If, after adding the sortable_element () helper, we update the list of products on the show () page, we will be able to drag the records up and down the list, changing the order of their placement both on the page and in the database.
Crosspost from
my blog