Rails & Stimulus for dynamic nested attribute forms

1.6k Views Asked by At

there, I'm coming back to Rails after years of only using it for APIs. There all sorts of new things and I'm trying to figure out how to accomplish some stuff with the new frameworks. One example is creating a form with accept_nested_attributes and adding dynamic has_many associations.

I have Company model and Partner models.

class Company < ApplicationRecord
  # ... name, size, registration_type, etc
  has_many :partners, dependent: :destroy
  accepted_nested_attributes_for :partners
end

class Partner < ApplicationRecord
  # ... name, email, phone
  belong_to :company
end

In my form I have:

<%= form_with(model: company) do |form} %>
  <!-- ... -->

  <div class="hidden" data-company-form-target="partnersForm">
    <%= form.fields_for :partners, company.partners do |partner_form| %>
      <div data-company-form-target="partnerFormInner">
        <div class="form-group">
          <div>
            <%= partner_form.label :name, "Name" %>
            <%= partner_form.text_field :name %>
          </div>

          <div>
            <%= partner_form.label :email, "Email" %>
            <%= partner_form.text_field :email %>
          </div>

          <div>
            <%= partner_form.label :phone_number, "Phone Number" %>
            <%= partner_form.text_field :phone_number %>
          </div>
        </div>
      </div>
    <% end %>
    <div>
      <button data-action="click->company-form#addPartner">
        +
      </button>
    </div>
  </div>
<% end %>

I have a js controller that uses the button to add a new line to the form to add more partners:

import { Controller } from "stimulus";

export default class extends Controller {
  static targets = [ "partnerFormInner" ]

  addPartner(e) {
    e.preventDefault(); e.stopPropagation();

    this.partnerFormInnerTarget.insertAdjacentHTML('beforeend', this.formTemplate)

    this.count++
  }

  connect() {
    this.count = 1
  }

  get formTemplate() {
    return `<div>
    <div>
      <label for="company_partners_attributes_${this.count}_name">Name</label>
      <input type="text" name="company[partners_attributes][${this.count}][name]" id="company_partners_attributes_${this.count}_name">
    </div>

    <div>
      <label for="company_partners_attributes_${this.count}_email">Email</label>
      <input type="text" email="company[partners_attributes][${this.count}][email]" id="company_partners_attributes_${this.count}_email">
    </div>

    <div>
      <label for="company_partners_attributes_${this.count}_phone_number">Phone Number</label>
      <input type="text" phone_number="company[partners_attributes][${this.count}][phone_number]" id="company_partners_attributes_${this.count}_phone_number">
    </div>
  </div>`
  }
}

Now this all works fine, however I feel like the js controller is a little hacky, copying the HTML output from a single partner and pasting it into my controller w/ some interpolation... I feel like there's probably some way for me to get that template directly from the Rails backend so that I have it all defined in one place in a partial or something, but I'm not sure how to connect those dots.

Is there a way to move the partner form to a partial and dynamically pull code for the next line in the form and insert it via JS, or do I need to just keep doing what I'm doing with the copy/paste?

2

There are 2 best solutions below

0
On

In your stimulus controller, instead of copying the HTML, you can use regex to replace the input string and use the current time to identify the added field. For simplicity:

this.partnerFormInnerTarget.innerHTML.replace(/RECORD_PATTERN/g, new Date().getTime())

You can use or read this gem. https://github.com/hungle00/rondo_form
It's a simple gem I created to handle dynamic nested form with Stimulus JS.

0
On

you need to use the Stimulus Rails Nested Form, this helps you to create new fields on the fly to populate your Rails relationship with accepts_nested_attributes_for. user bin/rails pin stimulus-rails-nested-form

add the extended controller

import NestedForm from 'stimulus-rails-nested-form'
    export default class extends NestedForm {
      connect() {
        super.connect()
        console.log('Do what you want here.')
      }
    }

read more about the stimulus nested form