Why clicking a checkbox fires a Phoenix LiveView event?

Background

I have a form that has some checkboxes. This form will show checkboxes depending on a condition.
After the form I have a button, which is supposed to send a phoenix event that will perform an action.

However, when I click a checkbox (to select/de-select it) the phoenix event is still created.
I honestly don’t know why, I have a feeling this is related to the fact that I am not using Phoenix forms (I am using pure HTML ones, which are less safe) but I don’t have enough experience to know.

Code

@spec render(map) :: Rendered.t
def render(assigns) do
  ~H"""  
  <div class="body">
    <form phx-change="filters">
      <div>
        <input type="hidden" name="syndicates[]" value="">
        <%= for synd <- @syndicates  do %>
          <%= syndicate_checkbox(synd: synd, checked: synd in @selected_syndicates) %>
        <% end %>
      </div>
    </form>

    <div class={@selected_syndicates |> none_active?() |> display()}>
      <p>Nothing to see here!.</p>
    </div>

    <button
      class={@selected_syndicates |> any_active?() |> display()}
      phx-click="execute_command"
      phx-value-command={@selected_command.id}
      phx-value-strategy={@selected_strategy.id}>
        Execute Command
    </button>

  </div>
  """
end

  @spec display(boolean) :: String.t()
  defp display(true), do: "show"
  defp display(_), do: "hidden"

  @spec any_active?([map]) :: boolean
  defp any_active?([]), do: false
  defp any_active?(_), do: true

  @spec none_active?([map]) :: boolean
  defp none_active?(data), do: !any_active(data)

  @spec syndicate_checkbox(map) :: Rendered.t
  defp syndicate_checkbox(assigns) do
    assigns = Enum.into(assigns, %{})

    ~H"""
      <div class={display(@checked)}>
        <div class="row single-syndicate">
          <input class="column single-checkbox" type="checkbox" id="{@synd.id}"
                  name="syndicates[]" value="{@synd.id}">

          <label for="{@synd.id}" class="column"><%= @synd.name %></label>
        </div>
      </div>
    """
  end

Problem

Basically, my objective here is simple:

  • if none_active? returns true, then I show a text and I hide the button
  • otherwise, I show the button and hide the text

However, the behavior displayed is very confusing to me (see Deactivate menu):

ezgif.com-gif-maker(1)

(For a bigger image see https://i.stack.imgur.com/9ThDu.gif)

The error shown in the console behind is:

11:09:28.334 [error] GenServer #PID<0.2115.0> terminating
** (FunctionClauseError) no function clause matching in WebInterface.Live.Window.handle_event/3
    (web_interface 1.1.0) lib/web_interface/live/window.ex:53: WebInterface.Live.Window.handle_event("filters", %{"_target" => ["syndicates"], "syndicates" => ["", "{@synd.id}"]}, #Phoenix.LiveView.Socket<assigns: %{__changed__: %{}, commands: [%{description: "\n          Activating a syndicate will cause the app to create a sell order on warframe.market for each product of the said syndicate.\n          The prices of each item will be determined accoring to a strategy that you can define.\n        ", id: :activate, name: "Activate"}, %{description: "\n          Deactivating a syndicate removes all sell orders from waframe.market for the given syndicate.\n        ", id: :deactivate, name: "Deactivate"}, %{description: "\n          Saving authentication information will allow this application to make requests in your behalf.\n          It is a required step for the application to work.\n        ", id: :authenticate, name: "Authenticate"}], flash: %{"info" => "Request completed: [ok: :success]"}, live_action: nil, selected_command: %{description: "\n          Deactivating a syndicate removes all sell orders from waframe.market for the given syndicate.\n        ", id: :deactivate, name: "Deactivate"}, selected_strategy: %{description: "\n          Gets the 3 lowest prices for the given item and calculates the average.\n        ", id: :top_three_average, name: "Top 3 Average"}, selected_syndicates: [%{id: "red_veil", name: "Red Veil"}], strategies: [%{description: "\n          Gets the 3 lowest prices for the given item and calculates the average.\n        ", id: :top_three_average, name: "Top 3 Average"}, %{description: "\n          Gets the 5 lowest prices for the given item and calculates the average.\n        ", id: :top_five_average, name: "Top 5 Average"}, %{description: "\n          Gets the lowest price for the given item and uses it.\n        ", id: :equal_to_lowest, name: "Equal to lowest"}, %{description: "\n          Gets the lowest price for the given item and beats it by 1.\n        ", id: :lowest_minus_one, name: "Lowest minus one"}], syndicates: [%{id: "red_veil", name: "Red Veil"}, %{id: "perrin_sequence", name: "Perrin Sequence"}, %{id: "new_loka", name: "New Loka"}, %{id: "arbiters_of_hexis", name: "Arbiters of Hexis"}, %{id: "steel_meridian", name: "Steel Meridian"}, %{id: "cephalon_suda", name: "Cephalon Suda"}, %{id: "simaris", name: "Cephalon Simaris"}]}, endpoint: WebInterface.Endpoint, id: "phx-FuUYGcft5EwElQBl", parent_pid: nil, root_pid: #PID<0.2115.0>, router: WebInterface.Router, transport_pid: #PID<0.2084.0>, view: WebInterface.Live.Window, ...>)
    (phoenix_live_view 0.17.6) lib/phoenix_live_view/channel.ex:349: anonymous fn/3 in Phoenix.LiveView.Channel.view_handle_event/3
    (telemetry 1.0.0) c:/Users/palme/Worskapce/fl4m3/market_manager/deps/telemetry/src/telemetry.erl:293: :telemetry.span/3
    (phoenix_live_view 0.17.6) lib/phoenix_live_view/channel.ex:206: Phoenix.LiveView.Channel.handle_info/2
    (stdlib 3.17.1) gen_server.erl:695: :gen_server.try_dispatch/4
    (stdlib 3.17.1) gen_server.erl:771: :gen_server.handle_msg/6
    (stdlib 3.17.1) proc_lib.erl:236: :proc_lib.wake_up/3
Last message: %Phoenix.Socket.Message{event: "event", join_ref: "37", payload: %{"event" => "filters", "type" => "form", "uploads" => %{}, "value" => "syndicates%5B%5D=&syndicates%5B%5D=%7B%40synd.id%7D&_target=syndicates%5B%5D"}, ref: "151", topic: "lv:phx-FuUYGcft5EwElQBl"}
State: %{components: {%{}, %{}, 1}, join_ref: "37", serializer: Phoenix.Socket.V2.JSONSerializer, socket: #Phoenix.LiveView.Socket<assigns: %{__changed__: %{}, commands: [%{description: "\n          Activating a syndicate will cause the app to create a sell order on warframe.market for each product of the said syndicate.\n          The prices of each item will be determined accoring to a strategy that you can define.\n        ", id: :activate, name: "Activate"}, %{description: "\n          Deactivating a syndicate removes all sell orders from waframe.market for the given syndicate.\n        ", id: :deactivate, name: "Deactivate"}, %{description: "\n          Saving authentication information will allow this application to make requests in your behalf.\n          It is a required step for the application to work.\n        ", id: :authenticate, name: "Authenticate"}], flash: %{"info" => "Request completed: [ok: :success]"}, live_action: nil, selected_command: %{description: "\n          Deactivating a syndicate removes all sell orders from waframe.market for the given syndicate.\n        ", id: :deactivate, name: "Deactivate"}, selected_strategy: %{description: "\n          Gets the 3 lowest prices for the given item and calculates the average.\n        ", id: :top_three_average, name: "Top 3 Average"}, selected_syndicates: [%{id: "red_veil", name: "Red Veil"}], strategies: [%{description: "\n          Gets the 3 lowest prices for the given item and calculates the average.\n        ", id: :top_three_average, name: "Top 3 Average"}, %{description: "\n          Gets the 5 lowest prices for the given item and calculates the average.\n        ", id: :top_five_average, name: "Top 5 Average"}, %{description: "\n          Gets the lowest price for the given item and uses it.\n        ", id: :equal_to_lowest, name: "Equal to lowest"}, %{description: "\n          Gets the lowest price for the given item and beats it by 1.\n        ", id: :lowest_minus_one, name: "Lowest minus one"}], syndicates: [%{id: "red_veil", name: "Red Veil"}, %{id: "perrin_sequence", name: "Perrin Sequence"}, %{id: "new_loka", name: "New Loka"}, %{id: "arbiters_of_hexis", name: "Arbiters of Hexis"}, %{id: "steel_meridian", name: "Steel Meridian"}, %{id: "cephalon_suda", name: "Cephalon Suda"}, %{id: "simaris", name: "Cephalon Simaris"}]}, endpoint: WebInterface.Endpoint, id: "phx-FuUYGcft5EwElQBl", parent_pid: nil, root_pid: #PID<0.2115.0>, router: WebInterface.Router, transport_pid: #PID<0.2084.0>, view: WebInterface.Live.Window, ...>, topic: "lv:phx-FuUYGcft5EwElQBl", upload_names: %{}, upload_pids: %{}}

Question

As far as I understand, the crash happens because I am sending a filter event that I am not catching in a higher level.

However, I don’t understand why this event is being generated in the first place. I am only selecting/deselecting a checkbox, nothing should be happening.

  • What am I doing wrong?
  • Should I replace this HTML form with a Phoenix one? If so, would I go about it?

The filters event is being triggered when you check the box because that is a form change, and you have set filters as the value for phx-change on the form.

The error is because you have not correctly defined the handle_event function on the live view controlling the form.

So, every time I change the form, I will get the filters event. I will see where this road leads me!

You can add a submit button to the form and use phx-submit instead of phx-change. Then you’ll only be sent an event if the form is submitted.

2 Likes

That said, I highly recommend a phx-change event since it is usually pretty critical to keep the form state in sync front end to backend.

2 Likes