0

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?

Brad Herman
  • 9,665
  • 7
  • 28
  • 30
  • If u wanna use a partial I think you'll need to make an ajax request to fetch it, no? Maybe start with vanilla js and see how it looks, later u can rewrite it in Stimulus – Joel Blum Jul 22 '21 at 20:14
  • Some ideas to a similar problem https://discuss.hotwired.dev/t/fetching-a-partial-on-click/1297/5 – Joel Blum Jul 22 '21 at 20:15
  • I would use Turbo to your advantage here. Instead of having the + button trigger Stimulus, you can send a GET request with format: :turbo_stream to a Rails controller and retrieve a partial to add to the DOM, just like you said. You would have a special method in your model's controller which responds to turbo requests and does something like `render turbo_stream: turbo_stream.append("#dom_id_to_append_to", partial: 'partner_form', locals: {partner: Partner.new})`. The idea is that each turbo response appends a form to the page. see: https://turbo.hotwired.dev/handbook/streams – aidan Aug 05 '21 at 18:36

1 Answers1

0

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.

hùng lê
  • 1
  • 2