dynamic-nested-attributes
Dynamic Nested Attributes
Overview
Implement Rails nested attributes with dynamic add/remove functionality using Turbo Streams and Simple Form. This pattern allows users to add and remove associated records inline within a parent form.
When to Use
- Building forms where users need to manage multiple child records (has_many associations)
- Adding/removing nested items without page refresh
- Bulk creation or editing of associated records
- Forms requiring progressive disclosure of additional fields
Key Components
1. Form Object or Model
- Accepts nested attributes for the association
- Use
accepts_nested_attributes_for :association_namein the model or form object
2. Main Form View
Create a form that includes:
simple_fields_forfor rendering existing nested items- A container element with an ID for appending new items (e.g.,
#accessories) - A link to add new items that triggers a Turbo Stream request
Example:
= simple_form_for resource do |f|
= f.simple_fields_for :items do |ff|
= render 'item_fields', f: ff, resource:
= render 'add_button', index: resource.items.size
Add Button Partial (_add_button.html.slim):
-# locals: (index:)
= link_to icon('add'), new_parent_item_path(index: index),
id: 'add_button', class: 'btn', data: { turbo_stream: true }
3. Nested Fields Partial
Create a partial (e.g., _item_fields.html.slim) that:
- Wraps fields in a unique container with an ID based on index
- Includes a data controller for remove functionality
- Shows a delete button for all records
- Includes all form inputs for the nested item
Example:
fieldset id="item_#{f.index}" controller='destroy-nested-attributes'
= f.hidden_field :_destroy, data: { destroy_nested_attributes_target: 'input' }
= f.input :name
= f.input :quantity
.form-row__actions
= button_tag icon('delete'), type: 'button', class: 'btn btn-delete',
data: { action: 'destroy-nested-attributes#perform' }
4. Controller Actions
Implement a new action that:
- Builds a new nested item
- Accepts
indexparameter for tracking position
Example:
def new
@item = Item.new
end
5. Turbo Stream Response
Create a new.turbo_stream.slim view that:
- Updates the "add" button with incremented index
- Appends the new nested fields to the container
- Uses
indexparameter to ensure unique field names - Works with non-persisted parents by using a symbol and empty URL
Example:
= turbo_stream.replace 'add_button', partial: 'add_button', locals: { index: params[:index].to_i + 1 }
= simple_form_for :parent, url: '' do |f|
= f.simple_fields_for :items_attributes, @item, index: params[:index] do |ff|
= turbo_stream.append 'items', partial: 'item_fields', locals: { f: ff }
6. Remove Stimulus Controller
Create a Stimulus controller to handle client-side removal:
Example JavaScript:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ['input']
static classes = ['destroyed']
connect() {
if (!this.hasDestroyedClass) {
this.element.setAttribute(`data-${this.identifier}-destroyed-class`, 'is-hidden')
}
}
perform() {
this.inputTarget.value = '1'
this.element.classList.add(this.destroyedClass)
}
}
Implementation Checklist
- Add
accepts_nested_attributes_forto model/form object - Create main form with
simple_fields_forand container element - Create nested fields partial with remove functionality
- Implement controller
newaction with index support - Create turbo_stream response view
- Add Stimulus controller for client-side removal
- Update routes to support nested resource creation
- Update strong parameters to permit nested attributes
- Add policy authorization if using Pundit
Common Patterns
Dynamic Collections Based on Parent Selection
Pass filtered collections to nested partials:
= render 'item_fields', f: ff, resource:, collection: resource.items
Routes Example
If there is not an existing new route in use, use the following pattern
resources :items, only: [:new]
If one does exist, create a new namespaced controller
namespace :parent do
resources :items, only: [:new]
end
Strong Parameters Example
def item_params
params.require(:parent).permit(
:category,
:subcategory,
items_attributes: %i[
name
quantity
part_id
optional
hidden
]
)
end
Related Patterns
- Turbo Frame inline editing
- Stimulus data controller integration
- Form object pattern for bulk operations
- Policy-scoped collections for associations
More from rolemodel/rolemodel-skills
bem-structure
Expert guidance for writing, refactoring, and structuring CSS using BEM (Block Element Modifier) methodology. Provides proper CSS class naming conventions, component structure, and Optics design system integration for maintainable, scalable stylesheets.
81optics-context
Use the Optics design framework for styling applications. Apply Optics classes for layout, spacing, typography, colors, and components. Use when working on CSS, styling views, or implementing design system guidelines.
37routing-patterns
Review, generate, and update Rails routes following professional patterns and best practices. Covers RESTful resource routing, route concerns for code reusability, shallow nesting strategies, and advanced route configurations.
28turbo-fetch
Implement dynamic form updates using Turbo Streams and Stimulus. Use when forms need to update fields based on user selections without full page reloads, such as cascading dropdowns, conditional fields, or dynamic option lists.
27stimulus-controllers
Create and register Stimulus controllers for interactive JavaScript features. Use when adding client-side interactivity, dynamic UI updates, or when the user mentions Stimulus controllers or JavaScript behavior.
26controller-patterns
Review and update existing Rails controllers and generate new controllers following professional patterns and best practices. Covers RESTful conventions, authorization patterns, proper error handling, and maintainable code organization.
26