VOOZH about

URL: https://dev.to/railsdesigner/adding-edit-delete-and-reposition-for-nested-forms-in-rails-with-stimulus-3okm

⇱ Adding edit, delete and reposition for nested forms in Rails with Stimulus - DEV Community


In a previous article, I explored building nested forms with Stimulus. But what about when you need to edit existing questions, remove ones you no longer need or reorganize them? Let’s extend that foundation by adding: editing, deleting and repositioning questions using drag-and-drop.

This article builds directly on the previous setup, so make sure you have that in place before continuing (check out the repo for the full code base). The reposition logic is inspired by this article to create a Kanban board.

First, update the migration to include a unique index:

class AddPositionToQuestions < ActiveRecord::Migration[8.1]
 def change
 add_column :questions, :position, :integer, null: false
 add_index :questions, [:survey_id, :position], unique: true
 end
end

I like the positioning gem for this, make sure to set it up correctly.

Editing questions is really just vanilla Rails stuff, update the SurveysController with edit and update actions. It will save the question’s content alongside the survey models (I recently wrote how you can do this without using accepts_nested_attributes_for).

Now the more interesting part is the logic to reposition questions. Let’s go over the related parts. Add a RepositionController that stores the new position after drag-and-drop.

class RepositionController < ApplicationController
 def update
 resources.each_with_index do |resource, index|
 resource.update!(position: params[:new_position].to_i + index)
 end
 end

 private

 def resources
 resource_class.where(id: Array(params[:ids]))
 end

 def resource_class
 request.path.split("/")[1].singularize.classify.constantize
 end
end

Next is to updates your routes to include the new action:

Rails.application.routes.draw do
+ resources :questions, only: %w[destroy] do
+ collection do
+ patch :reposition, controller: "reposition", action: "update"
+ end
+ end
end

Update the nested-fields controller to support question removal.

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
+ remove(event) {
+ const field = event.target.closest("[data-sortable-id-value]");
+ const destroyInput = field.querySelector('input[name*="_destroy"]');

+ if (destroyInput) {
+ destroyInput.value = "1";

+ field.hidden = true;
+ } else {
+ field.remove();
+ }
+ }
}

The remove() method checks if there’s a _destroy hidden field (for existing records). If so, it sets the value to “1” and hides the field. For new records without this field, it simply removes the element from the DOM.

Sortable Stimulus controller

Create a new Stimulus controller (this is mostly copied verbatim from the Kanban board article I mentioned) for drag-and-drop functionality:

// app/javascript/controllers/sortable_controller.js
import { Controller } from "@hotwired/stimulus"
import { Sortable } from "sortablejs"
import { patch } from "@rails/request.js"

export default class extends Controller {
 static values = { endpoint: String };

 connect() {
 Sortable.create(this.element, {
 group: "questions",
 animation: 150,
 easing: "cubic-bezier(1, 0, 0, 1)",
 ghostClass: "opacity-50",
 selectedClass: "selected",
 onEnd: (event) => this.#updatePosition(event)
 });
 }

 async #updatePosition(event) {
 const items = event.items?.length > 0 ? event.items : [event.item];
 const ids = items.map(item => item.dataset.sortableIdValue);

 await patch(this.endpointValue, {
 body: JSON.stringify({
 ids: ids,
 new_position: event.newIndex + 1
 })
 });
 }
}

This controller uses SortableJS and @rails/request.js to enable drag-and-drop.

Update the edit view

Create the edit view ( app/views/surveys/edit.html.erb ):

<h1>Edit Survey</h1>

<%= form_with model: @survey, data: {controller: "nested-fields"} do |form| %>
 <div>
 <%= form.label :name %>

 <%= form.text_field :name %>
 </div>

 <ul data-nested-fields-target="fields" data-controller="sortable" data-sortable-endpoint-value="<%= reposition_questions_path %>">
 <%= form.fields_for :questions do |question_form| %>
 <li data-sortable-id-value="<%= question_form.object.id %>">
 <%= question_form.hidden_field :_destroy %>

 <%= question_form.label :content, "Question" %>
 <%= question_form.text_area :content %>

 <%= button_tag "Remove", type: :button, data: { action: "nested-fields#remove" } %>
 </li>
 <% end %>
 </ul>

 <div>
 <!-- … -->
 </div>
<% end %>

Key points in this view:

  • The <ul> has both nested-fields and sortable controllers
  • Each <li> has data-sortable-id-value for drag-and-drop tracking
  • The _destroy hidden field marks questions for deletion
  • The template includes the _destroy field for new questions too

And there you have it. A complete nested forms solutions for Rails with Stimulus that includes adding new questions, deleting and reposition them. From here it is straight-forward to add nested answers too!