Using nested forms in Rails we may have a need to create additional nested fields on the fly inside the main form.
To achieve this result, we can use Rails Nested Form, a Stimulus controller from the Stimulus Components collection.
1. Installing the library
Install the library using importmap
with this command:
$ bin/importmap pin @stimulus-components/rails-nested-form
> Pinning "@stimulus-components/rails-nested-form" to
> vendor/javascript/@stimulus-components/rails-nested-form.js
> via download from https://ga.jspm.io/npm:@stimulus-components/[email protected]/dist/stimulus-rails-nested-form.mjs
> Pinning "@hotwired/stimulus" to vendor/javascript/@hotwired/stimulus.js
> via download from https://ga.jspm.io/npm:@hotwired/[email protected]/dist/stimulus.js
This action changes config/importmap.rb
like so:
diff --git a/config/importmap.rb b/config/importmap.rb
-pin "@hotwired/stimulus", to: "stimulus.min.js"
+pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2
+pin "@stimulus-components/rails-nested-form", to: "@stimulus-components--rails-nested-form.js" # @5.0.0
It also adds the following files to vendor/javascript
:
@hotwired--stimulus.js
@stimulus-components--rails-nested-form.js
2. Generate the Stimulus controller
$ bin/rails generate stimulus nested_form
This will generate app/javascripts/controller/nested_form_controller.js
Remove all the generated code in the file and add the following code, copied from the
documentation
under Extending Controller
.
// app/javascripts/controller/nested_form_controller.js
import NestedForm from "@stimulus-components/rails-nested-form"
export default class extends NestedForm {
connect() {
super.connect()
console.log("Do what you want here.")
}
}
3. Update the form
In the form, add some data
attributes required by the nested form controller:
<!-- app/views/pathways/_form.html.erb -->
<%= form_with model: pathway,
data: {
controller: "nested-form",
nested_form_wrapper_selector_value: '.nested-form-wrapper'
} do |form| %>
If we load the page with the form at this point
http://localhost:3000/pathways/new
we should see in the browser console the output of the
console.log
command that we put in the nested form controller. This confirms
the controller is accessed correctly.
// app/javascripts/controller/nested_form_controller.js
...
console.log("Do what you want here.")
...
Inside the form, add a <template>
element that wraps the nested fields. Adjust
the template to add the new object to add Step.new
, and a child_index
attribute with the value of NEW_RECORD
.
Inside the nested fields, render a Rails partial with the actual fields needed, shown below.
<template data-nested-form-target="template">
<%= form.fields_for :steps, Step.new, child_index: 'NEW_RECORD' do |step_fields| %>
<%= render "step_form", step_fields: step_fields, employer: employer %>
<% end %>
</template>
Immediately below the template
element, add the regular nested fields,
rendering the same Rails partial, like so:
<%= form.fields_for :steps do |step_fields| %>
<%= render "step_form", step_fields: step_fields, employer: employer %>
<% end %>
Below this code, add a div
that will be used by Nested Forms to inject the
steps:
<!-- Inserted elements will be injected before that target. -->
<div data-nested-form-target="target"></div>
Finally, add a button to add the steps
<button type="button" data-action="nested-form#add">Add step</button>
The Rails partial that’s rendered when we add a step looks like this:
<!-- app/views/pathways/_step_form.html.erb -->
<div class="nested-form-wrapper mb-8 border border-stone-300 p-4 flex flex-row gap-4"
data-new-record="<%= step_fields.object.new_record? %>">
<p class="my-4">
<%= step_fields.label :place, "Step", class: "block font-bold mb-2 uppercase text-xs" %>
<%= step_fields.select :place, options_for_select(1..10, step_fields.object.place), prompt: "Select" %>
</p>
<p class="my-4">
<%= step_fields.label :position_id, "Position", class: "block font-bold mb-2 uppercase text-xs" %>
<%= step_fields.select :position_id,
options_from_collection_for_select(employer.positions.all, :id, :title, step_fields.object.position_id),
prompt: "Choose Position",
class: "bg-stone-200 rounded-lg border border-stone-500" %>
</p>
<button type="button" data-action="nested-form#remove">Remove step</button>
<%= step_fields.hidden_field :_destroy %>
</div>
It includes a button to remove the step.
It also includes a hidden field that passes the _destroy
param, to remove this
step.
Remember to allow the _destroy
param in the controller:
# app/controllers/pathways_controller.rb
def pathway_params
params.require(:pathway).permit(:name, steps_attributes: [:id, :place, :position_id, :_destroy])
end