はじめに
久々にrailsの並び替え機能を実装したので、その際の作業録です。
実装
昔は、以下のgemを利用した時期もありましたが、jqueryが利用されていることだったり、メンテがされてないとかがあったので、今回は利用しないことにしました
https://github.com/itmammoth/rails_sortable
今回は、stimuls componetsにあるこちらを利用しました。
https://www.stimulus-components.com/docs/stimulus-sortable
javascript関係
yarn add @stimulus-components/sortable sortablejs @rails/request.js
import { Application } from '@hotwired/stimulus'
import Sortable from '@stimulus-components/sortable'
const application = Application.start()
application.register('sortable', Sortable)
view 関係
今回テーブル内を入れ替える予定なので、tbody配下全てを対象とするため以下のようにしました。
%tbody{ 'data-controller': 'sortable', 'data-sortable-handle-value': '.handle'}
= render partial: 'test', collection: @tests
並びが終わった直後のポスト先を指定しときます
%tr{ 'data-sortable-update-url': position_change_admin_test_path(test)}
%td.border.cursor-pointer{class: 'handle'}
%i.fa-solid.fa-up-down
%td.border
%#- 省略
resources :tests, shallow: true do
patch :position_change, on: :member
end
ここまで設定していると、とりあえず動く状態にはなるかと思います
コントローラーにはこんな感じで、パラメータが送られてくるのが確認できました。
Parameters: {"position"=>"11", "id"=>"1"}
コントローラー 関係
少し力業が否めないですが、自分はこのように実装しました。
考え方:①クエリ上で表示順が欲しい
データが取得した後に番号を割り当てるのもできますが、クエリ内でそれを実装したかったので、以下のようなクエリを生成していました。
ad_num_query = Test.all.select('id , ROW_NUMBER() OVER(ORDER BY tests.display_order ASC) as row_num')
Test.all.joins("JOIN (#{ad_num_query.to_sql}) sub ON tests.id = sub.id")
クエリはこんな感じになります
SELECT
`tests`.*
FROM
`tests`
JOIN (
SELECT
id
, ROW_NUMBER() OVER (ORDER BY tests.display_order ASC) as row_num
FROM
`tests`
) sub
ON tests.id = sub.id
考え方:②①の順番でレコードを更新したい
既存レコードが1から順次割り当てられてるとは限らないので、割り当てる処理を考えてみました。
ad_num_query = Test.all.select('id , ROW_NUMBER() OVER(ORDER BY tests.display_order ASC) as row_num')
Test.all.joins("JOIN (#{ad_num_query.to_sql}) sub ON tests.id = sub.id")
.update_all('display_order = sub.row_num')
クエリはこんな感じになります
UPDATE `tests`
JOIN (
SELECT
id
, ROW_NUMBER() OVER (ORDER BY tests.display_order ASC) as row_num
FROM
`tests`
) sub
ON tests.id = sub.id
SET
display_order = sub.row_num
scopeを準備
上記で毎回同じjoinを利用してたので、scope化しました
scope :add_row_num, -> {
ad_num_query = self.select('id , ROW_NUMBER() OVER(ORDER BY tests.display_order ASC) as row_num')
joins("JOIN (#{ad_num_query.to_sql}) sub ON tests.id = sub.id")
}
①と②を踏まえ、コントローラーを作成
def position_change
@test = Test.find(params[:id])
return unless @test.update(display_order: params['position'])
base_query = Test.all
if params['position'].to_i == 1
# p '最初'
base_query.where.not(id: @test).add_row_num
.update_all('display_order = sub.row_num + 1')
elsif params['position'].to_i == base_query.size
# p '最後'
base_query.where.not(id: @test).add_row_num
.update_all('display_order = sub.row_num')
else
# p '途中'
base_query.where.not(id: @test).add_row_num
.where(display_order: ..params['position'].to_i)
.update_all('display_order = sub.row_num')
base_query.where.not(id: @test).add_row_num
.where(display_order: params['position'].to_i + 1..)
.update_all('display_order = sub.row_num + 1')
end
end
※補足説明
- 【最初(position=1)】
-
・自分のレコードは先にposition=1で更新
・自分を除くレコード(この時点では、1からスタートされてる)をposition=row_num + 1で更新 - 【最後(position=last)】
-
・自分のレコードは先にposition=lastで更新
・自分を除くレコードをposition=row_numで更新 - 【途中(position=???)】
-
・自分のレコードは先にposition=lastで更新
・自分を除く自分より数字の小さいレコードをposition=row_numで更新
・自分を除く自分より数字の大きいレコード(この時点では、配置したpositionからスタートされてる)をposition=row_num + 1で更新
さいごに
コントローラーに関しては、色々なやり方があると思います。
自分のやり方も正解とは思っていませんので、あくまで参考レベルで見ていただけると幸いです。
もっといい方法あるよ!という意見がありましたら、ご教授ください。