Pull to refresh

Создание сортируемого списка с перетаскиваемыми элементами

Reading time7 min
Views2.1K
Проблема
В приложении есть модель со списком, который нужно отсортировать. Жела­тельно управлять порядком сортировки списка, находящегося в базе данных, и предоставить пользователям привлекательный, современный интерфейс с пе­ретаскиванием, позволяющий установить порядок следования элементов списка.


Решение
Предположим, что создается приложение для управления списком закупаемых продуктов. Учитывая размеры современных продуктовых супер­маркетов, прежде чем добраться до торговых рядов, важно выработать стратегию закупок. В противном случае можно потратить впустую драгоценные часы жиз­ни, следуя по неоптимальным закупочным маршрутам. Файл миграции Active Record для приложения по оптимизации закупок имеет следующий вид:

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.


Из кода видно, что у нас есть таблицы со списком людей, списками закупок и записями продуктов, попадающих в эти списки (наряду с нужным количеством каждого продукта). Все они связаны стандартным в Active Record отношением has_many( ), за исключением столбца position в таблице food_items. Через пару минут мы поймем, что этот столбец играет особую роль.
Все связанные с таблицами файлы моделей одинаково коротки и незатейли­вы. Модели Person принадлежит множество объектов GroceryList:

class Person < ActiveRecord:: Base
has_many: grocery_lists
end
* This source code was highlighted with Source Code Highlighter.


А у каждой модели GroceryList имеется список объектов Foodltem, который будет извлекаться в соответствии со значением столбца position таблицы food_items:

class GroceryList < ActiveRecord:: Base
has_many: food_items,: order =>: position
belongs_to: person
end
* This source code was highlighted with Source Code Highlighter.


И наконец, мы дошли до самого вкусного. Класс Foodltem содержит объявление Active Record acts_as_list( ), позволяющее содержащемуся в нем объекту (Grocery -List) «автоматически» управлять порядком следования его элементов:

class FoodItem < ActiveRecord:: Base
belongs_to: grocery_list
acts_as_list: scope =>: grocery_list
end
* This source code was highlighted with Source Code Highlighter.


Параметр :scope сообщает acts_as_list(), что порядок сортировки распространя­ется на содержимое одного списка, элементы которого имеют одинаковый grocery_ list_id. Таким образом, сортировка одного закупочного списка не повлияет на по­рядок, установленный в других списках.
Имя столбца position играет для acts_as_list( ) особую роль. По соглашению, когда в модели присутствует объявление acts_as_list( ), Rails будет автоматически ис­пользовать это имя столбца для управления порядком сортировки. Если здесь нужно будет использовать нестандартное имя столбца, то можно передать объяв­лению параметр :column, но для нашего скромного списка продуктов имеет смысл имя position, поэтому мы его оставим в покое.
После запуска миграции и создания файлов моделей, давайте включим кон­соль Rails и испытаем эту новую структуру:

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 ...>>
Итак, теперь в нашей базе данных есть человек по имени Kelly, который, по­хоже, планирует вечеринку по случаю празднования Нового года по тибетскому календарю. Пока в ее списке лишь три наименования. Разумеется, под списком еще не подведена черта, но вы сможете приготовить тибетское блюдо и из этих трех ингредиентов. Давайте взглянем на то, что произошло со столбцом position при создании этих объектов:

>> list.food_items.find_by_name(«Pound of Ground Beef»).position
=> 2
>> list.food_items.find_by_name(«Bag of flour»).position
=> 1

Вот это да! Active Record обновил для нас столбец position! К тому же объявле­ние acts_as_list( ) привело к установке целого пакета превосходных и удобных ме­тодов для выполнения таких задач, как выбор следующей (по порядку) записи в списке или перемещение позиции записи вверх или вниз. Но давайте все же не будем именно сейчас разбираться во всем, что есть в модели. У нас уже все готово для того, чтобы добраться до интересующей нас вещи — перетаскивания.
Как и всегда, собираясь применить какую-нибудь модную вещь, связанную с Ajax, нужно где-нибудь в HTML включить необходимые JavaScript-библиоте­ки. Обычно я создаю в файле app/views/layouts/standard.rhtml стандартный макет, а за­тем наполняю его следующим кодом:

<! DOCTYPE HTML 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.


Далее, вообразив, что у нас уже есть некий интерфейс для создания списка и связывания его с конкретной персоной, давайте создадим контроллер и дейст­вие, откуда будет проводиться изменение порядка следования элементов списка. Мы создадим контроллер app/views/controllers/grocery_list_controller.rb, содержащий дей­ствие под названием show().
Начало кода контроллера должно выглядеть следующим образом:

class GroceryListController < ApplicationController
layout «standard»

def show
@grocery_list = GroceryList.find(params[: id])
end
#…
* This source code was highlighted with Source Code Highlighter.


Заметьте, что мы включили макет standard.rhtml и определили главное действие, которое будет просто искать список продуктов на основе предоставленных пара­метров.
Затем в файле app/views/grocery_list/show.rhtml мы создадим соответствующее пред­ставление:
Пока что не видно ничего необычного. Это стандартный материал Action View, предназначенный только для чтения. Хотя стоит отметить, что для тегов <li> автоматически сгенерированы уникальные идентификаторы элементов. Это по­надобится для перехода к коду сортировки, поэтому на данном этапе нужно не упустить это обстоятельство. Мы можем взглянуть, на что похожа эта страница, запустив сервер разработки приложения и указав браузеру (предположив ис­пользование порта по умолчанию) на localhost:3000/grocery_list/show/listid, где listid — это id объекта модели Grocerytist, созданного при работе в режиме консоли.
Теперь давайте сделаем список доступным для сортировки. Для этого в конце содержимого файла show.rhtml добавим следующий код:

<h2><%= @grocery_list.person.name %>'s Grocery List</h2>
<h3><%= @grocery_list.name %></h3>
<ul idgrocery-list»>
<% @grocery_list.food_items.each do |food_item| %>
<li iditem_&#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.


Этот помощник сгенерирует JavaScript, необходимый для превращения наше­го неупорядоченного списка в динамическую, сортируемую путем перетаскива­ния форму. Первый параметр, grocery-list, ссылается на идентификатор элемента на текущей HTML-странице, который должен быть преобразован в сортируемый список. Параметр :url определяет такие элементы, как контроллер и действие, из которых будет составлен URL, вызываемый после внесения изменений в сор­тировку. В нем мы определили действие sort( ) текущего контроллера, к которо­му добавлен идентификатор текущего списка продуктов. И наконец, параметр xomplete устанавливает визуальный эффект, который будет применен, как только действие sort( ) будет завершено.
Давайте воплотим код действия sort( ) в реальность и посмотрим, как все это будет работать. Добавим в файл grocery_list_controller.rb действие sort( ), которое вы­глядит следующим образом:

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.


Сначала по предоставленному идентификатору выбирается список продук­тов. Затем осуществляется последовательный перебор записей списка, и позиция каждой записи изменяется в соответствии с ее индексом в параметре grocery-list. Этот параметр автоматически генерируется помощником sortable_element( ), кото­рый создает упорядоченный массив идентификаторов записей списка. Посколь­ку значение столбцов position начинается с единицы, а индексация массива начина­ется с нуля, перед тем, как сохранить позицию, мы увеличиваем значение индекса на единицу.
В завершение мы абсолютно ясно указываем Rails, что действие не должно ни­чего отправлять. Поскольку визуальное отображение сортируемого списка и есть сам этот список (который уже отображается), мы позволим действию завершить его работу без внешних проявлений.
Если бы мы хотели обновить HTML-страницу результатами выполнения дей­ствия, нам нужно было бы добавить к вызову sortable_element( ) параметр update, пе­редав ему идентификатор HTML-элемента, заполняемого этими результатами.
Если мы после добавления помощника sortable_element( ) обновим список про­дуктов на странице show( ), то получим возможность перетаскивать записи вверх и вниз по списку, изменяя порядок их размещения как на странице, так и в базе данных.

Кросспост с моего блога
Tags:
Hubs:
Total votes 6: ↑4 and ↓2+2
Comments3

Articles