Checkbox binding in LiveView

I am trying to bind a checkbox to a property in my assigns. My very basic LiveView 0.16.0 module looks like this:

defmodule CheckboxWeb.Live.PlaygroundLive do
  use CheckboxWeb, :live_view

  def mount(_, _, socket) do
    {:ok, assign(socket, counter: 0, is_selected: true)}
  end

  def render(assigns) do
    ~H"""
    <input type="checkbox" checked={@is_selected} phx-click="toggle">
    """
  end

  def handle_event("toggle", _, %{assigns: %{counter: counter}} = socket) do
    {:noreply, assign(socket, counter: counter + 1)}
  end
end

I would expect the checkbox to stay checked after the server response, however the check box toggles on and off. When I add something like data-counter={@counter} to the input element, it behaves as I expect.

Very new to Elixir, Phoenix, and LiveView and feel like I’m missing something really basic here, but have not had much luck figuring out what it is.

1 Like

I’d recommend using Phoenix.HTML.Form - checkbox

Looks like you do not want to create a form for a changeset, so look at “With limited data” and “Without form data” here: Phoenix.HTML.Form — Phoenix.HTML v3.0.4

Example:

<form action="" phx-change="my_form_changed">
  <%= Phoenix.HTML.Form.checkbox(:my_checkboxes, "my_first_checkbox",
    checked_value: "1",
    unchecked_value: "0",
    value: @my_checkboxes_values.my_first_checkbox
  )
  %>
  <%= Phoenix.HTML.Form.checkbox(:my_checkboxes, "my_second_checkbox",
    checked_value: "1",
    unchecked_value: "0",
    value: @my_checkboxes_values.my_second_checkbox
  )
  %>
</form>

...
# assign
my_checkboxes_values: %{my_first_checkbox: "0", my_second_checkbox: "0"}
...

@impl true
def handle_event("my_form_changed", params, socket) do
  %{"_target" => ["my_checkboxes", ref], "my_checkboxes" => map} = params
  IO.inspect({ref, map, Map.get(map, ref)}, label: :my_form_changed)
  # update my_checkboxes_values assign
  {:noreply, socket}
end

will output:

my_form_changed: {
 "my_first_checkbox",
 %{"my_first_checkbox" => "1", "my_second_checkbox" => "0"}, 
 "1"
}
3 Likes

When you click the checkbox, the browser toggles it, but since you bound click event handler, after the checkbox is toggled by the browser - liveview processes the event and rerenders your template using your old @is_checked value (which didn’t change).

1 Like

Is there a recommended solution to this problem?

Let’s assume I have checkbox that can be checked only when the server allows to do it. When user checks such checkbox, browser will check it, server will validate some condition, and if validation was unsuccessful, it will uncheck the checkbox and render an error. When user tries to check the checkbox once again, browser will check it but server won’t uncheck it as nothing will change on the server side (server thinks that the checkbox is uncheked).

To handle this scenario, you’ll need to maintain additional state beyond just the checkbox’s checked status. The key is to track the validation history, which can be done using a counter or similar mechanism. Here’s how it works:

  1. When the user first checks the box, the UI updates optimistically while the server validates
  2. If validation fails, you revert the checkbox and store that a validation attempt occurred
  3. On subsequent attempts, you can use this validation history to determine the final state

This way, the server remains the source of truth, but you have the context needed to handle the UI appropriately. Does it make sense?

1 Like

Why not just disable the checkbox if it’s not allowed?

2 Likes

This was what we actually did :sweat_smile:

1 Like