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::StringInput
def input
input_html_options[:type] ||= input_type
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}][]"))
end.join.html_safe
end
def input_type
:text
end
end
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_params
params.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', @addEmailField
addEmailField: (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::StringInput
def input
input_html_options[:type] ||= input_type
existing_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}][]"))
end
existing_values.push @builder.text_field(nil, input_html_options.merge(value: nil, name: "#{object_name}[#{attribute_name}][]"))
existing_values.join.html_safe
end
def input_type
:text
end
end
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_emails
def remove_blank_emails
emails.reject!(&:blank?)
end
FURTHER READING
- RailsGuides.net: Simple form array text input
- SimpleForm: Documentation