Dynamic Nested Forms

Public
A helper and Stimulus controller to make building nested forms easier.
Icons/chart bar
Used 15 times
Created by
B Brian Weaver

Usage
A helper and Stimulus controller to make building nested forms easier.

Full credit to user Alex on Stack Overflow for his incredibly thorough answer that includes this solution. https://stackoverflow.com/a/71715794

After running the template, use the helper in your forms like this.

<%= form_with model: foo do |form| %>
  <%= dynamic_fields_for form, :bars do |bars_form| %>
    # NOTE: this block will be rendered once for the <template> and once for every `bar`
    <%= tag.div do %>
      <%= bars_form.text_field :name %>
      <%= bars_form.check_box :_destroy, title: "Check to delete" %>
    <% end %>

    # NOTE: double nested dynamic fields also work
    <%# <%= dynamic_fields_for ff, :things do |things_form| %>
    <%#   <%= things_form.text_field :name %>
    <%# <% end %>
  <% end %>
<% end %>

Run this command in your Rails app directory in the terminal:

rails app:template LOCATION="https://railsbytes.com/script/x9Qs78"
Template Source

Review the code before running this template on your machine.

file "app/javascript/controllers/dynamic_fields_controller.js", <<-CODE
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["template"]

  add(event) {
    event.preventDefault()
    event.currentTarget.insertAdjacentHTML(
      "beforebegin",
      this.templateTarget.innerHTML.replace(
        /__CHILD_INDEX__/g,
        new Date().getTime().toString()
      )
    )
  }
}
CODE

inject_into_file "app/helpers/application_helper.rb", after: "ApplicationHelper" do
  <<-CODE

  def dynamic_fields_for(f, association, button_text = "Add")
    # stimulus:      controller v
    tag.div data: {controller: "dynamic-fields"} do
      safe_join([
        # render existing fields
        f.fields_for(association) do |ff|
          yield ff
        end,

        # render "Add" button that will call `add()` function
        # stimulus:         `add(event)` v
        button_tag(button_text, data: {action: "dynamic-fields#add"}),

        # render "<template>"
        # stimulus:           `this.templateTarget` v
        tag.template(data: {dynamic_fields_target: "template"}) do
          f.fields_for association, association.to_s.classify.constantize.new,
            child_index: "__CHILD_INDEX__" do |ff|
              #           ^ make it easy to gsub from javascript
              yield ff
          end
        end
      ])
    end
  end
  CODE
end
Comments

Sign up or Login to leave a comment.