javascript erb ruby 99 lines · 4 tabs

Stimulus: nested fields add/remove without re-rendering

Shared by codesnips Jan 2026
4 tabs
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["wrapper", "template", "anchor"]

  add(event) {
    event.preventDefault()

    const index = new Date().getTime()
    const html = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, index)

    this.anchorTarget.insertAdjacentHTML("beforebegin", html)
  }

  remove(event) {
    event.preventDefault()

    const row = event.target.closest("[data-nested-form-row]")
    if (!row) return

    const destroyInput = row.querySelector("input[name*='_destroy']")

    if (destroyInput) {
      destroyInput.value = "1"
      row.style.display = "none"
    } else {
      row.remove()
    }
  }
}
4 files · javascript, erb, ruby Explain with highlit

This snippet shows the classic Rails accepts_nested_attributes_for problem solved on the client with Stimulus, so adding and removing child rows never touches the server until submit. The core trick is a <template> tag holding one blank child form whose input names contain a placeholder index; on insert the placeholder is swapped for a unique number so Rails parses each row into a distinct nested-attributes hash.

The nested_form_controller.js tab defines the controller. It declares a wrapper target (the container that holds existing rows) and a template target (the hidden blueprint). The add action clones templateTarget.innerHTML, replaces every NEW_RECORD token with new Date().getTime() to guarantee a collision-free index, and appends it before an anchor node. Using a timestamp rather than a running counter avoids index reuse when rows are added, removed, and re-added within the same page, which would otherwise cause two rows to submit under the same key and silently overwrite each other.

Removal is handled differently for new versus persisted records. In remove, if the row has no database id it is simply detached from the DOM. If it is an existing record, the row is hidden and a hidden _destroy input is set to 1, which tells accepts_nested_attributes_for (with allow_destroy: true) to delete that association on save. This preserves the ability to undo-by-not-submitting and keeps unsaved edits intact.

The _form.html.erb tab wires the markup: data-controller, the data-nested-form-target attributes, and data-action click handlers. Rails' fields_for renders existing children, while the <template> renders a single set of fields built with child_index: 'NEW_RECORD' so the placeholder ends up in the input names.

The line_items_controller.rb tab shows the server side that makes this coherent: strong params permit the nested attributes array including :id and :_destroy, and the model enables allow_destroy with reject_if to drop blank rows. The trade-off is that all validation still happens on submit, so client-side add/remove is purely structural; complex conditional logic or server-computed defaults still require a request. For ordinary CRUD-style nested forms, though, this pattern is fast, dependency-light, and degrades gracefully.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Stimulus: nested fields add/remove without re-rendering — share card
Link copied