ruby-on-railsjbuildersortablejsacts-as-list

How to handle drag and drop between multiple lists using SortablejS and acts_as_list gem?


I've been searching for a day and a half for a solution.

I've got several tables (Categories) that allow the user to drag and drop each row (Item) to reorder the position of each item on that table (Category). I'm using SortableJS to handle the drag and drop, and I have it so that reordering within the same Category works.

On my backend I've got a Rails API that uses Jbuilder to return JSON, and am using the acts_as_list gem to handle the positions for me.

I'm having issues figuring out how to handle the reorder when I drag an Item from Category A to Category B. I believe the issue lies in the controller action and my inability to come up with a solution to receive updates for multiple categories, and then return the updated category's positions with Jbuilder. Some help would be greatly appreciated!

ItemsList.vue

    <script>
        methods: {
            async dragReorder({ item, to, from, oldIndex, newIndex, oldDraggableIndex, newDraggableIndex, clone, pullMode }) {

            // item that was dragged
            const draggedItem = JSON.parse(item.getAttribute('data-item'));

            // initial payload
            const payload = {
              category_id: draggedItem.category_id,
              category_item_id: draggedItem.pivot.id,
              item_id: draggedItem.id,
              new_index: newIndex + 1,
              user_id: this.user.id,
            };

            const newCategory = JSON.parse(to.getAttribute('data-category'));

            // if item is dragged to new category
            if (pullMode) payload.new_category_id = newCategory;

            await categoryService.updatePosition(payload);
          },
        },
        mounted() {
          this.$nextTick(() => {
            const tables = document.querySelectorAll('.items-table-container');
            tables.forEach(table => {
              const el = table.getElementsByTagName('tbody')[0];
              Sortable.create(el, {
                animation: 150,
                direction: 'vertical',
                easing: 'cubic-bezier(.17,.67,.83,.67)',
                group: 'items-table-container',
                onEnd: this.dragReorder,
              });
            });
          })
        },
    </script>

category.rb

    class Category < ApplicationRecord
      has_many :categories_items, -> { order(position: :asc) }
      has_many :items, through: :categories_items, source: :item

      accepts_nested_attributes_for :items
    end

categories_item.rb

    class CategoriesItem < ApplicationRecord
      belongs_to :category
      belongs_to :item

      acts_as_list scope: :category
    end

item.rb

    class Item < ApplicationRecord
      has_many :categories_items
      has_many :categories, through: :categories_items, source: :category
    end

categories_controller.rb

    def update_position
       item = CategoriesItem.find_by(category: params[:category_id], item: params[:item_id])

       # if item was moved to new category
       categories = []
       if params[:new_category_id]
          item.update(category_id: params[:new_category_id])
          Item.find(params[:item_id]).update(category_id: params[:new_category_id])
          item.insert_at(params[:new_index]) unless !item
          categories << Category.find(params[:category_id])
          categories << Category.find(params[:new_category_id])
        else
          item.insert_at(params[:new_index]) unless !item
          categories << Category.find(params[:category_id])
        end
        @categories = categories
    end

update_position.json.jbuilder

    json.array! @categories do |category|
      json.(category, :id, :name, :created_at, :updated_at)
      json.categories_items category.categories_items do |category_item|
        json.(category_item, :id, :category_id, :item_id, :created_at, :updated_at, :position)
      end
    end

Solution

  • Got something working finally, here's my working code:

        def update_position
          @categories = []
    
          ## if item moved to new scope ##
          if params[:new_category_id] 
            @category_item.update(category_id: params[:new_category_id])
            @category_item.insert_at(params[:new_index])
            @categories << Category.find(params[:new_category_id])
            @categories << Category.find(params[:category_id])
    
          ## else if item was moved within same scope ##
          else
            @category_item.insert_at(params[:new_index])
            @categories << Category.find(params[:category_id])
          end
    
          @categories
          render :index
        end