Rails offers the possibility to create nested forms. By default, this is a static process. It turns that we can add an associated instance field to the form “on the fly”, with just 4 lines of Vanilla Javascript, without jQuery nor any JS library nor advanced Ruby stuff.
TL;TR
In the view rendering the nested form, we inject (with Javascript) modified copies of the associated input field. We previously passed an array of the desired attributes in the 'strong params' and declared the association with accepts_nested_attributes_for so we are able to submit this form and commit to the database.
Setup of the example
We setup a minimalistic example. Take two models Author and Book with respective attributes ‘name’ and ‘title’ of type string. Suppose we have a one-to-many association, where every book is associated with at most one author. We run rails g model Author name  and rails g model Book title author:references. We tweak our models to:
#author.rb                                  #book.rb
Class Author < ActiveRecord                 Class Book
  has_many :books                             belongs_to :author
  accepts_nested_attributes_for :books      end 
end
The controller should be (the ‘index’ method here is for quick rendering):
#/controllers/authors_controller.rb
class AuthorsController < ApplicationController
  def new
    @author = Author.new;  @author.books.build
  end
  def create
    author = Author.create(author_params);   
    redirect_to authors_path
  end
  def index
   render json: Author.all.order(‘create_at DESC’).includes(:books)
               .to_json(only:[:name],include:[books:{only: :title}])
  end
  def author_params
    params.require(:author).permit(:name, books_attributes:[:title])
  end
end
The authors#new action uses the collection.build() method that comes with the has_many method. It will instantiate a nested new Book object that is linked to the Author object through a foreign key. We pass an array of nested attributes for books in the sanitization method author_params, meaning that we can pass several books to an author. 
To be ready, set the routes: 
resources :authors, only: [:index,:new,:create].
The nested form
We will use the gem Simple_Form to generate the form. We build a nested form view served by the controller’s action authors#new. It will display two inputs, one for the author’s name and one for the book’s title.
#views/authors/new.html.erb
<%= simple_form_for @author do |f| %>
  <%= f.input :name %>
  <%= f.simple_fields_for :books do |b| %>
    <div id=”fieldsetContainer”>
      <fieldset id="0">
        <%= b.input :title %>
      <% end%>
      </fieldset>
    </div>
  <%= f.button :submit %>
<% end%>
Note that we have wrapped the input into fieldset and div tags to help us with the Javascript part for selecting and inserting code as we will see later. We can also build a wrapper, <%= b.input :title, wrapper: :dynamic %> in the Simple Form initializer to further clean the code (see note at the end).
Array of nested attributes
Here is an illustration of how collection.build and fields_for statically renders an array of the nested attributes. Change the 'authors#new' to:
# authors_controller
def new
  @author = Author.new;  @author.books.build;  @author.books.build
end
The 'new.html.erb' view is generated by Rails and the form_for will use the new @author object, and the fields_for will automagically render the array of the two associated objects @books. If we inspect with the browser's devtools: we have two inputs for the books, and the naming will be  author[books_attributes][i][title] where i is the index of the associated field (cf Rails params naming convention). This index has to be unique for each input. On submit, the Rails logs will display params in the form:
{author : {
  name: “John”,
  books_attributes : [
    “0”: {title: “RoR is great”},
    “1”: {title: “Javascript is cool”}
 ]}}
Javascript snippet
Firstly, add a button after the form in the ‘authors/new’ view to trigger the insertion of a new field input for a book title. Add for example:
<button id=”addBook”>Add more books</button>
Then the Javascript code. It copies the fragment which contains the input field we want to replicate, and replaces the correct index and injects it into the DOM. With the strong params set, we are able to commit to the database.
We locate the following code in, for example, the file javascript/packs/addBook.js:
const addBook = ()=> {
  const createButton = document.getElementById(“addBook”);
  createButton.addEventListener(“click”, () => {
    const lastId = document.querySelector(‘#fieldsetContainer’).lastElementChild.id;
    const newId = parseInt(lastId, 10) + 1;
    const newFieldset = document.querySelector('[id=”0"]').outerHTML.replace(/0/g,newId);
    document.querySelector(“#fieldsetContainer”).insertAdjacentHTML(
        “beforeend”, newFieldset
    );
  });
}
export { addBook }
We attach an event listener on the newly added button and:
- find the last fieldset tag within the div '#fieldsetContainer' with the lastElementChild method
- read the id: it contains the last index
- increment it: we chose to simply increment this index to get a unique one
- copy the HTML fragment, serializes it with outerHTML, and reindex it with a regex to replace all the “0”s with the new index
- and inject back into the DOM. Et voilà!
Note 1: Turbolinks
With Rails 6, We want to wait for Turbolinks to be loaded on the page to use our method. In the file javascript/packs/application.js, we add:
import { addBook } from ‘addBook’
document addEventListener(‘turbolinks:load’, ()=> {
  if (document.querySelector(‘#fieldsetContainer’)) {
    addBook()
  }
})
and we add defer: true in the file /views/layouts/application.html.erb.
<%= javascript_pack_tag ‘application’, ‘data-turbolinks-track’: ‘reload’, defer: true %>
This simple method can be easily adapted for more complex forms. The most tricky part is probably the regex part, to change some indexes “0” to the desired value at the proper location, depending on the form.
Note 2: Simple Form wrapper
Last tweak, we can create a custom Simple Form input wrapper to reuse this easily.
#/config/initializers/simple_form_bootstrap.rb
config.wrappers :dynamic, tag: 'div', class: 'form-group' do |b|
  b.use :html5
  b.wrapper tag: 'div', html: {id: "fieldsetContainer"} do |d|
    d.wrapper tag: 'fieldset', html: { id: "0"} do |f|
      f.wrapper tag: 'div', class: "form-group" do |dd|
        dd.use :label
        dd.use :input, class: "form-control"
      end
    end
  end
end
so that we can simplify the form:
<%= simple_form_for @author do |f| %>
  <%= f.input :name %>
  <%= f.simple_fields_for :books do |b| %>
    <%= b.input :title, wrapper: :dynamic %>
  <% end%>
  <%= f.button :submit %>
<% end%>
Note 3: Delete
If we want to add a delete button to eachbook.titleinput, we can add a button:
 <%= content_tag('a', 'DEL', id: "del-"+b.index.to_s, class:"badge badge-danger align-self-center")  %>
and to do something like:
function removeField() {
  document.body.addEventListener("click", (e) => {
    if ( e.target.nodeName === "A" &&
         e.target.parentNode.previousElementSibling) {
/* to prevent from removing the first fieldset as it's previous sibling is null */
      e.target.parentNode.remove();
    }
  });
}
 

 
    
Top comments (4)
so quick question. maybe I'm missing something obvious, but ive been trying to figure out how to integrate "on the fly" fields for forms on Rails 6 using as little javascript as possible and not breaking the Rails methods for submitting data to the controllers. I like the start you have here, but I am running into an issue with your code.
the section in the controller for Authors_Controller:
def author_params
params.require(:author).permit(:name, books_attributes[:title])
end
throws an error every time because the books_attributes[.....] comes up as an undefined method. I'd really like to find a solution to getting this running as I am trying to develop an app that uses normalized data through 3 models and allows for selecting names from a drop down and the titles of that person from a drop down as well, and then linking them to a team. The form is complex as it also allows for adding new names into the name drop down area. The title types are fixed and cannot be added to. I've seen everything from massive javascript that clones and mutates but then doesnt submit the data to the controller because the ID's are not unique or not within the model. Your method looks simple and if you can help me figure out the broken books_attributes issue that would be great. I've tried several different solutions to no avail.
The model used here is very simple, a one-to-many with Author 1<n Book. Make sure the Author model
accepts_nested_attributes_for :booksso you can pass an array ofbooks_attributes(titleis a field of the modelBook). Maybe have a look at this: medium.com/@nevendrean/rails-forms... Good luckand this repo github.com/ndrean/Dynamic-Ajax-forms (it's just a toy example for me playing with forms)
Just change the line in author_params
params.require(:author).permit(:name, books_attributes: [:title])
Thanks!