Hi,
i would like to share the following component that you can use in your phoenix forms to provide a input field with a customizable source of suggestions. Could be used as a tagging component etc.
defmodule MyProjectWeb.Components.SelectComponent do use MyProjectWeb, :live_component def render(assigns) do ~H""" <div class="flex flex-row items-baseline gap-4 break-inside-avoid" phx-target={@myself}> <.label><%= @label %></.label> <div class="w-full"> <div class="flex flex-wrap items-center gap-1"> <.inputs_for :let={f} field={@form[@field_name]}> <.tag_badge color={@color_function.(f[:type].value)}> <%= f[:name].value %> <:actions> <button type="button" phx-click="autocomplete_remove" phx-target={@myself} phx-value-index={f.index} class="hover:text-red-500 pl-1" > × </button> </:actions> </.tag_badge> <.input field={f[:id]} type="hidden" /> <.input field={f[:name]} type="hidden" /> <input type="hidden" name={"case[#{@field_name}_sort][]"} value={f.index} /> </.inputs_for> </div> <.input field={@form[:"new_#{@field_name}"]} type="text" placeholder={@placeholder} phx-keydown="autocomplete_key" onkeydown="return (event.keyCode!=13);" phx-target={@myself} /> <input type="hidden" name={"case[#{@field_name}_drop][]"} /> <div> <%= for {suggestion, index} <- Enum.with_index(@suggestions) do %> <div class={"suggestion #{if index == @selected_index, do: "bg-indigo-200"} rounded-sm p-1"} phx-click="autocomplete_select" phx-target={@myself} phx-value-index={index} > <%= suggestion.name %> </div> <% end %> </div> </div> </div> """ end def mount(socket) do {:ok, assign(socket, suggestions: [], selected_index: 0)} end def update(assigns, socket) do {:ok, assign(socket, assigns)} end def handle_event("autocomplete_key", %{"key" => key, "value" => value}, socket) do case key do "ArrowDown" -> {:noreply, update_selected_index(socket, 1)} "ArrowUp" -> {:noreply, update_selected_index(socket, -1)} "Enter" -> selected = get_from_suggestions(socket, socket.assigns.selected_index) || %{name: value} {:noreply, socket.assigns.on_select.(socket, selected)} _ -> suggestions = socket.assigns.search_function.(value) {:noreply, assign(socket, suggestions: suggestions, selected_index: 0)} end end ## In real life there seems to be always a value, but to make tests easier ## this def allows events without value def handle_event("autocomplete_key", %{"key" => key}, socket) do handle_event("autocomplete_key", %{"key" => key, "value" => ""}, socket) end def handle_event("autocomplete_remove", %{"index" => index}, socket) do {:noreply, socket.assigns.on_remove.(socket, index)} end def handle_event("autocomplete_select", %{"index" => index}, socket) do {int, _} = Integer.parse(index) selected = get_from_suggestions(socket, int) {:noreply, socket.assigns.on_select.(socket, selected)} end defp get_from_suggestions(socket, index) do Enum.at(socket.assigns.suggestions, index) end defp update_selected_index(socket, direction) do update( socket, :selected_index, &rem( &1 + direction + length(socket.assigns.suggestions), length(socket.assigns.suggestions) ) ) end end
use it like this:
<.live_component module={SelectComponent} id={@id} form={@form} field_name="tags" label="Tags" placeholder="Enter a tag" search_function={&search_suggestions/1} color_function={&get_color_for_actor/1} on_select={&handle_select/2} on_remove={&handle_remove/2} />
You need to define these callback functions for it to work…