Search code examples
ruby-on-railsrubysimple-formsimple-form-for

Params hash from a SimpleForm with multiple records has a different structure for one record than for two


I struggled to digest this into a title.

I'm using SimpleForm to construct a bulk-edit page with one or more fieldsets - one for each record in a collection of ActiveRecord models that have been built but not yet saved.

My form looks like this:

= simple_form_for :courses, method: :patch do |f|
  - @courses.each do |course|
    = field_set_tag do
      = f.simple_fields_for 'courses[]', course do |c|
        = c.input :title
        .row
          .medium-6.columns
            = c.input :start_date, as: :string, input_html: { class: 'input-datepicker' }
          .medium-6.columns
            = c.input :end_date, as: :string, input_html: { class: 'input-datepicker' }
  = f.submit 'Save', class: 'primary button'

The params hash for one record looks like this:

"courses"=>{"courses"=>[{"title"=>"Course Y", "start_date"=>"2017-09-26", "end_date"=>"2017-07-31"}]}

with an array, while for two records it looks like this:

"courses"=>{"courses"=>{"1"=>{"title"=>"Course X", "start_date"=>"2018-01-16", "end_date"=>"2018-07-30"}, "2"=>{"title"=>"Course Y", "start_date"=>"2017-09-26", "end_date"=>"2018-07-30"}}}

with a stringy-integer-keyed hash.

This becomes a problem when I try and use strong parameters. After much hacking, I ended up with this piece of code, which works for multiple records but fails when only one is submitted:

ActionController::Parameters
  .new(courses: params[:courses][:courses].values)
  .permit(courses: [:title, :start_date, :end_date])
  .require(:courses)

It fails with param is missing or the value is empty: courses highlighting the .require(:courses) line above.

The problem is "solved" by harmonising the single-record case with the multiple-record case:

if params[:courses][:courses].is_a?(Array)
  params[:courses][:courses] = { '1': params[:courses][:courses][0] }
end

but it feels like there should be a simpler way of doing it.

Is there a better way to write the form for this use-case? Am I missing a trick with strong parameters?

I'm using rails 5.0.5 and simple_form 3.5.0.


Solution

  • It turns out that the f.simple_fields_for 'courses[]' ... method only gives that fieldset an ID if the form is populated by an existing record, and the params structure of a string ID mapping to a course hash is only used in this case. For "fresh" records, there is no ID and the course hashes are placed in a plain array.

    This bit of code was running in the context of "rolling over" courses from one year to another - copying a previous course and changing the dates. This meant that each fieldset had the ID of the original course.

    When the form was submitted, a new record was created and validated with the new attributes, and it was this fresh record with no ID that repopulated the form. The "it only happens when one course is submitted" thing was a red herring - a product of the test scenario.

    So worth noting: f.simple_fields_for 'courses[]' ... creates an array for new records and a hash mapping IDs to attributes for existing records.