Stimulus controller for dynamic nested forms - works with Rails, React, Vue, or any Stimulus app
npm install hotwire-nested-form-stimulusA Stimulus controller for dynamic nested forms. Add and remove nested form fields with ease.
``bash`
npm install hotwire-nested-form-stimulusor
yarn add hotwire-nested-form-stimulus
`javascript
import { Application } from "@hotwired/stimulus"
import NestedFormController from "hotwire-nested-form-stimulus"
const application = Application.start()
application.register("nested-form", NestedFormController)
`
` html`
data-action="nested-form#add"
data-placeholder="NEW_ITEM_RECORD"
data-insertion="append"
data-target="#items">
Add Item
| Attribute | Description | Default |
|-----------|-------------|---------|
| data-placeholder | Placeholder string in template to replace with unique ID | "NEW_RECORD" |data-insertion
| | Where to insert: before, after, append, prepend | before |data-count
| | Number of fields to add per click | 1 |data-target
| | CSS selector for insertion container | Parent element |
Note: For backward compatibility, data-template (inline HTML) is still supported, but tags are recommended for deep nesting support.
`html
data-nested-form-min-value="1"
data-nested-form-max-value="5"
data-nested-form-limit-behavior-value="disable">
| Attribute | Description | Default |
|-----------|-------------|---------|
|
data-nested-form-min-value | Minimum items required | 0 |
| data-nested-form-max-value | Maximum items allowed | unlimited |
| data-nested-form-limit-behavior-value | "disable", "hide", or "error" | "disable" |$3
Requires SortableJS:
`bash
npm install sortablejs
``javascript
import Sortable from 'sortablejs'
window.Sortable = Sortable
``html
data-nested-form-sortable-value="true"
data-nested-form-sort-handle-value=".drag-handle">
☰
| Attribute | Default | Description |
|-----------|---------|-------------|
|
data-nested-form-sortable-value | false | Enable sorting |
| data-nested-form-position-field-value | "position" | Position field name |
| data-nested-form-sort-handle-value | (none) | Drag handle selector |$3
Add smooth CSS transitions when items are added or removed:
`javascript
import "hotwire-nested-form-stimulus/css/animations.css"
``html
data-nested-form-animation-value="fade"
data-nested-form-animation-duration-value="300">
| Attribute | Default | Description |
|-----------|---------|-------------|
|
data-nested-form-animation-value | "" | "fade", "slide", or "" (none) |
| data-nested-form-animation-duration-value | 300 | Duration in milliseconds |$3
For multi-level nesting, use
tags and data-placeholder attributes. Each nesting level needs its own data-controller="nested-form" and a unique placeholder:`html
data-placeholder="NEW_TASK_RECORD"
data-insertion="append" data-target="#tasks">Add Task
`The controller replaces only the matching placeholder per button, so nested templates stay intact.
$3
Accessibility is enabled by default. The controller automatically:
- Sets
role="group" and aria-label on the container
- Creates a live region for screen reader announcements
- Manages focus on add/remove/duplicate actionsDisable with:
`html
data-nested-form-a11y-value="false">
`$3
Add a duplicate button to clone an existing item with its field values:
`html
`The clone gets a new unique index and any persisted record ID is removed so it saves as a new record.
$3
| Event | Cancelable | Detail |
|-------|------------|--------|
|
nested-form:before-add | Yes | { wrapper } |
| nested-form:after-add | No | { wrapper } |
| nested-form:before-remove | Yes | { wrapper } |
| nested-form:after-remove | No | { wrapper } |
| nested-form:limit-reached | No | { limit, current } |
| nested-form:minimum-reached | No | { minimum, current } |
| nested-form:before-sort | Yes | { item, oldIndex } |
| nested-form:after-sort | No | { item, oldIndex, newIndex } |
| nested-form:before-duplicate | Yes | { source } |
| nested-form:after-duplicate | No | { source, clone } |$3
`javascript
document.addEventListener("nested-form:after-add", (event) => {
console.log("Added:", event.detail.wrapper)
})document.addEventListener("nested-form:before-remove", (event) => {
if (!confirm("Are you sure?")) {
event.preventDefault()
}
})
``For Rails users, we recommend using the hotwire_nested_form gem which provides view helpers.
MIT