crusso
Many to many checkbox form
I have roles and permissions schemas in many_to_many relationships:
schema "roles" do
field(:name, :string)
many_to_many(:permissions, X.Accounts.Permission, join_through: "permissions_roles")
end
schema "permissions" do
field(:name, :string)
many_to_many(:roles, X.Accounts.Role, join_through: "permissions_roles")
end
In my Roles form, I’d like to have a list of checkboxes of all of the Permissions that could be associated with a Role. Checking a checkbox would mean an entry in the permissions_roles table, which is a database-only relationship that is not represented by a named module.
I’ve considered various ways of solving this, but I feel like I’m writing a lot of code for something that should be simpler.
Is there a pattern for creating a form like this or a good example someone could recommend?
Marked As Solved
josevalim
Every time you will show the form, you should load all permissions as well as preload all permissions in the role you are editing right in your controller.
In your view, you will have to traverse the full list of permissions. Something like this:
<%= for permission <- @permissions do %>
<input checked="<%= role_has_permission?(@role, permission)" name="role[permissions_ids]" type="checkbox" value="<%= @permission.id %>">
<% end %>
You should probably put the input generation function in your view but that’s the code in rough lines. Keep in mind that params["permissions_ids"] will be nil if no checkbox is checked.
Also Liked
amarraja
My current application uses multi-select checkboxes all over the place.
I came up with the solution below, which works for both new and updated records, and maintains user selections in the case of errors. It was some of the first code I wrote in phoenix, and specifically using form helpers / changesets so it could do with some cleaning up and a nicer api.
<%=
multiselect_checkboxes(
f,
:categories,
Enum.map(@categories, fn c -> { c.name, c.id } end),
selected: Enum.map(@changeset.data.categories,&(&1.id))
)
%>
And the helper
defmodule MyApp.CheckboxHelper do
use Phoenix.HTML
def multiselect_checkboxes(form, field, options, opts \\ []) do
{selected, _} = get_selected_values(form, field, opts)
# HACK: If there is an error on the form, we inspect the posted params
# as expected, but they may be strings and we're converting ints.
selected_as_strings = Enum.map(selected, &"#{&1}")
boxes =
for {val, key} <- options, into: [] do
content_tag(:span, class: "checkbox") do
field_id = input_id(form, field, key)
checkbox =
tag(
:input,
name: input_name(form, field) <> "[]",
id: field_id,
type: "checkbox",
value: key,
class: "check_boxes optional",
checked: Enum.member?(selected_as_strings, "#{key}")
)
content_tag(:label) do
[checkbox, val]
end
end
end
content_tag(:div, class: "form-group check_boxes optional") do
[label(form, field), boxes]
end
end
defp get_selected_values(form, field, opts) do
{selected, opts} = Keyword.pop(opts, :selected)
param = field_to_string(field)
case form do
%{params: %{^param => sent}} ->
{sent, opts}
_ ->
{selected || input_value(form, field), opts}
end
end
defp field_to_string(field) when is_atom(field), do: Atom.to_string(field)
defp field_to_string(field) when is_binary(field), do: field
end
crusso
Okay, I’ve managed to put together a solution. One important change to your raw code (besides the closing “%>” tag needed is the addition of brackets to the role[permissions_ids] name so that the returned results comes back as an array.
I think that part of my hesitation was due to not understanding the name mapping from the form to the parameters sent back to the controller.
Thanks!
sweeneydavidj
Hi,
I think we also need to deal with the case where the user while (creating and/or updating) selects some checkboxes and then hits submit but there are are some validation errors caused, perhaps by a field somewhere else on the form. In that case when the page is refreshed the checkboxes that the user just selected should still be checked.
I struggled with this also and the following is what I came up with. Sorry that it’s not very polished (I even have a TODO in there) and I’m sure someone will tell me that I’ve overcooked this but here goes anyway
…
I wanted to have a legend on the left then the checkboxes with the label of each checkbox on the right. Clicking on the checkbox label also selects the checkbox - so the IDs had to match. And my example is of a User with many_to_many Roles.
In the form I have…
<%= render SharedView, "_form_group_multi_checkbox.html", form: f, field_name: :roles, collection: @all_roles %>
And this partial looks like…
<div class="form-group row">
<legend class="col-form-label col-sm-3"
<%= "id= #{@form.id}_#{@field_name}_legend"%>
>
<%=
opts = assigns[:opts] || []
opts[:label_value] || humanize @field_name
%>
</legend>
<div class="col-sm-6">
<%
selected_checkboxes = get_selected_checkboxes(@form, @field_name)
%>
<%= for {desc, value} <- @collection do %>
<div class="form-check">
<%=
checkbox String.to_atom(@form.id), "#{@field_name}_#{value}",
value: multi_checkbox_value(selected_checkboxes, value),
name: "#{@form.id}[#{@field_name}][#{value}]",
class: "form-check-input #{state_class(@form, @field_name)}"
%>
<%=
# TODO look for a better way to do this
label :blah, :blah, desc, for: input_id(@form, @field_name, value), class: "form-check-label"
%>
</div>
<% end %>
<%= get_error(@form, @field_name) %>
</div>
</div>
Then I have an inputs_helper.ex with the functions that are called from the partial.
# Check if a particular checkbox is selected or not.
# If selected return true, otherwise return false.
def multi_checkbox_value(selected_checkboxes, field_value) do
selected_checkboxes && field_value in selected_checkboxes
end
# Returns an array of selected checkbox values
# For example ["option-two", "option-four"]
#
# Checkboxs may be backed by
# - a simple list of strings.
# - a many_to_many association in which case
# we need to deals with the various case where the data will come
# from parameters or a list of structs or a list of changesets.
def get_selected_checkboxes(form, field) do
input_value(form, field)
|> _get_selected_checkboxes()
end
# inputs is an empty list
defp _get_selected_checkboxes([]) do
[]
end
# inputs is a list of strings
defp _get_selected_checkboxes([h | _t] = inputs) when is_binary(h) do
# return unchanged.
inputs
end
# inputs is a map of params
defp _get_selected_checkboxes(%{} = inputs) do
Janey.Schema.CheckboxHelper.parse_checkbox_data(inputs)
end
# inputs is a list of changesets or a list of structs
# not practical to match on a list of something so we do it while enumerating.
defp _get_selected_checkboxes(inputs) do
Enum.map(inputs, &get_input_value(&1))
|> Enum.filter(&(&1 != nil))
end
# from changeset
defp get_input_value(%Ecto.Changeset{} = %{action: action, data: data} = _input) do
if(action == :update) do
data.value
else
nil
end
end
# from struct
defp get_input_value(%_{} = input) do
input.value
end
Any feedback is welcome!
Last Post!
yessikameliza
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex










