Skip to content

Instantly share code, notes, and snippets.

@Haumer
Last active February 10, 2025 14:43
Show Gist options
  • Save Haumer/d175c967028202cae6339b996be3a63a to your computer and use it in GitHub Desktop.
Save Haumer/d175c967028202cae6339b996be3a63a to your computer and use it in GitHub Desktop.

Sortable List in Rails

Context

Create a drag and droppable List in Rails

Resources

Setup

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

Code

Views

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>

StimulusJS

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
    })
  }
}

Route & Controller

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

Debugging

Import Rails if its not defined in the stimulus controller

import Rails from "@rails/ujs"

Further Options

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;
  }

Example

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment