Link Search Menu Expand Document

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

image

這樣就完成了。

15-3 Ajax UI

所謂的 Ajax 拖拉 UI,就是直接用鼠標進行拖拉排序,這種方式對用戶來說操作速度更快。

image

拖拉的 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>

設計解說:

  1. 我們將整個 tr 包在 tbody 之中,好讓 $( ".sortable" ).sortable 將 tbody 標籤內的 tr 都變成可以拖拉,而不會拖拉到標題列 thead 中的 tr。
  2. 因為每個活動的 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),來回傳不同格式。

這樣就完成了。


Copyright © 2010-2021 Wen-Tien Chang All Rights Reserved.