Search code examples
formsphoenix-live-view

How to make checkbox work with form bindings Phoenix LV?


Background

I have a LiveView page where I have a small form. This form is supposed ton have a group of radio buttons, and a group of checkboxes.

To achieve this, I am trying to use Form Bindings:

Code

This is what I currently have. I want to give the user the option to receive notifications about some topics.

For the radio button group, the user chooses whether to receive the notification via email or sms. These are mutually exclusive.

For the checkbox group, the user chooses the topics. These are not mutually exclusive.

This is my html.heex file:

    <.form for={@form} phx-submit="save">
       <.input id={"email"} name="notifications" type="radio" value={"email"} field={@form[:notifications]} />
       <.input id={"sms"} name="notifications" type="radio" value={"sms"} field={@form[:notifications]} />

      <.input id={"sports"} name="topics" type="checkbox" value={"sports"} field={@form[:topics]} />
      <.input id={"science"} name="topics" type="checkbox" value={"science"} field={@form[:topics]} />

      <button>Save</button>
    </.form>

And this is the corresponding LivewView:

defmodule MyApp.Settings do
  use MyApp, :live_view

  @impl true
  def mount(_params, _session, socket) do
  
    form = to_form(%{})
    updated_socket = assign(socket, form: form)

    {:ok, updated_socket}
  end

  def handle_event(event, params, socket) do
    Logger.info("Event: #{inspect(event)} ; #{inspect(params)}")
    {:noreply, socket}
  end

end

If you are a keen reader, you will see I have no label, or legend tags anywhere. This is for simplification purposes.

Problem

Even though I get the correct value for the "notifications" radio group, the problem is that my checkboxes always return true or false:

 [debug] HANDLE EVENT "save" in MyApp.Settings
  Parameters: %{"notifications" => "sms", "topics" => "true"}

This is rather confusing. I was expecting something like:

Parameters: %{"notifications" => "sms", "topics" => ["sports", "science"]}

After reading the relevant parts of the docs, I don't think the type checkbox is considered a special type, so it is not documented (in the link above mentioned).

I also don't quite understand why I need a schema when to_form seems to work perfectly fine with %{}.

Questions

  • How can I get a list of values instead of a boolean for checkboxes?
  • I don't use schemas in my application. Can to_form work with a Struct?
  • What would be the benefit of passing a Struct to to_form ?

Solution

  • After reading both Articles provided by the Elixir community:

    I have arrived to a code that mixes what I learned from both. Definitely recommend the reading. Here are the code pieces for your inspiration:

    core_components.ex

      @doc """
      Generate a checkbox group for multi-select.
    
      ## Examples
    
        <.checkgroup
          field={@form[:genres]}
          label="Genres"
          options={[%{name: "Fantasy", id: "fantasy"}, %{name: "Science Fiction", id: "sci-fi"}]}
          selected={[%{name: "Fantasy", id: "fantasy"}]}
        />
    
      """
      attr :id, :any
      attr :name, :any
      attr :label, :string, default: nil
      attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form, for example: @form[:genres]"
      attr :errors, :list
      attr :required, :boolean, default: false
      attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
      attr :rest, :global, include: ~w(disabled form readonly)
      attr :class, :string, default: nil
    
      attr :selected, :any, default: [],
        doc: "the currently selected options, to know which boxes are checked"
    
      def checkgroup(assigns) do
        new_assigns =
          assigns
          |> assign(:multiple, true)
          |> assign(:type, "checkgroup")
    
        input(new_assigns)
      end
    
    
    
      def input(%{type: "checkgroup"} = assigns) do
        ~H"""
        <div class="mt-2">
          <%= for opt <- @options do %>
    
            <div class="relative flex gap-x-3">
              <div class="flex h-6 items-center">
                <input id={opt.id} name={@name} type="checkbox" value={opt.id} class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" checked={opt in @selected}>
              </div>
              <div class="text-sm leading-6">
                <label  for={opt.id} class="text-base font-semibold text-gray-900"><%= opt.name %></label>
              </div>
            </div>
    
          <% end %>
        </div>
        """
      end
    

    usage_live.html.heex

          <.simple_form for={@form} phx-change="change" phx-submit="execute">
    
            <div class="mt-4">
              <.checkgroup field={@form[:genres]} label="Genres" options={@all_genres} selected={@selected_genres} required/>
            </div>
    
            <div class="mt-4">
              <.button}>Execute Command</.button>
            </div>
          </.simple_form>