How does LiveView generates the `params` / 2nd argument for the `handle_event` value?

So here is my situation, I have a main form on a page, it manages a list of products. Each of these products is a changeset struct, and it can be added dynamically.

<%= for {product, i} <- @products do %>
<%= live_component @socket, Web.Components.ProductForm, id: i, product: product %>
<% end %>

and in the Web.Components.ProductForm live component, there are 2 forms, one to actually manage the product information, and another to search and provide an auto-completion of some kind.

<div class="column is-one-quarter">
  <%= s = form_for :search, "#", [as: "search-#{@id}", phx_change: :product_search, phx_target: @myself] %>
    <%= label s, :query, class: "label" %>
    <%= text_input s, :query, class: "input" %>
    <datalist>
      <%= for {product_id, product_code} <- @search_results do %>
        <option value="<%= product_id %>"><%= product_code %></option>
      <% end %>
    </datalist>
  </form>
</div>
<div class="column">
  <%= f = form_for  @product, "#", [as: "product-#{@id}",phx_change: :form_change, phx_target: @myself] %>
    <%= label f, :remarks, class: "label" %>
    <%= text_input f, :remarks, class: "input" %>
    <%= label f, :quantity, class: "label" %>
    <%= number_input f, :quantity, default: 0, class: "input" %>
  </form>
</div>

And in the component itself, there are these handlers:

  @impl true
  def handle_event("form_change", params, socket) do
    IO.inspect(params, label: "FORM CHANGE")
    {:noreply, socket}
  end

  @impl true
  def handle_event("product_search", params, socket) do
    IO.inspect(params, label: "SEARCH CHANGE")
    {:noreply, socket}
  end

So my questions:

  • When my the main form changes, I got the params value of %{"product-0" => %{....}, with the product-0 as the map key, which I’m assuming comes from the as param passed into the form? But when the search form changes, the params has the value of %{"search" => %{....}} , which seems to come from the atom that are passed into the form_for/4 call? Or is it derived from something else entirely that I’m missing?

  • With the above setup, I’m getting an issue where when multiple @products are present and multiple components are rendered, only search box for the last product gets rendered. It seems to be having an issue with diffing? The issue went away if I were to use form_for String.to_atom("search-#{@id}") instead, but I don’t think it is such a good idea to generate atoms dynamically. Is there any other way to generate the forms?

Yes I believe this comes from the form name, which Phoenix derives from the struct, or the value of as.

Perhaps try adding an id attr to the search form.

1 Like