Multiple records in one form
I would like to create a form that has this format:
Shared Fields
Fields Unique to record A
[add another record button]
If the [add] button can be clicked as many times as necessary to create
Shared Fields
Fields Unique to record A
Fields Unique to record B
Fields Unique to record C
[add another record button]
Submitting the form creates records A, B and C
back side
This creates 3 records, each with its own common and unique attributes according to the fields in the form.
Update
One thing that is shared is each one belonging to the "employer". So this should be form_for @employer?
Thinking out loud, we could keep the shared attributes as attr_accessors on the parent and assign them to the children in the child # create child controller action.
source to share
Since you are filling multiple models in one form without nesting, you cannot simply generate the form with form_for @something
, since you do not have a single object to fill out.
I would look at the database structure to see if there is a way to retrieve the common anto fields for a particular model. But if there is no way to do it cleanly, read on.
The Rails object form rendering helpers lay out fields in such a way that Rails can correctly parse as a hash. Which, in turn, can be used to construct an object. Using this:
form_for @thing do |f|
t.text_field :name
end
You will get a form with a field thing[name]
that looks params
like this:
{..., thing: {name: "Hello world"}, ...}
See the guides for details .
And now here's the catch: you don't have one object to fill. But you can neglect this and build a form that will still be processed as a nested hash. You just have to manually fill in what Rails would otherwise assume: form the URL and object name.
form_for :thing, url: thing_path do |f|
f.text_field :name
end
If you want to create a group of fields, use the fields_for
helper inside this form. More information in the documentation in a form that might look like this:
form_for :thing, url: thing_url do |f|
f.text_field :name
f.fields_for :something do |s|
s.text_field :value
end
end
This should render the field to be processed as a single hash. But you might want an array of nested fields in your case, so see the documentation.
You end up with:
- Hash
- Common parameters
- Array of objects
- Object A
- Object A
- Object B
- ...
And now the final problem - you need to add fields to the form via JavaScript, but he needs to know in advance which label to add. There are many ways to solve this problem: one could submit a rough shape for each unique model and sample it using JavaScript.
Edit: Coincidentally, I needed to figure out how to render a "naked" (not object supported) form that can be copied by JavaScript and will look like an array of objects in parameters when multiple forms are given.
The tricky part: if you use fields_for :something
it will give a shape for a single object, not an array of them. Digging around, I discovered what appears to be an undocumented feature (digging through the code from this post ). It's used like this (I'm using the HAML / Slim syntax for brevity):
= form_for :thing, url: whatever do |f|
= f.fields_for :stuff, index: '' do |s| #<< the `index: ''` part
= s.text_field :v
Semantically, this means the following: create a form containing a field stuff
that is filled with one or many fields with an empty index . Under the hood, it generates a few awkward field names at first glance:
thing[stuff][v] # Before, without ` index: '' `
thing[stuff][][v] # After, see the empty index?
The interesting part of this is that you can just clone the resultig value set without modifying it, and Rails (or even Rack?) Will resolve that form set into a separate object each. It depends on the browser preserving the field order, which is true in most cases.
source to share
In your new.html.erb
<div id="formDiv">
<%= form_for @user do |f| %>
<%= render 'shared/error_messages' %><br>
<%= f.label :name %><br>
<%= f.text_field :name %><br>
<%= f.label :email %><br>
<%= f.email_field :email %><br>
<%= f.label :password %><br>
<%= f.password_field :password %><br>
<%= f.label :password_confirmation, "Confirmation" %><br>
<%= f.password_field :password_confirmation %><br>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
<input id="add" type="submit" value="Add"/>
</div>
<script>
$("#add").click(function() {
$.ajax({
url: "newAjax",
success: function (html) {
$("#formDiv").append(html);
}
});
});
$('form').submit(function(){
$('.multiform').each(function() {
$(this).submit();
});
return true;
});
</script>
This uses AJAX to add a new form every time you click the button, and when you try to submit the form, it loops through other forms with the "multiform" class and submits them first.
In your newAjax.html.erb
<%= form_for @user, remote: true, html: { class: 'multiForm' } do |f| %>
<%= f.label :name %><br>
<%= f.text_field :name %><br>
<%= f.label :email %><br>
<%= f.email_field :email %><br>
<%= f.label :password %><br>
<%= f.password_field :password %><br>
<%= f.label :password_confirmation, "Confirmation" %><br>
<%= f.password_field :password_confirmation %><br>
<% end %>
We use AJAX on these forms so that they can be submitted without redirection. And the last one in your controller
def newAjax
@user = User.new
render :layout => false
end
We want to make sure our layout is not rendered with an AJAX form, so we don't reload JS / CSS files, etc.
source to share
Here is the working code I used at the end. Thanks to everyone who helped. The next method is D-side, but using "Employer" as the base model. Perhaps I could refactor this to remove attr_accessors in Employer and use field tags in the view instead. At the end of the day, I am only pulling the shared values from the "params" array passed to the controller.
Order controller:
def new
@employer = current_employer
@booking = @employer.bookings.build
respond_with(@booking)
end
def create
errors = []
booking_create_params["new_booking_attributes"].each do |booking|
new_booking = current_employer.bookings.build(booking)
new_booking.job_type_id = booking_create_params["job_type_id"]
new_booking.vehicle_id = booking_create_params["vehicle_id"]
new_booking.location_id = booking_create_params["location_id"]
new_booking.pay = booking_create_params["pay"]
unless new_booking.save
errors << new_booking.errors.full_messages
end
end
if errors == []
flash[:notice] = "Bookings posted!"
redirect_to new_booking_path
else
flash[:notice] = "Error: #{errors.join(', ')}!"
redirect_to new_booking_path
end
end
In view:
<div id="bookings">
<ol class="numbers">
<li>
<legend>Location, Pay, & Vehicle</legend>
<div class="form-group">
<div class="row">
<div class="col-sm-6">
<label>Type of job</label><br>
<%= f.select(:job_type_id, options_from_collection_for_select(JobType.all, :id, :name_with_delivery), {}, { id: 'job-type', class: 'form-control' }) %>
</div>
<div class="col-sm-6">
<label>Vehicle needed</label><br>
<%= f.select(:vehicle_id, options_from_collection_for_select(Vehicle.all, :id, :name), {}, { id: 'vehicle-type', class: 'form-control' }) %>
</div>
</div>
</div>
<div class="form-group">
<div class="row">
<div class="col-sm-6">
<label>Location</label>
<% if current_employer.locations.count > 1 %>
<%= f.select :location_id, options_from_collection_for_select(current_employer.locations.all, :id, :name_or_address_1), {}, { class: 'form-control' } %>
<% elsif current_employer.locations.count == 1 %>
<p><strong>Location: </strong><%= current_employer.locations.first.name_or_address_1 %></p>
<%= f.hidden_field :location_id, current_employer.locations.first.id %>
<% end %>
<%= link_to "or add new location", new_employer_location_path(current_employer, Location.new) %>
</div>
<div class="col-sm-6">
<%= f.label :pay %><br>
<%= f.text_field :pay, class: 'form-control' %>
</div>
</div>
</div>
</li>
<legend>Shifts</legend>
<%= render 'booking', booking: Booking.new %>
</ol>
</div>
If partial:
<li class="close-list-item">
<!-- Recommended: post at least 1 week in advance
& shift length at least 4 hours.
-->
<% new_or_existing = booking.new_record? ? 'new' : 'existing' %>
<% prefix = "employer[#{new_or_existing}_booking_attributes][]" %>
<%= fields_for prefix, booking do |booking_form| -%>
<div class="shifts">
<div class="form-group shift">
<div class="row">
<div class="col-sm-4">
<label for="">Date</label>
<i class="glyphicon glyphicon-time"></i>
<%= booking_form.text_field :start, class: 'form-control booking-date', placeholder: 'Date', data: { provide: "datepicker", date_clear_btn: "true", date_autoclose: "true", date_start_date: '+1d', date_format: "yyyy-mm-dd" } %>
</div>
<div class="col-sm-4">
<label>Time Start</label><br>
<%= booking_form.select(:start_time, options_for_select(
booking_times_array
), { include_blank: true }, { class: 'form-control booking-time booking-time-start' } ) %>
</div>
<div class="col-sm-4">
<label>
Time End
</label>
<%= link_to "javascript:;", class: 'pull-right remove-shift' do %>
<i class="glyphicon glyphicon-remove"></i>
<% end %>
<script type="text/javascript">
$(".remove-shift").click(function(){
$(this).parents("li").remove();
});
</script>
<%= booking_form.select(:end_time, options_for_select(
booking_times_array
), { include_blank: true }, { class: 'form-control booking-time booking-time-end' } ) %>
</div>
</div>
</div>
</div>
</li>
<% end -%>
source to share