24 August 2020
This is a techy post as I’m going to start sharing some of the day to day things that I uncover while working on client projects in the hope that it helps other developers.
This is much easier that writing full series on complete project builds like I have in the past!
In a recent client project I wanted to create a form which accepts_nested_attributes_for
a model association BUT I wanted to display ALL of the available options to the user which are pre-existing records themselves.
Let me explain with the actual example.
In this Rails project, I have material_summaries
that have_many :materials
.
I also have summary_prices
that belong_to :material_summary
and which also has_many :prices
which match to materials!
In my form for a new or existing summary_price
, I wanted to pre-populate a list of potentially available prices
for the summary_price
by referencing the already saved materials
.
(Let me know if this could be explained better in the comments)
The traditional way that was explained very nicely in this (RailsCast episode)[] is where the user adds individual groups of form fields for each new association using Rail’s fields_for
form helper BUT I wanted to show the user ALL of the available options at once (which we know from the list of created materials
)
Using Rail’s fields_for
and the new_record?
method, I was able to create the following solution. This is a simplified version of what I actually created and I’d recommend using helpers to tidy up the logic:
<%= form_with(model: summary_price], local: true) do |form| %>
<#% fields for the parent summary_price can go here %>
<table class='table'>
<thead>
<tr>
<th>Name</th>
<th>Start Date</th>
<th>Status</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<% @material_summary.materials.each do |material| %>
<% if summary_price.new_record? %>
<% form_object = form.object.send(:prices).klass.new(material_id: material.id, start_date: Date.today.at_beginning_of_month.next_month) %>
<% else %>
<% form_object = form.object.prices.find_by(material_id: material.id).present? ? form.object.prices.find_by(material_id: material.id) : form.object.send(:prices).klass.new(material_id: material.id, start_date: Date.today.at_beginning_of_month.next_month) %>
<% end %>
<%= form.fields_for(:prices, form_object, child_index: form_object.id.present? ? form_object.id : form_object.object_id) do |builder| %>
<tr>
<%= builder.hidden_field :material_id, value: builder.object.material_id %>
<td>
<%= material.combined_name %>
</td>
<td>
<%= builder.date_field :start_date, class: 'form-control' %>
</td>
<td>
<%= builder.select :status, options_for_select(Price.statuses.map { |k, _v| [k.humanize.capitalize, k] }), {}, class: 'form-control' %>
</td>
<td>
<%= builder.number_field :amount, class: 'form-control', min: 0, step: 0.01, required: true %>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
<%= form.submit class: 'btn btn-brand btn-upper btn-bold mt-3' %>
<% end %>
For each material
in the @material_summary
that is being referenced, we’re creating a new form_object
that either uses default data for a new price
record OR we’re finding the associated existing price
record and using the data from that record for the form.
This solution does make a new DB call with each iteration of the .each
method, so be careful if you’re using this solution for very large amounts of data and where performance is crucial.
Have any questions? Let me know in the comments!