Nested Forms from Scratch

#70 Nested Forms from Scratch
3/5/2017

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.
1
rails form javascript

Summary

Terminalrails g scaffold todo_list name
rails g model task todo_list:references name completed:boolean due:date
models/todo_list.rbclass 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.rbclass Task < ApplicationRecord
  belongs_to :todo_list, optional: true
end
helpers/application_helper.rbmodule 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>

Photo
Simon Kiteley said 22 days ago:

I'm really torn to whether this approach gives a good user experience. Its so easy to hit back or refresh the browser and loose what you have entered. I tend to use ajax and save as I go along. Wonder what other think?

Is a great tutorial though :)

635114?v=3&s=64
kobaltz said 22 days ago:

From a UX perspective, it is consistent with how, by default, other forms work. You're able to refresh the browser or hit the back button to navigate away from normal forms without warning and losing any field changes.

My approach is usually to provide some form validations via javascript. So if the user adds a record or if they modify existing fields on the page, set a variable in JS. If the user navigates away from the page before saving, then show them an alert to prevent leaving the page on accident.

Photo
Mike Belyakov said 21 days ago:

Good video!

The line

$(this).prev('input[type=hidden]').val('1');

will work good only of the form have 1 hidden field. So, better to fetch _delete input with other selector

Login to Comment