0

I'm trying to implement the "Drag and Drop Sortable Lists" (https://gorails.com/episodes/rails-drag-and-drop-sortable) with nested_attributes.

Unfortunately, I'm getting the wrong :position. It jumps from 2 to 5 to 7, even when there should be only three positions in total.

I believe the problem is that hidden input tags are being counted as well. If this is the main problem, any way around this?

// config/routes.rb

  resources :recipes, except: :index do
    resources :steps do
      resource :step_position, only: :update
    end
  end

// app/views/recipes/_form.html.erb

<div data-controller="sortable" data-sortable-url="/recipes/:recipe_id/steps/:id/step_position">
  <%= f.fields_for :steps, @recipe.steps.order(position: :asc) do |step| %>
    <div data-id="<%= step.object.id %>">
      <%= step.text_area :description %>
    </div>
  <% end %>
</div>
// app/javascript/controllers/sortable_controller.js

import { Controller } from "stimulus";
import Rails from "@rails/ujs";
import { Sortable } from "sortablejs";

export default class extends Controller {
  connect() {
    this.sortable = Sortable.create(this.element, {
      onEnd: this.end.bind(this),
    });
  }

  end(event) {
    let id = event.item.dataset.id;
    let data = new FormData();
    data.append("position", event.newIndex + 1);
    Rails.ajax({
      url: this.data.get("url").replace(":id", id),
      type: "PATCH",
      data: data,
    });
  }
}
// app/controllers/step_positions_controller.rb

class StepPositionsController < ApplicationController
  def update
    @step = Step.find(params[:step_id])
    authorize @step.recipe

    @step.insert_at(params[:position].to_i)

    head :ok
  end
end
2
  • Did you ever get this working? I am stuck at the same spot and my sort order is doing the same thing. Jumping up to 7 when I only have 3 total. If so what did you end up doing?
    – spacerobot
    Commented Sep 9, 2020 at 18:20
  • @spacerobot, yeah I did! Just posted the answer. Commented Sep 11, 2020 at 2:47

2 Answers 2

1

The problem was that hidden input tags were being counted as well.

I solved it by adding <%= f.hidden_field :id %>

<!-- app/views/recipes/_form.html.erb -->

<div data-controller="sortable" data-sortable-url="/recipes/:recipe_id/steps/:id/step_position">
  <%= f.fields_for :steps, @recipe.steps.order(position: :asc), include_id: false do |step| %>
    <%= render "step_fields", f: step %>
  <% end %>
</div>
<!-- app/views/recipes/_step_fields.html.erb -->

<%= content_tag :div, class: "nested-fields", data: { new_record: f.object.new_record?, id: f.object.id } do %>
  <%= f.hidden_field :id %>
  <%= f.text_area :description %>

  <small><%= link_to "Delete", "#", data: { action: "click->new-fields#removeField" } %></small>
  <%= f.hidden_field :_destroy %>
<% end %>
3
  • Thanks! Would you mind posting your step controller?
    – spacerobot
    Commented Sep 11, 2020 at 15:13
  • @spacerobot Stayed the same as above! Didn't have to touch it. Commented Sep 12, 2020 at 2:18
  • @MirhaMasala Thanks, in my case adding <%= f.hidden_field :id %> solves the issue.
    – aldrien.h
    Commented Apr 25, 2022 at 3:59
0

Firstly take a look at the parameters being passed to the server. Do these look ok? If not, then the javascript might be to blame.

Secondly, insert_at isn't the best tool for the job in this case. If you want to explicitly set the position explicitly without interference from the callbacks etc... I usually use where(:id => id).update_all(:position => the_position) on each element in the list (provided you know you have all of them in your params.

The best solution is to have the javascript send you the item being moved and the position in the list that you want it to be, then just set the position column to that value and acts_as_list will shuffle things out of the way for you. The only downside to this is concurrent updates from other users. Your list will get out of sync if this is the case.

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