Checkboxes as lists of ids: common case?

Phoenix.HTML.Form has a checkbox function, but it seems targeted at standalone boolean checkboxes, like hiding/showing a password:

Screen Shot 2020-02-20 at 6.42.53 PM

However, another common case (I think) is to select from a list of options, like this:

(Because I don’t think many ordinary humans are comfortable using multiple-select <select> tags.)

When I created this form, I wanted the POST to produce a list of chosen_animal_ids. My code was this:

checkbox(f, :animal_ids,
  name: "#{input_name(:animals, :chosen_animal_ids)}[]",
  value: a.id,
  hidden_input: false)

That doesn’t actually work. Unlike other Phoenix.HTML.Form functions, value: here doesn’t set the HTML value attribute. The result of the above is this HTML:

<input type="checkbox" value="true">

(I don’t understand the checkbox documentation for value: “the value used to check if a checkbox is checked or unchecked. The default value is extracted from the form data if available”. So I could be missing something obvious.)

Working code is this:

checkbox(f, :animal_ids,
   name: "#{input_name(:animals, :chosen_animal_ids)}[]",
   checked_value: a.id,
#  ^^^^^^^
   hidden_input: false)

Questions:

  1. Am I doing something completely wrong?
  2. Are there libraries that extend Phoenix.HTML.Form that I should know about? (I’ve only found this which addresses a different concern.)
  3. Would it be useful for Phoenix.HTML.Form to have a special function for this case (like the one I’m about to write) - or documentation that describes the issue? (In addition to the value conclusion, I think the explicit name: "#{input_name(:animals, :chosen_animal_ids)}[]" is kind of gross.)
4 Likes

I have build my helper for this case, it is mostly coming from this post

It is checked, not checked_value

Here is one I made for Bootstrap 4

  def multiselect_checkboxes(form, field, options, opts \\ []) do
    {selected, _} = get_selected_values(form, field, opts)
    selected_as_strings = Enum.map(selected, &"#{&1}")

    # Use inline mode by default
    class = opts[:class] || "form-check form-check-inline"
    # By default, input is active (not disabled)
    disabled = opts[:disabled]

    for {value, key} <- options, into: [] do
      id = input_id(form, field, key)
      content_tag(:div, class: class) do
        [
          tag(:input,
            name: input_name(form, field) <> "[]",
            id: id,
            class: "form-check-input",
            type: "checkbox",
            value: key,
            checked: Enum.member?(selected_as_strings, "#{key}"),
            disabled: disabled
          ),
          content_tag(:label, value, class: "form-check-label", for: id)
        ]
      end
    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

and I call it like this

  <label for="roles"><%= gettext("Roles") %></label>
  <div class="form-group">
    <%= 
      multiselect_checkboxes(
        f,
        :roles,
        select_roles(),
        selected: user_selected_roles(@changeset.data),
        disabled: is_self_and_last_admin?(
          @current_user, @changeset.data
        )
      )
    %>
  </div>

Some of the private functions are missing, but there are not difficult to reproduce.

There is nothing specific to Phoenix, I do remember having the same problem with Rails :slight_smile:

6 Likes

Yes, it’s “value” if you use tag, “checked_value” if you use checkbox.

2 Likes

ah, ok :slight_smile:

Actually, working with checkbox is awkward enough that I think you were right to work with tag.