0

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

2 Answers 2

1

acts_as_list allows you to set the new scope parameter and position of a list item, then just save the item and the item will be moved out of the old scope and into the new scope at the position you desire automatically (using after_save callbacks).

In this regard, you should be able to just do that, then freshly fetch the items in each of your two scopes and return them to your front end for updating your display.

2
  • I think I figured it out late last night! I posted an answer here, mind checking it out to see if that's a good working solution? Btw I love that you're so active in answering questions here for your own gem. Makes the lives of simple people like me much easier!
    – J. Jackson
    Commented Apr 1, 2020 at 14:54
  • You're most welcome :) I have to admit, I'm just the maintainer of the gem. Other people did most of the authoring of the gem :) Commented Apr 2, 2020 at 2:35
0

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
1
  • 1
    That's basically right, however you can just include the position in the update clause: @category_item.update(category_id: params[:new_category_id], position: params[:new_index]). acts_as_list is designed to allow you to set the position and the scope and everything will be handled. As far as syncing the lists on the front end, you could just rely on the update having been a success and not push anything to the front except for head :no_content. Depends on your interface and if you're worried about concurrent updates. Commented Apr 2, 2020 at 2:35

Not the answer you're looking for? Browse other questions tagged or ask your own question.