ドラッグ&ドロップで自動連番の処理を実装する
イメージ
実装経緯
- 並び替え機能はメニュー表など管理画面で実装頻度が高かった。
- 管理画面もスマホやタブレットに対応してほしいという声も多かった。
実装以前は、編集フォームに遷移して順番(position
)カラムの整数値をセレクトフィールドで選択して更新していました。
また、自動連番処理はモデル側でしてました。
知識を整理するため簡単なアプリ上に作ってみようと思います。
使用するライブラリ
-
acts_as_list
順番の並び替えの制御を担当する。
同ポジションにパフォーマンスが上のranked-modelがある。
今回acts_as_list
を使用した経緯- レコード数が少ない
- 今後もレコード数は多くはならない
- 少ないレコード数なので順番の値を把握しておきたい。
※レコード数が多く順番の値を気にしない場合はranked-modelを選択した方がよさげ
-
SortableJS
ドラッグ&ドロップで要素の並び替えを担当する
今回SortableJS
を使用した経緯- jQueryUIを使わずに実装したかった。(jQuery UIのドラッグ操作がスマホ対応していないのが難点)
- スマホ特有のドラッグ操作を
handle
で制御出来る
実装
1. 順番(position
)カラムの追加
今回はMenu
モデルにposition
カラムを追加する形で作っていきます
class AddPositionToMenu < ActiveRecord::Migration[6.1]
def change
add_column :menus, :position, :integer, null: false, default: 0
add_index :menus, :position # ここちょっと怖い
end
end
※ ここでindexにunique
を設定するとacts_as_list
の仕様で順番を更新できなくなります。
2. acts_as_listを追加
gem "acts_as_list"
bundle install
する
- 対象のモデルに
acts_as_list
を記述する
class Menu < ApplicationRecord
acts_as_list
validates :title, presence: true, length: { maximum: 50 } validates :title, presence: true, length: { maximum: 50 }
3. jqueryを追加
-
ajax処理が書きやすい(慣れている)ため導入します
yarn add jquery
-
jqueryの初期化
config/webpack/environment.js
const { environment } = require('@rails/webpacker')
// jqueryの設定
const webpack = require('webpack')
environment.plugins.prepend('Provide',
new webpack.ProvidePlugin({
$: 'jquery/src/jquery',
jQuery: 'jquery/src/jquery'
})
)
module.exports = environment
4. SortableJsの追加
-
導入
yarn add sortablejs
-
view設定
id:js-sortable-menus
内をソートの範囲
class:i.handle
をドラッグ&ドロップのトリガーにする
app/views/menus/index.html.haml
%tbody#js-sortable-menus
- @menus.each do |menu|
%tr
%td
%i.handle
.fa.fa-list-ul
%td= menu.title
- sortablejsの設定
app/javascript/packs/views/menus/index.js
import Sortable from 'sortablejs';
$(function() {
const el = document.getElementById('js-sortable-menus');
new Sortable(el, {
handle: "i.handle",
axis: 'y',
animation: 300,
onUpdate: function (evt) {
return $.ajax({
url: `/api/menu/positions/${evt.oldIndex}`,
type: 'patch',
data: {
from: evt.oldIndex,
to: evt.newIndex
}
});
}
});
});
handle:
でトリガーになる要素を設定
oldIndex
は移動前の画面上の順番(0から始まる)
newIndex
は移動後の画面上の順番(0から始まる)
- api作成
まずnamespaceを使ってapiのroutesを記述する
config/routes.rb
Rails.application.routes.draw do
root to: "static_pages#index"
resources :menus
namespace :api do
namespace :menu do
resources :positions, only: [:update]
end
end
end
- apiコントローラーの作成
app/controllers/api/menu/positions_controller.rb
class Api::Menu::PositionsController < ApplicationController
skip_before_action :verify_authenticity_token
def update
menu = Menu.find_by!(position: params[:from].to_i + 1) # 0から始まるので+1する
menu.update!(position: params[:to].to_i + 1) # 0から始まるので+1する
head :ok
end
end
skip_before_action :verify_authenticity_token
でCSRF トークン
の検証をスキップする
5. 順番(position)カラム順に出力する
app/models/menu.rb
scope :sorted, -> { order(position: :asc) }
app/controllers/menus_controller.rb
# GET /menus or /menus.json
def index
@menus = Menu.sorted
end
app/views/menus/index.html.haml
順番を並び替えるviewにjs
ファイルを読み込ませる
= javascript_pack_tag "views/menus/index", "data-turbolinks-track": "reload"
%h1 Listing menus
%table
%thead
%tr
%th
%th Title
%th Price
%th
%th
%th
%tbody#js-sortable-menus
- @menus.each do |menu|
%tr
%td
%i.handle
.fa.fa-list-ul
%td= menu.title
%td= menu.price
%td= link_to "Show", menu
%td= link_to "Edit", edit_menu_path(menu)
%td= link_to "Destroy", menu, method: :delete, data: { confirm: "Are you sure?" }
%br
= link_to "New Menu", new_menu_path
成果
Demo動作確認
※Herokuの無料プランなので立ち上がりに時間がかかる時があります。