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?
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:
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.