Dynamic nested forms with Stimulus *and* has_many :through relationship
Hi team,
I've been following along with the Dynamic Nested Forms with Stimulus JS lesson, but I've been attempting to adapt it for a has_many :through relationship.
So far, I've managed to get the new and create methods working just fine. However, I'm really having trouble with editing. I've posted my code below.
When I edit a Book, I'm getting duplicate records created.
For instance, if I try to edit a Book that already has two Authors, then it adds these two authors plus any extras that I add during the edit. This then compounds and before I know it I have a Book with many, many Authors.  
Anyone got a clue what's happening?
EDIT: Here's a sample app with the code
book.rb
class Book < ApplicationRecord
    has_many :book_authors, inverse_of: :book, dependent: :destroy
    has_many :authors, through: :book_authors
    validates_presence_of :title, :description
    accepts_nested_attributes_for :book_authors, reject_if: :all_blank, allow_destroy: true
    def book_authors_attributes=(book_author_attributes)
    book_author_attributes.values.each do |author_attribute|
      author = Author.find_or_create_by(name:author_attribute["author_attributes"]["name"])
      self.authors << author
    end
  end
end
author.rb
class Author < ApplicationRecord
    has_many :books, through: :book_authors
    has_many :book_authors, dependent: :destroy
end
book_author.rb
class Author < ApplicationRecord
    has_many :books, through: :book_authors
    has_many :book_authors, dependent: :destroy
end
books_controller.rb
class BooksController < ApplicationController
  before_action :set_book, only: [:show, :edit, :update, :destroy]
  def new
    @book = Book.new
    @book.book_authors.build.build_author
  end
  def edit
  end
  def create
    @book = Book.new(book_params)
    respond_to do |format|
      if @book.save
        format.html { redirect_to @book, notice: 'Book was successfully created.' }
        format.json { render :show, status: :created, location: @book }
      else
        format.html { render :new }
        format.json { render json: @book.errors, status: :unprocessable_entity }
      end
    end
  end
  def update
    respond_to do |format|
      if @book.update(book_params)
        format.html { redirect_to @book, notice: 'Book was successfully updated.' }
        format.json { render :show, status: :ok, location: @book }
      else
        format.html { render :edit }
        format.json { render json: @book.errors, status: :unprocessable_entity }
      end
    end
  end
  private
    def set_book
      @book = Book.find(params[:id])
    end
    def book_params
      params.require(:book).permit(:title, :description, book_authors_attributes: [:id, :author_id, :_destroy, author_attributes: [:id, :_destroy, :name]])
    end
end
_form.html.erb
<%= form_for @book do |form| %>
  <% if @book.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@book.errors.count, "error") %> prohibited this book from being saved:</h2>
      <ul>
      <% @book.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>
  <div class="field">
    <%= form.text_field :title %>
  </div>
  <div class="field">
    <%= form.text_field :description %>
  </div>
  <div data-controller="nested-form">
    <template data-target="nested-form.template">
      <%= form.fields_for :book_authors, child_index: "NEW_RECORD" do |book_author| %>
        <%= render "book_author_fields", form: book_author %>
      <% end %>
    </template>
    <%= form.fields_for :book_authors do |book_author| %>
      <%= render "book_author_fields", form: book_author %>
    <% end %>
    <div data-target="nested-form.links">
      <%= link_to "add author", "#", data: { action: "nested-form#add_association" } %>
    </div>
  </div>
  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>
_book_author_fields.html.erb
<%= content_tag :div, class: "nested-fields", data: { new_record: form.object.new_record? } do %>
    <div class="nested-field-input d-flex justtify-content-between mb-2">
        <div class="col-11 pl-0">
            <%= form.fields_for(:author) do |author_form| %>
        <%= author_form.text_field :name %>
      <% end %>
        </div>
        <div class="col-1">
            <%= link_to "delete", "#", data: { action: "nested-form#remove_association" } %>
    </div>
    </div>
    <%= form.hidden_field :_destroy, as: :hidden %>
<% end %>
nested_form_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
  static targets = [ "links", "template" ]
  connect() {
  }
  add_association(event) {
    event.preventDefault()
    var content = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, new Date().getTime())
    this.linksTarget.insertAdjacentHTML('beforebegin', content)
  }
  remove_association(event) {
    event.preventDefault()
    let wrapper = event.target.closest(".nested-fields")
    if(wrapper.dataset.newRecord == "true") {
        wrapper.remove()
    } else {
        wrapper.querySelector("input[name*='_destroy']").value = 1
        wrapper.style.display = "none"
    }
  }
}
Hey Nino,
Editing requires the id of the record to be in the form so it knows which record to edit. You've got _destroy, but not id so it wouldn't know how to delete those either.. You want it as a hidden field and permitted params. 
    <%= form.hidden_field :id %>
Hi Chris,
I put <%= form.hidden_field :id %> into my form, and at first it didn't change anything. However, I commented-out the following lines in book.rb that find or create an Author, and the edit form works a charm, so thanks for your help there.
def book_authors_attributes=(book_author_attributes)
  book_author_attributes.values.each do |author_attribute|
    author = Author.find_or_create_by(name: author_attribute["author_attributes"]["name"])
    self.authors << author
  end
end
My question now is how might I be able to make the above code work? It works great on the new form, but I get duplicates again when I use it for editing. I think the problem lies with the self.authors << author part.
I'm guessing that this line is reinserting all the authors back into the form after I've deleted them and causing the duplicates once more?
Yeah, you were manually replacing part of the functionality that's built-in to Rails there.
I don't see anything in this code for deleting authors, just adding new ones so that's part of the problem.
I wanted to generate a pair of partials. My approach is prerhaps flawed, but I was generating the same "id" for both partials when doing the replace for "NEW_RECORD". The way I managed to get around that was...
(my apologies I don't know how to add code block here...)
content = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, Math.floor(new Date().valueOf() * Math.random())).replace(/NEW_SECOND_RECORD/g, Math.floor(new Date().valueOf() * Math.random()))
Perhaps this is helpful to anyone else that comes across this post.


