Rails 在 Rails 中实现拖拽排序功能

jesktop · 2018年04月18日 · 最后由 baurine 回复于 2018年05月29日 · 7815 次阅读
本帖已被管理员设置为精华贴

回想到很久以前,JavaScript 为了实现前端拖拽排序的事情,都是一个不容易的事。自从有了 jQuery 和jQuery UI后,很多常见的功能直接使用起来已经非常方便了,不需要自己进行二次开发。

好,今天我们就使用sortable,完成加上服务器端(Rails)的逻辑,完成一个完整的拖拽排序功能。

实现的逻辑是什么?

在服务器端添加排序的字段(position),然后我们的 order 排序根据 position 进行排序。每当我们前端拖拽结束后,通过异步 Ajax 把排序的结果发送到服务器,服务器根据传送过来的内容,重新设置 position 信息。

添加需要使用的 Gem

gem 'acts_as_list'
gem 'jquery-ui-rails'

jquery-ui中的 sortable 提供前端拖拽排序效果的功能,acts_as_list则是一款服务器端排序功能的 gem,提供丰富的方法,当然如果仅仅是一个简单的排序功能,这里不使用acts_as_list也是可行的。

前端加入拖拽排序效果

我们先在application.js引入jquery-ui

//= require jquery-ui/widgets/sortable

然后我们加入调用sortable的方法:

document.addEventListener("turbolinks:load", function() {
  $( "[data-behavior='sortable']" ).sortable();
})

这样一来,只要在需要排序的 dom 中,加入data-behavior='sortable'就可以实现拖拽排序的效果了,参考:

<table class="table table-bordered table-hover">
  <thead>
    ...
  </thead>
  <tbody data-behavior='sortable'>
    ...
  </tbody>
</table>

加入排序接口

首先我们根据acts_as_list的文档,加入一个排序需要用到的字段:

rails g migration AddPositionToTodoItem position:integer
rails db:migrate

然后我们需要添加一个接口,用于处理排序信息:

// route.js

Rails.application.routes.draw do 
  resources :todo_items do
    collection do
      patch :sort 
    end
  end 
  ...
end

// controller

class TodoItemsController < ApplicationController
  def index
    @todo_items = TodoItems.order(:position)
  end

  def sort

  end
end

发送异步请求

现在服务器端的接口完成了,我们的目的就是当排序结束后,把数据打包发到这个接口中,为了方便操作,我把接口地址放在 Dom 里的data-url属性中。

<tbody data-url="<%= sort_todo_items_path %>" data-behavior='sortable'>
...
</tbody>

并且每个拖拽的元素,我们把它的 ID 设置为model的名称_model的id

<tbody data-url="<%= sort_todo_items_path %>" data-behavior='sortable'>
  <% @todo_items.each_with_index do |todo_item, index| %>
    <tr id="<%= dom_id(todo_item) %>">
      ...
    </tr>
  <% end %>
</tbody>

然后我们在原来的sortable中,添加 update 方法,处理异步事务:

$( "[data-behavior='sortable']" ).sortable({
  update: function(e, ui) {
    $.ajax({
      url: $(this).data("url"),
      type: "PATCH",
      data: $(this).sortable('serialize')
    })
  }
});

通过上面的 JavaScript 代码,就可以实现,每天拖拽更新后,就会发送数据到对应的接口上,那发送过去的数据会是怎么样的呢?

处理数据

当我们尝试操作一遍后,就会发现发送到服务器的数据格式如下:

Parameters: {"todo_item"=>["2", "1", "3"]}

这个就是todo_item数据里,每个 id 的排序情况。所以我们可以根据这个排序信息,重新设置position就可以达到我们想要的排序结果。

...
def sort
  params[:todo_item].each_with_index do |id, index|
    TodoItem.where(id: id).update_all(position: index + 1)
  end

  head :ok
end
...

好了,目前我们前后端的功能都完成了,赶快试试效果吧。

原文链接:https://www.jianshu.com/p/d9911c234107

jasl 将本帖设为了精华贴。 04月19日 13:49

GoRails 最近也有一期讲 Drag and drop 的 https://gorails.com/episodes/sortable-drag-and-drop

厉害厉害 学习了

zchar 回复

回复是为了上下文的对应关系

这个做法的要点有

  • decimal 的长度适当长一点,有时会因长度不够,而造成多个 position 都为 0.00。
  • 对移动到第一位,或者最后一位做 if 处理

补上 ruby 代码:

def reposition
  task = Task.find(params[:id])
  next_task = params[:next_id] && Task.find(params[:next_id])
  prev_task = params[:prev_id] && Task.find(params[:prev_id])
  position = if params[:prev_id].blank?
               next_task.position / 2
             elsif params[:next_id].blank?
               prev_task.position + 100000
             else
               (prev_task.position + next_task.position) / 2
             end
  task.update(position: position)
end
zchar 回复

想知道,当拖动一个元素排序之后,如何获取它和它所处的前后元素的 ID?


自己试了一下 用比较笨的方式 拿到了,有没有更好的方式?我去看了文档没有找到可以直接选择当前元素的方法。

$( "[data-behavior='sortable']" ).sortable({
  update: function(e, ui) {

    console.log($(ui.item[0]).prev().attr("id")])
    console.log($(ui.item[0]).attr("id"))
    console.log($(ui.item[0]).next().attr("id"))
    $.ajax({
      url: $(this).data("url"),
      type: "PATCH",
      data: $(this).sortable('serialize')
    })
  }
});

今天我把这两种方式的排序实验了一下,如果页面有分页的话第二页的排序会混乱,所以不能用分页,并且当前页的数据量不能太大,操作也不能太频繁,生成的 sql 很多。

GoRails 直接用 Vue.Draggable 解决了

@zchar 应该用 float, decimal 不但慢,容错性性也有问题。

我这里的做法是把顺序以 array 格式单独存在另一个表里,这样每次只更新一次,当然这样做的话,读取时要读两张表,或者 join 两张表。

感觉@zchar@fan124 的做法都不错。 数据量大了,我说的方法会产生大量的 SQL。另外关于分页这个问题我也想过,但是考虑到,数据量很大的需要分页了,然后通过拖拽去排序这种情况好像没有见过。

alvin2ye 回复

谢谢指正,确实是 float,我写错了 😀

受教了,目前有类似的需求,决定试试 @fan124 的方案。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号