Integrating an array column with Simple Form in Rails
THE PROBLEM
Gwen and I were working on a project where we migrated an attribute on a model (Course) from a string (email) to an array (emails), and we needed to integrate it into the SimpleForm-built, user-facing form.
We wanted an experience that was more user-friendly than simply a textbox with comma-separated emails; we found a blog post that helped us get started, but in their version, if there weren't already a value for the array attribute, it wouldn't show any input fields when the form loaded.
THE CODE
As the post describes, you can define custom inputs for SimpleForm, saving them in app/inputs.
array_input.rb
class ArrayInput < SimpleForm::Inputs::StringInputdef inputinput_html_options[:type] ||= input_typeArray(object.public_send(attribute_name)).map do |array_el|@builder.text_field(nil, input_html_options.merge(value: array_el, name: "#{object_name}[#{attribute_name}][]"))end.join.html_safeenddef input_type:textendend
Then, integrate the change into your form:
Form view (using Haml)
= f.input :emails, as: :array%button.add-email-button Add additional email
Finally, if you're using strong params, include your attribute and designate it as an array:
Controller
def model_paramsparams.require(:course).permit(:attribute1, :attribute2, emails: [])end
This wasn't detailed in the original post we looked at, but we also added some JavaScript to handle inserting another email field:
CoffeeScript
$(document).ready ->$('.add-email-button').on 'click', @addEmailFieldaddEmailField: (ev) ->ev.preventDefault()$lastEmailField = $('input[name="course[emails][]"]:last-of-type').clone()$lastEmailField.val("")$(".input.course_emails").append($lastEmailField)
THE SOLUTION
The way we ensured there was always a blank input field for the user was to insert a few more lines of code into our ArrayInput modification file:
class ArrayInput < SimpleForm::Inputs::StringInputdef inputinput_html_options[:type] ||= input_typeexisting_values = Array(object.public_send(attribute_name)).map do |array_el|@builder.text_field(nil, input_html_options.merge(value: array_el, name: "#{object_name}[#{attribute_name}][]"))endexisting_values.push @builder.text_field(nil, input_html_options.merge(value: nil, name: "#{object_name}[#{attribute_name}][]"))existing_values.join.html_safeenddef input_type:textendend
By pushing a blank value to the input, it essentially creates an empty field when the form loads, whether or not the saved emails column is blank.
Finally, to ensure we didn't save blank emails to the database, we added the following to the Course model:
before_save :remove_blank_emailsdef remove_blank_emailsemails.reject!(&:blank?)end
FURTHER READING
- RailsGuides.net: Simple form array text input
- SimpleForm: Documentation