Create a drag and droppable List in Rails
Add acts_as_votable to the Gemfile
gem 'acts_as_list
Add StimulusJS
rails webpacker:install:stimulus
Add SortableJS
yarn add sortablejs
Example of Models - the child should have a position
column of type integer
rails g model TodoList
rails g model TodoListItem position:integer todo_list:references
rake db:migrate
Define the acts_as_list scope
class TodoList < ActiveRecord::Base
has_many :todo_list_items, -> { order(position: :asc) }
end
class TodoListItem < ActiveRecord::Base
belongs_to :todo_list
acts_as_list scope: :todo_list
end
Note that we have a data-controller="drag-and-drop"
attribute which will link up with our StimulusJS controller and signal to SortableJS that elements within container
are draggable. The card element should have a StimulusJS target and data-id attribute like so: data-target="drag-and-drop.id" data-id="<%= item.id %>
.
<div class="container" data-controller="drag-and-drop" data-drag-url="/sections/:id/move">
<% @todo_list.todo_list_items.each do |item| %>
<div class="card card-body m-1" data-target="drag-and-drop.id" data-id="<%= item.id %>">
Item with id: <%= item.id %>
</div>
<% end %>
</div>
Create a new stimulus controller (or rename the hello_controller.js
to drag_and_drop_controller.js
). Inside the stimulus controller this.element
refers to the element on which the stimulus controller is mounted, ie div.container
. When passed to Sortable.create
Sortable recognizes this as the parent element and all child elements will be sortable. The onEnd: ''
option allows us to capture the end of the drag-and-drop event. With onEnd: this.end.bind(this)
we are passing on that event to a stimulus function, which we defined as end()
below. Inside this function we grab the url
and id
as well as defining the data for our ajax
request. data.append("position", event.newIndex + 1)
adds a parameter of position
which we will then access in our controller.
import { Controller } from "stimulus"
import Sortable from 'sortablejs'
export default class extends Controller {
static targets = [ "id" ]
connect() {
this.sortable = Sortable.create(this.element, {
onEnd: this.end.bind(this),
})
}
end(event) {
let url = this.element.dataset.dragUrl
let data = new FormData()
let id = event.item.dataset.id
data.append("position", event.newIndex + 1)
Rails.ajax({
url: url.replace(":id", id),
type: 'PATCH',
data: data
})
}
}
Add the corresponding route
patch "items/:id/move", to: "sections#move"
and the corresponding action in the TodoListController
def move
@item = Item.find(params[:id])
@item.insert_at(params[:position].to_i)
head :ok
end
Import Rails if its not defined in the stimulus controller
import Rails from "@rails/ujs"
you can add a ghostClass (the original position of the element you are dragging) or a dragClass (the element that is being dragged) to target them with css. If you dont want the whole child div to be draggable you can add a handle and the div will be draggable via that handle.
this.sortable = Sortable.create(this.element, {
onEnd: this.end.bind(this),
ghostClass: "sortable-ghost",
dragClass: "sortable-drag",
handle: '.fa-grip-vertical'
})
Such that your view looks like this
<div class="container" data-controller="drag-and-drop" data-drag-url="/sections/:id/move">
<% @todo_list.todo_list_items.each do |item| %>
<div class="card card-body m-1" data-target="drag-and-drop.id" data-id="<%= item.id %>">
Item with id: <%= item.id %>
<i class="fas fa-grip-vertical"></i>
</div>
<% end %>
</div>
With the following example style
.sortable-ghost {
background: red;
}
.sortable-drag {
background: blue;
}