How to add new nested fields to a form using Rails fields_for with child_index and minimal javascript.
Recently I worked on a task where a user needs to add arbitrary number of images to imageshelf.
Existing models were setup as follows:
class Imageshelf < ActiveRecord::Base
has_many :images
accepts_nested_attributes_for :images
validates_presence_of :images
end
class Image < ActiveRecord::Base
end
Existing views were setup as follows:
app/views/imageshelves/_image.html.erb
<div class="js-form">
<%= image_form.label :image_url %>
<%= image_form.text_field :image_url %>
</div>
app/views/imageshelves/_new.html.erb
<%= form_for @imageshelf, url: /imageshelf_endpoint do |f| %>
<%= f.label :heading, "Imageshelf Heading" %>
<%= f.text_field :heading %>
<div id="js-images-container">
<%= f.fields_for :images do |image_form| %>
<%= render "image", image_form: image_form %>
<% end %>
</div>
<%= f.submit "Save Imageshelf" %>
<% end %>
It turns out, Rails 5 and beyond does not have native support for what I needed.
In addition, maintaining form markup inside javascript code was something I wanted to avoid.
I ultimately implemented following:
app/views/imageshelves/_new.html.erb
<%= form_for @imageshelf, url: /imageshelf_endpoint do |f| %>
<%= f.label :heading, "Imageshelf Heading" %>
<%= f.text_field :heading %>
<div id="js-images-container">
<%= f.fields_for :images do |image_form| %>
<%= render "image", image_form: image_form %>
<% end %>
</div>
<% content_for :add_image do %>
<div class="js-add-image-form is-hidden">
<%= f.fields_for :images,
Image.new,
child_index: "_tmp_index_" do |add_image| %>
<%= render "image", image_form: add_image %>
<% end %>
</div>
<% end %>
<%= button_tag "Add Image", id: "js-add-button" %>
<%= f.submit "Save Imageshelf" %>
<% end %>
<%= yield :add_image %>
When generating a form on initial render, I am using content_for helper to “capture” image form markup.
In addition, I am using child_index with “tmp_index” as placeholder to be later replaced on client side.
With views in place now is the time for javascript:
app/webpack/application.js
const addImageInputFields = () => {
const visibleImageFormElements = document.querySelectorAll('#js-images-container > div.js-form')
const nextIndex = visibleImageFormElements.length
const hiddenImageFormElement = document.querySelector('div.js-add-form.is-hidden')
const hiddenImageFormElementClone = hiddenImageFormElement.cloneNode(true)
const formHtmlMarkup = hiddenImageFormElementClone.innerHTML.replace(/_tmp_index_/g, `${nextIndex}`)
const lastVisibleFormElement = visibleImageFormElements[nextIndex - 1]
lastVisibleFormElement.insertAdjacentHTML('afterend', formHtmlMarkup)
}
Having addImageInputFields execute when user clicks “Add Image” button, the method will find “hidden” form element generated by Rails, clone it (without cloneNode the “hidden” form elements would be removed from the DOM), replace tmp_index placeholder using value of nextIndex and append to existing form.
Summary
- Use fields_for helper method to generate nested form fields.
- Rely on child_index as index placeholder, later to be replaced on client side.
- Partial “_image.html.erb” will serve as single source of nested form fields.
- Javascript is only responsible to fetch hidden element, replace index and append to form.
Thanks for reading!