I recently found myself addressing a product requirement involving a form with input fields generated from a list of data we retrieved from a third-party provider. This requirement proved challenging as the prescribed user experience stretched a little beyond the familiar tools for form building in Elixir.
Even more frustrating, the wireframes presented the data as a table with checkboxes for each row and not a select_multiple
type element.
To recreate a minimal working example, I started my own survey company.
The survey company is responsible for gathering lists of people's favorite animals. We get this list from an external API and then need to allow a user to make a selection on our surveys page.
For simplicity's sake, we will only work with a hardcoded list of strings to represent our external data.
Modeling the Survey
Our survey data is modeled as such:
- Name
- Favorite animals
I've added two fields to represent each of our examples from the form. One for a select multiple element and one for a checkbox group type element.
schema "surveys" do
field :name, :string
field :favorite_animal_select_multiple, {:array, :string}
field :favorite_animal_checkbox_group, {:array, :string}
timestamps()
end
Structuring the Form Around the Survey Schema
Given that list of animal choices is outside of our direct control:
- How do we represent the form data as a list of strings while presenting the field as a group of checkboxes?
- How can we use our schema changeset for form validations?
The phoenix documentation has a really nice example on how to implement a similar behavior with a multi-select element (multiple_select/4). https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#multiple_select/4
multiple_select(f, :favorite_animal_select_multiple, ["Phoenix", "Wallaby", "Numbat"])
Returns the following HTML:
<select id="favorite-animals-survey-form_favorite_animal_select_multiple" multiple="" name="survey[favorite_animal_select_multiple][]"><option value="Phoenix">Phoenix</option><option value="Wallaby">Wallaby</option><option value="Numbat">Numbat</option></select>
Generating Unserialized Inputs
Default adding a checkbox adds a key to the map that represents the form. If we generated our form inputs in a loop, we end up with some html like this:
["Phoenix", "Wallaby", "Numbat"]
|> Enum.map(&(~s(<input type="checkbox" name="#{&1}">)))
|> Enum.join()
Returns the following markup:
<input type="checkbox" name="Phoenix" value="Phoenix">
<input type="checkbox" name="Wallaby" value="Wallaby">
<input type="checkbox" name="Numbat" value="Numbat">
*** In case you’re not familiar with the ~s() syntax, we are using a sigil for the convenience of not having to escape the double quotes in the example.
https://elixir-lang.org/getting-started/sigils.html#strings-char-lists-and-word-lists-sigils
The code above creates a map representation of our form that grows by one key every time a new animal gets added to our survey.
%{
"Phoenix" => "Phoenix",
"Wallaby" => "Wallaby",
"Numbat" => "Numbat",
"Honey Badger" => "Honey Badger"
}
We need a different solution if we want to use our survey changeset for validations.
Let’s compare the checkbox data against what we get from Phoenix’s select_multiple/4 function.
%{
"Favorite_animal_select_multiple" =>
["Phoenix", "Wallaby", "Numbat"]
}
This data conforms to our schema nicely, so how can we implement this array structure for checkboxes?
There is, unfortunately, no shortcut here like there is with select_multiple
, but if we inspect the markup that is generated by this function, we can borrow a few html patterns to group our checkboxes into a single field.
<select id="favorite-animals-survey-form_favorite_animal_select_multiple" multiple="" name="survey[favorite_animal_select_multiple][]"><option value="Phoenix">Phoenix</option><option value="Wallaby">Wallaby</option><option value="Numbat">Numbat</option></select>
The name attribute syntax is exactly what we need to model our checkboxes as a collection.
name="survey[favorite_animal_select_multiple][]"
We can also group the checkboxes in html using a fieldset
element.
<fieldset id="favorite-animals-survey-form_favorite_animal_checkbox_group">
<label>What is Your Favorite Animal?</label>
<%= for animal <- @animal_choices do %>
<input type="checkbox" name="survey[favorite_animal_checkbox_group][]" value={animal} /><%= animal %><br />
<% end %>
</fieldset>
Now when we inspect our form data from the submit event, we can see it conforms to the same array type structure as the select_multiple
.
%{
"favorite_animal_checkbox_group" =>
["Phoenix", "Wallaby", "Numbat"]
}
Using a Changeset to Display Errors
Now that our form fields pass the correct data structure to the backend, we still need to validate it and show errors to the user. We can do so by adding an error-tag.
<%= error_tag(f, :favorite_animal_checkbox_group) %>
We run the form data through our survey changeset for validation when the form is submitted.
# lib/select_multiple/surveys/survey.ex
def changeset(survey, attrs) do
survey
|> cast(attrs, [:name, :favorite_animal_select_multiple, :favorite_animal_checkbox_group])
|> validate_required([
:name,
:favorite_animal_select_multiple,
:favorite_animal_checkbox_group
])
end
Our entire form component looks as such in the template:
# lib/select_multiple_web/live/survey_live/form_component.html.heex
<.form
let={f}
for={@changeset}
id="favorite-animals-survey-form"
phx-target={@myself}
phx-submit="save"
>
<%= label(f, :name) %>
<%= text_input(f, :name) %>
<%= error_tag(f, :name) %>
<label>What is Your Favorite Animal?</label>
<%= multiple_select(f, :favorite_animal_select_multiple, @animal_choices) %>
<fieldset id="favorite-animals-survey-form_favorite_animal_checkbox_group">
<label>What is Your Favorite Animal?</label>
<%= for animal <- @animal_choices do %>
<input type="checkbox" name="survey[favorite_animal_checkbox_group][]" value={animal} /><%= animal %><br />
<% end %>
<%= error_tag(f, :favorite_animal_checkbox_group) %>
</fieldset>
<div>
<%= submit("Save", phx_disable_with: "Saving...") %>
</div>
</.form>
And our handle_event
for form submit
# lib/select_multiple_web/live/survey_live/form_component.ex
def handle_event("save", %{"survey" => survey_params}, socket) do
save_survey(socket, socket.assigns.action, survey_params)
end
defp save_survey(socket, :new, survey_params) do
case Surveys.create_survey(survey_params) do
{:ok, _survey} ->
{:noreply,
socket
|> put_flash(:info, "Survey created successfully")
|> push_redirect(to: socket.assigns.return_to)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
Conclusion
There is a clean, testable way forward to working with form checkbox inputs when your data model is a list of items. multiple_select
has great support out of the box, but you can get similar behavior for checkboxes with a little bit of extra markup in your template.
Resources: https://github.com/thejohncotton/dynamic-form-inputs-example
Feedback
"*" indicates required fields
Founded in 2007, Binary Noggin is a team of software engineers who serve as a trusted extension of your team, helping your company succeed through collaboration. We forge customizable solutions using Agile methodologies and our mastery of Elixir, Ruby and other open-source technologies. Share your ideas with us on Facebook and Twitter.