Handling events in Phoenix LiveComponents

Hello,

I’m currently struggling while programming the following reusable auto-complete component:

defmodule AppWeb.AutoCompleteField do
  use AppWeb, :component

  defmacro __using__(opts \\ []) do
    fun = Keyword.fetch!(opts, :search_function)

    quote do
      @impl true
      def handle_event("search", %{"_target" => [name]} = params, socket) do
        %{^name => value} = params
        %{id: id} = socket.assigns
        pid = self()

        spawn(fn →
          items = unquote(fun).(Map.fetch!(params, name))
          send_update(pid, __MODULE__, id: id, items: items)
        end)

        {:noreply, socket}
      end

      @impl true
      def handle_event("select", params, socket) do
        IO.inspect(params, label: "select")

        # TODO: send_update

        {:noreply, assign(socket, items: [])}
      end

      @impl true
      def handle_event("select", %{"index" => index}, socket) do
        index = String.to_integer(index)
        value = Enum.at(socket.assigns.items, index)

        {:noreply, assign(socket, items: [], value: value)}
      end
    end
  end

  attr :items, :list, default: []
  attr :"list-class", :string, default: ""
  attr :"item-class", :string, default: ""
  attr :"container-class", :string, default: ""
  attr :rest, :global

  def autocomplete_field(assigns) do
    ~H"""
    <div class={Map.get(assigns, :"container-class")}>
      <input
        role="combobox"
        type="text"
        {@rest}
        phx-change="search"
        phx-debounce="500"
        aria-controls="options"
        aria-expanded="false"
      />

      <%= if @items != [] do %>
        <ul role="listbox" class={Map.get(assigns, :"list-class")}>
          <%= for {suggestion, index} <- Enum.with_index(@items) do %>
            <li
              role="option"
              class={Map.get(assigns, :"item-class")}
              phx-click="select"
              phx-value-index={index}
            >
              <%= render_slot(@inner_block, suggestion) %>
            </li>
          <% end %>
        </ul>
      <% end %>
    </div>
    """
  end
end

It mostly works, the only problem is this that the first event, the debounced search is properly sent to the parent live view like this:

[debug] HANDLE EVENT
  Component: AppWeb.LocationLive.FormComponent
  View: AppWeb.LocationLive.Index
  Event: "search"
  Parameters: %{"_target" => ["name"], "name" => "a"}

But the following select event is sent incorrectly, hence the missing Component debug statement:

[debug] HANDLE EVENT
  View: AppWeb.LocationLive.Index
  Event: "select"
  Parameters: %{"index" => "0", "value" => 0}

Any ideas what I’m doing wrong? Or understand incorrectly?

Off the top of my head, I’d try setting phx-target within the <li> as described in the Targeting Component Events docs. You’ll probably need to pass in the parent LiveComponent’s @myself assign. My guess is that the <input> is working either because phx-target is set by the <form> it’s nested under or passed in via @rest.

1 Like

This is a standard stateless component, so @myself does not exist. And no, I’m not setting any phx-target manually. But thanks for the hint about <form> doing it automatically, this could be the reason. I will investigate.

You need to set phx-target otherwise the phx-click event will be sent to your live view.

Your function component needs to receive the target via assign and set it for its phx-click event.

Basically what @codeanpeace said :slight_smile:

1 Like

And to what? I don’t have any value I can set it to.

The parent live component has to pass it via assigns

Thanks a lot @codeanpeace and @trisolaran, solved it by providing an explicit phx-target attribute and forwarding it to all elements:

defmodule AppWeb.AutoCompleteField do
  use AppWeb, :component

  defmacro __using__(opts \\ []) do
    fun = Keyword.fetch!(opts, :search_function)

    quote do
      @impl true
      def handle_event("search", %{"_target" => [name]} = params, socket) do
        %{^name => value} = params
        %{id: id} = socket.assigns
        pid = self()

        spawn(fn ->
          items = unquote(fun).(Map.fetch!(params, name))
          send_update(pid, __MODULE__, id: id, items: items)
        end)

        {:noreply, socket}
      end

      @impl true
      def handle_event("select", params, socket) do
        IO.inspect(params, label: "select")
        # TODO: send_update

        {:noreply, assign(socket, items: [])}
      end

      @impl true
      def handle_event("select", %{"index" => index}, socket) do
        index = String.to_integer(index)
        value = Enum.at(socket.assigns.items, index)

        {:noreply, assign(socket, items: [], value: value)}
      end
    end
  end

  attr :items, :list, default: []
  attr :"phx-target", :any, required: true
  attr :"list-class", :string, default: ""
  attr :"item-class", :string, default: ""
  attr :"container-class", :string, default: ""
  attr :rest, :global

  def autocomplete_field(assigns) do
    ~H"""
    <div class={Map.get(assigns, :"container-class")}>
      <input
        role="combobox"
        type="text"
        {@rest}
        phx-change="search"
        phx-debounce="500"
        phx-target={Map.get(assigns, :"phx-target")}
        aria-controls="options"
        aria-expanded="false"
      />

      <%= if @items != [] do %>
        <ul role="listbox" class={Map.get(assigns, :"list-class")}>
          <%= for {suggestion, index} <- Enum.with_index(@items) do %>
            <li
              role="option"
              class={Map.get(assigns, :"item-class")}
              phx-click="select"
              phx-value-index={index}
              phx-target={Map.get(assigns, :"phx-target")}
            >
              <%= render_slot(@inner_block, suggestion) %>
            </li>
          <% end %>
        </ul>
      <% end %>
    </div>
    """
  end
end
1 Like