Search code examples
ruby-on-railsruby-on-rails-5nested-attributesfields-for

Model with nested attributes doesn't show fields_for in field that is not an attribute in the model


I have an Exam and an ExamBattery that is just a collection of Exams. They have a has_and_belong_to_many declaration for each other, and ExamBattery accepts nested attributes for Exam, like so:

class Exam < ApplicationRecord
  has_and_belongs_to_many :exam_batteries
  validates_presence_of :name
end

class ExamBattery < ApplicationRecord
  has_and_belongs_to_many :exams
  accepts_nested_attributes_for :exams, reject_if: lambda { |attrs| attrs['name'].blank? }
  validates_presence_of :name
end

When I create a new Exam, I want to be able to assign it to one or many ExamBatteries, so in ExamsController I whitelisted the array exam_battery_ids to accept multiple ExamBatteries to assign them to the current Exam (no other change was made, the controller is just from the scaffold):

def exam_params
  params.require(:exam).permit(:name, :description, :order, :price, exam_battery_ids: [])
end

Also, in the view exams/new I added a multiple select to send the desired exam_battery_ids as params:

 <%= form_with(model: exam, local: true) do |form| %>
   # ... typical scaffold code
   <div class="field">
     <% selected = exam.exam_batteries.collect { |eb| eb.id } %>
     <%= form.label :exam_battery_ids, 'Add batteries:' %>
     <%= form.select :exam_battery_ids,
                options_from_collection_for_select(ExamBattery.all, :id, :name, selected),
                { prompt: 'None' },
                multiple: true %>
   </div>
 <% end %>

The idea is to be able to create a new ExamBattery with new Exams in it, in the same form (I haven't wrote that part yet, I can only edit for now). Also, when I edit an ExamBattery I want to be able to edit its Exams and even assign them to other ExamBatteries (if I select 'None', or JUST another exam battery, it would stop being assigned to the current ExamBattery), so in exam_batteries/edit (actually, the form partial in it) I have this code:

<%= form_with(model: exam_battery, local: true) do |form| %>
  # ... normal scaffold code
  <div class="field">
    <!-- it should be exam_battery[exams_attributes][#_of_field][order] -->
    <!-- it is exam_battery[exam_battery_ids][] -->
    <% selected = exam_battery.exams.map { |exam| exam.id } %>
    <%= form.label :exam_battery_ids, 'Edit batteries:' %>
    <%= form.select :exam_battery_ids,
                    options_from_collection_for_select(ExamBattery.all, :id, :name, selected),
                    { prompt: 'None' },
                    multiple: true %>
  </div>
<% end %>

And in ExamBatteriesController I whitelisted the exam_batteries_attributes, with exam_battery_ids: [] as a param:

params.require(:exam_battery).permit(:name, :certification, exams_attributes: [:name, :description, :order, :price, exam_battery_ids: []])

But when in the ExamBattery form I try to edit the Exam's exam_batteries, the info doesn't update, because the params are like this:

Parameters: {"utf8"=>"✓", "authenticity_token"=>"blah", "exam_battery"=>{"name"=>"Battery1", "certification"=>"test1", "exams_attributes"=>{"0"=>{"name"=>"Exam1", "description"=>"", "order"=>"", "id"=>"3"}, "1"=>{"name"=>"Exam2", "description"=>"", "order"=>"", "id"=>"4"}, "2"=>{"name"=>"Exam3", "description"=>"", "order"=>"", "id"=>"5"}}, "exam_battery_ids"=>["", "", "", "", "", "3"]}, "commit"=>"Update Exam battery", "id"=>"3"}

The exam_battery_ids are sent as a different param because the select name is exam_battery[exam_battery_ids][] instead of something like exam_battery[exams_attributes][0][name], as it happens with the other fields. How can I fix that?

Thanks.


Solution

  • I had an error in the form. In exam_batteries/edit I didn't notice I was using the form_with variable (form) and not the fields_for variable (builder), so it should be like this:

    <div class="field">
      <!-- it should be exam_battery[exams_attributes][0][order] -->
      <!-- it is exam_battery[exam_battery_ids][] -->
      <% selected = exam_battery.exams.map { |exam| exam.id } %>
      <%= builder.label :exam_battery_ids, 'Escoge una batería' %>
      <%= builder.select :exam_battery_ids,
                      options_from_collection_for_select(ExamBattery.all, :id, :name, selected),
                      {
                        include_hidden: false,
                        prompt: 'Ninguna'
                      },
                      multiple: true %>
    </div>
    

    With that it should work. The only issue now is that I can't get the selected batteries when I show them in the fields_for, but I'm working on it.

    UPDATE: I can show the current exam_batteries of the exam in the nested form by replacing the selected variable in the view with this:

    <% selected = exam_battery.exams[builder.options[:child_index]].exam_batteries.map { |eb| eb.id } %>
    

    If you know about a cleaner method, please let me know.