Adding edit, delete and reposition for nested forms in Rails with Stimulus
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.
Make JavaScript your second favorite language
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 bothnested-fieldsandsortablecontrollers - Each
<li>hasdata-sortable-id-valuefor drag-and-drop tracking - The
_destroyhidden field marks questions for deletion - The template includes the
_destroyfield 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!
Want to read me more?
-
Nested Forms With Turbo (without dependencies)
Nested forms with Rails can now be done without using any third-party gem! Turbo Stream's is all you need to add nested fields to any form you want. -
Building Nested Forms in Rails with Stimulus
Adding nested forms in Rails is easy enough with Stimulus, no third-party gems, like Cocoon, needed. -
Extending the Kanban board (using Rails and Hotwire)
This article extends the previous kanban board with adding cards and columns and moving multiple cards at once.
Over to you…
What did you like about this article? Learned something knew? Found something is missing or even broken? 🫣 Let me (and others) know!
Comments are powered by Chirp Form
{{comment}}