Nested forms with older versions of Rails involved installing gems and enduring
a minor pain in the ass to get everything working, but thanks to StimulusJS and
the following approach of using HTML <template>
tags (found on Drifting
Ruby)
it’s now much easier and cleaner to add nested forms to a
has_and_belongs_to_many
associated model.
# models/post.rb
class Post < ApplicationRecord
has_many :categories, dependent: :destroy
accepts_nested_attributes_for :categories, allow_destroy: true, reject_if: proc { |attr| attr['label'].blank? }
end
# models/category.rb
class Category < ApplicationRecord
belongs_to :post
end
# posts_controller.rb
def post_params
params.require(:post).permit(
:name,
categories_attributes: [
:_destroy,
:id,
:name
]
)
end
// app/javascript/controllers/posts_form_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["add_item", "template"]
connect() {
}
add_association() {
event.preventDefault()
var content = this.templateTarget.innerHTML.replace(/TEMPLATE_RECORD/g, new Date().valueOf())
event.target.insertAdjacentHTML('beforebegin', content)
}
remove_association() {
event.preventDefault()
let item = event.target.closest("article")
item.querySelector("input[name*='_destroy']").value = 1
item.style.display = 'none'
}
}
# posts/_form.html.erb
<div data-controller="posts-form">
<template data-posts-form-target='template'>
<article>
<%= form.fields_for :categories, Category.new, child_index: 'TEMPLATE_RECORD' do | category | %>
<%= render 'categories_fields', form: category %>
<% end %>
</article>
</template>
<article>
<%= form.fields_for :categories do | category | %>
<%= render 'categories_fields', form: category %>
<% end %>
</article>
<%= link_to "+ Add Category",
"#",
role: "button",
data: { action: "posts-form#add_association" }
%>
</div>
# posts_categories_fields.html.erb
<%= form.label :name %>
<%= form.text_field :name %>
<p>
<%= link_to "Remove",
"#",
role: "button",
class: "outline",
data: { action: "click->posts-form#remove_association" }
%>
</p>