Create a drag and droppable List in Rails
Add acts_as_votable to the Gemfile
gem 'acts_as_list
Add StimulusJS
rails webpacker:install:stimulusAdd SortableJS
yarn add sortablejsExample 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:migrateDefine 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
endNote 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
endImport 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;
}