Ruby on Rails

Stimulus components nested form

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

← All posts Recent blog posts →