15. 自訂列表順序
15-1 需求和 Model 設計
情境:在後台可以自定義活動列表的顯示順序
目前在前後台的 events index action,我們並沒有指定用什麽順序。 如果要指定順序,可以用 order
,例如根據新建的順序遞降 Event.order("id DESC")
或遞增 Event.order("id")
。
接下來我們想做的功能,就是可以用管理員可以自定義順序,例如讓需要曝光的活動放在前面、不重要的活動放在後面。
需要的 Model 設計很簡單,就是增加一個整數欄位來存「位置」。這種列表順序的功能,有兩個 gem 可以用:
這些 gem 提供了方法,幫助我們快速修改這個位置數字。
這裡介紹用 ranked-model,兩個 gem 大同小異。
裝 Gemfile
+ gem 'ranked-model'
編輯 app/models/event.rb
class Event < ApplicationRecord
+ include RankedModel
+ ranks :row_order
執行 bundle
,重啟伺服器
執行 rails g migration add_row_order_to_events
編輯 db/migrate/20170427130106_add_row_order_to_events.rb
,增加一個 row_order
欄位到 events
上。
這是因為 ranked-model 預設的位置欄位命名是
row_order
class AddRowOrderToEvents < ActiveRecord::Migration[5.0]
def change
+ add_column :events, :row_order, :integer
+
+ # 因為要改用這個 row_order 來做排序,而資料庫已經有的 events 預設是 nil
+ # 因此這裡要設定 row_order 的值,其中 `:last` 是 ranked-model 提供的方式,會將資料放到列表最後。
+ Event.find_each do |e|
+ e.update( :row_order => :last )
+ end
+
+ add_index :events, :row_order
end
end
執行 rake db:migrate
修改 app/controllers/events_controller.rb
,改成用 rank(:row_order)
方法來做排序。
class EventsController < ApplicationController
def index
- @events = Event.all
+ @events = Event.rank(:row_order).all
end
修改 app/controllers/admin/events_controller.rb
class Admin::EventsController < AdminController
def index
- @events = Event.all
+ @events = Event.rank(:row_order).all
end
重新瀏覽看看,應該一切正常沒有變化。
我們可以實驗看看這個 gem 的作用,請執行 rails console
e = Event.last
e.row_order_position = :up
e.save!
重新瀏覽看看,本來放最後的活動,應該順序往前一筆。
e.row_order_position = :first
e.save!
重新瀏覽看看,這一筆應該會移動到第一筆。
接著,重頭戲是如何讓管理員可以編輯這個排序,有兩種 UI 解法:
- 傳統 UI:提供往上、往下、移到頂端、移到尾端 等四個按鈕
- Ajax UI:直接用鼠標進行拖拉排序
15-2 傳統 UI
所謂的傳統 UI,就是提供往上、往下、移到頂端、移到尾端 等四個按鈕,讓我們來實作看看。
編輯 config/routes.rb
resources :events do
resources :tickets, :controller => "event_tickets"
+ member do
+ post :reorder
+ end
編輯 app/views/admin/events/index.html.erb
,加上四個按鈕:
<td>
+ <%= link_to "上移", reorder_admin_event_path(event, :position => :up), :method => :post, :class => "btn btn-default" %>
+ <%= link_to "下移", reorder_admin_event_path(event, :position => :down), :method => :post, :class => "btn btn-default" %>
+ <%= link_to "置頂", reorder_admin_event_path(event, :position => :first), :method => :post, :class => "btn btn-default" %>
+ <%= link_to "置底", reorder_admin_event_path(event, :position => :last), :method => :post, :class => "btn btn-default" %>
<%= link_to "Tickets", admin_event_tickets_path(event), :class => "btn btn-default" %>
在路由方法
reorder_admin_event_path
中,我們額外傳了:position
參數,等會在 controller 中就可以接收到params[:position]
代表移動的方式
編輯 app/controllers/admin/events_controller.rb
+ def reorder
+ @event = Event.find_by_friendly_id!(params[:id])
+ @event.row_order_position = params[:position]
+ @event.save!
+
+ redirect_to admin_events_path
+ end
這樣就完成了。
15-3 Ajax UI
所謂的 Ajax 拖拉 UI,就是直接用鼠標進行拖拉排序,這種方式對用戶來說操作速度更快。
拖拉的 UI 需要額外的前端套件,這裡介紹 jQuery UI 的 Sortable Plugin,並直接使用 jquery-ui-rails 這個 gem 來安裝
編輯 Gemfile
+ gem 'jquery-ui-rails'
編輯 app/assets/javascripts/admin.js
//= require bootstrap-datepicker/core
//= require bootstrap-datepicker/locales/bootstrap-datepicker.zh-CN
//= require ckeditor/init
+ //= require jquery-ui
編輯 app/assets/stylesheets/admin.scss
,其中 .sortable_icon
是給畫面中可被拖拉的 ☰ 的樣式
+ @import "jquery-ui";
+
+ .sortable .sortable_icon {
+ border: none;
+ color: #ECECEC;
+ font-size: 20px;
+ cursor: move;
+ padding-right: 10px;
+ }
執行 bundle
,重啟伺服器
編輯 app/views/admin/events/index.html.erb
<table class="table">
+ <thead>
<tr>
<th><%= check_box_tag "全選", "1", false, :id => "toggle_all" %></th>
<th>Event Name</th>
<th>Actions</th>
</tr>
+ </thead>
+ <tbody class="sortable">
<% @events.each do |event| %>
- <tr>
+ <tr data-reorder-url="<%= reorder_admin_event_path(event) %>">
<td>
<%= check_box_tag "ids[]", event.id %>
</td>
- <td><%= link_to event.name, admin_event_path(event) %></td>
+ <td>
+ <span class="sortable_icon">☰</span>
+ <%= link_to event.name, admin_event_path(event) %>
+ </td>
<td>
- <%= link_to "上移", reorder_admin_event_path(event, :position => :up), :method => :post, :class => "btn btn-default" %>
- <%= link_to "下移", reorder_admin_event_path(event, :position => :down), :method => :post, :class => "btn btn-default" %>
<%= link_to "置頂", reorder_admin_event_path(event, :position => :first), :method => :post, :class => "btn btn-default" %>
<%= link_to "置底", reorder_admin_event_path(event, :position => :last), :method => :post, :class => "btn btn-default" %>
<%= link_to "Tickets", admin_event_tickets_path(event), :class => "btn btn-default" %>
<%= link_to "Edit", edit_admin_event_path(event), :class => "btn btn-default" %>
<%= link_to "Delete", admin_event_path(event), :method => "delete", :data => { :confirm => "Are you sure?" }, :class => "btn btn-danger" %>
</tr>
<% end %>
+ </tbody>
</table>
<p>
<%= select_tag :event_status, options_for_select( Event::STATUS.map{ |s| [t(s, :scope => "event.status"), s] }), :class => "form-control" %>
<%= submit_tag t(:bulk_update), :class => "btn btn-primary" %>
<%= submit_tag t(:bulk_delete), :class => "btn btn-danger", :data => { :confirm => "Are you sure?" } %>
</p>
<% end %>
<script>
$("#toggle_all").click(function(){
if ( $(this).prop("checked") ) {
$("input[name='ids[]']").prop("checked", false);
}
})
+ $( ".sortable" ).sortable({
+ axis: 'y', // 限制只能上下拖拉
+ items: 'tr', // 拖拉整個 tr
+ cursor: 'move', // 變更拖拉時的 icon
+ handle: ".sortable_icon", // 限制只有點 ☰ 才能開始拖拉,砍掉這行的話,會是整個 tr 都可以進行拖拉,你可以試試看
+ stop: function(e, ui){ // 當拖拉結束時,會調用這個方法
+ ui.item.children('td').effect('highlight', {}, 1000)
+ },
+ update: function(e, ui) { // 當拖拉結束並且 DOM 上的位置變更時,會調用這個方法
+ reorder_url = ui.item.data('reorder-url')
+ position = ui.item.index() // 取得順序
+ $.ajax({
+ type: 'POST',
+ url: reorder_url,
+ dataType: 'json',
+ data: { position: position }
+ })
+ }
+ });
</script>
設計解說:
- 我們將整個 tr 包在 tbody 之中,好讓
$( ".sortable" ).sortable
將 tbody 標籤內的 tr 都變成可以拖拉,而不會拖拉到標題列 thead 中的 tr。 - 因為每個活動的 reorder 網址都不同,所以我們將網址放在
data-reorder-url="<%= reorder_admin_event_path(event) %>
之中,這樣在 jQuery 裡面透過reorder_url = ui.item.data('reorder-url')
就可以取得 Ajax 要送去的網址。
編輯 app/controllers/admin/events_controller.rb
def reorder
@event = Event.find_by_friendly_id!(params[:id])
@event.row_order_position = params[:position]
@event.save!
- redirect_to admin_events_path
+ respond_to do |format|
+ format.html { redirect_to admin_events_path }
+ format.json { render :json => { :message => "ok" }}
+ end
end
respond_to
可以讓 Rails 根據 request 請求的格式(在 $ajax 中我們有指定了 dataType 是 json),來回傳不同格式。
這樣就完成了。