Nested Forms from Scratch

Episode #70 by Teacher's Avatar David Kimura

Summary

Learn how to handle multiple models in a single form with accepts_nested_attributes_for and learn how to add and remove nested records through JavaScript.
rails form javascript 9:48

Resources

Summary

# Terminal
rails g scaffold todo_list name
rails g model task todo_list:references name completed:boolean due:date

# models/todo_list.rb
class TodoList < ApplicationRecord
  has_many :tasks, dependent: :destroy
  accepts_nested_attributes_for :tasks, allow_destroy: true, reject_if: proc { |att| att['name'].blank? }
  # accepts_nested_attributes_for :tasks, allow_destroy: true, reject_if: :all_blank
end

# models/task.rb
class Task < ApplicationRecord
  belongs_to :todo_list, optional: true
end

# helpers/application_helper.rb
module ApplicationHelper
  def link_to_add_row(name, f, association, **args)
    new_object = f.object.send(association).klass.new
    id = new_object.object_id
    fields = f.simple_fields_for(association, new_object, child_index: id) do |builder|
      render(association.to_s.singularize, f: builder)
    end
    link_to(name, '#', class: "add_fields " + args[:class], data: {id: id, fields: fields.gsub("\n", "")})
  end
end

# application.js
$(document).on('turbolinks:load', function() {

  $('form').on('click', '.remove_record', function(event) {
    $(this).prev('input[type=hidden]').val('1');
    $(this).closest('tr').hide();
    return event.preventDefault();
  });

  $('form').on('click', '.add_fields', function(event) {
    var regexp, time;
    time = new Date().getTime();
    regexp = new RegExp($(this).data('id'), 'g');
    $('.fields').append($(this).data('fields').replace(regexp, time));
    return event.preventDefault();
  });
  
});

# _form.html.erb
<%= simple_form_for(@todo_list) do |f| %>
  <%= f.error_notification %>
  <div class="form-inputs">
    <%= f.input :name %>
  </div>

  <table class='table'>
    <thead>
      <tr>
        <th></th>
        <th>Task Name</th>
        <th>Completed</th>
        <th>Due</th>
      </tr>
    </thead>
    <tbody class='fields'>
      <%= f.simple_fields_for :tasks do |builder| %>
        <%= render 'task', f: builder %>
      <% end %>
    </tbody>
  </table>

  <div class="form-actions">
    <%= f.button :submit %>
    <%= link_to_add_row('Add Task', f, :tasks, class: 'btn btn-primary') %>
  </div>
<% end %>

# _task.html.erb
<tr>
  <td>
    <%= f.input_field :_destroy, as: :hidden %>
    <%= link_to 'Delete', '#', class: 'remove_record' %>
  </td>
  <td><%= f.input :name, label: false %></td>
  <td><%= f.input :completed, label: false %></td>
  <td><%= f.input :due, label: false, as: :string %></td>
</tr>