Resources

Trello Clone Episodes
1 - https://www.driftingruby.com/episodes/multitenancy
2 - https://www.driftingruby.com/episodes/drag-and-drop-with-draggable

Update:

There is a bug where the item cannot be found when moving and sorting between the lists because in the ItemsController, we're only looking for the item based on the column it was now in. So, the code has been updated as follows:

# items controller
  def update
    params[:positions].uniq.each_with_index do |id, index|
      # Changed @column to @list
      @list.items.find(id).update(position: index + 1)
    end
    ...
  end
  ...
  def set_item
    # changed @column to @list
    @item = @list.items.find(params[:id])
  end

# models/list.rb
# add association to items from a list
has_many :items, through: :columns
Download Source Code

Summary

# Terminal
rails g migration add_position_to_items position:integer
rails db:migrate

# db/migrate/XXXXXX_add_position_to_items.rb
class AddPositionToItems < ActiveRecord::Migration[6.0]
  def change
    add_column :items, :position, :integer, default: 0
  end
end

# javascript/controllers/draggable_controller.js
import { Controller } from 'stimulus'
import { Sortable } from '@shopify/draggable'

export default class extends Controller {
  static targets = ['column', 'item']
  initialize() {}
  connect() {
    if (this.hasItemTarget) {
      this.itemTargets.forEach(item => {
        item.setAttribute('style', 'z-index: 1000;')
      })
      const sortable = new Sortable(this.columnTargets, {
        draggable: 'li'
      })
      sortable.on('sortable:stop', function(event) {
        let array = Array.from(event.newContainer.children)
        array.forEach((item, index) => {
          if (item.classList.contains('draggable--original')) {
            array.splice(index, 1)
          } else if (item.classList.contains('draggable-mirror')) {
            array.splice(index, 1)
          }
        })
        let positions = array.map(item => item.dataset.id)
        let url = event.dragEvent.source.getAttribute('data-url')
        let column = event.newContainer.getAttribute('data-id')
        let data = { item: { column_id: column }, positions: positions }
        let token = document.head.querySelector('meta[name="csrf-token"]').getAttribute('content')
        fetch(url, {
          method: 'PUT',
          credentials: 'same-origin',
          headers: {
            "X-CSRF-Token": token,
            "Accept": "application/json",
            "Content-type": "application/json"
          },
          body: JSON.stringify(data)
        })
      })
    }
  }
  disconnect() {}
}

# views/items/_item.html.erb
<%= content_tag :li, data: { target: 'draggable.item',
                             id: item.id,
                             url: list_column_item_path(list, column, item) } do %>

# controllers/items_controller.rb
  def update
    params[:positions].uniq.each_with_index do |id, index|
      @list.items.find(id).update(position: index + 1)
    end
    if @item.update(item_params)
      respond_to do |format|
        format.html { redirect_to @list, notice: 'Item was successfully updated.' }
        format.json {}
      end
    else
      render :edit
    end
  end

# controllers/lists_controller.rb
    def set_list
      @list = current_company.lists
                             .includes(columns: :items)
                             .order('columns.id, items.position ASC')
                             .find(params[:id])
    end