View Source Cheatsheet

Style LiveSelect like a Phoenix Core Component, with label and errors

1. Add this to core_components.ex:

def live_select(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
  assigns =
    assigns
    |> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
    |> assign(:live_select_opts, assigns_to_attributes(assigns, [:errors, :label]))
  
  ~H"""
  <div phx-feedback-for={@field.name}>
    <.label for={@field.id}><%= @label %></.label>
    <LiveSelect.live_select
      field={@field}
      text_input_class={[
        "mt-2 block w-full rounded-lg border-zinc-300 py-[7px] px-[11px]",
        "text-zinc-900 focus:outline-none focus:ring-4 sm:text-sm sm:leading-6",
        "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5",
        "border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5",
        @errors != [] && "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10"
      ]}
      {@live_select_opts}
    />
  
    <.error :for={msg <- @errors}><%= msg %></.error>
  </div>
  """
end

2. Then call it this way:

<.live_select field={@form[:city]} label="City" phx-target={@myself} />

You can also pass any of the other LiveSelect options.

Implementing a simple search functionality

1. With no options displayed if the user doesn't enter any text

Heex template:

<.live_select field={@form[:locations]} update_min_len={1} phx-focus="clear" />

Live view:

@impl true
def handle_event("live_select_change", %{"id" => id, "text" => text}, socket) do
 options =
   retrieve_locations()
   |> Enum.filter(&(String.downcase(&1) |> String.contains?(String.downcase(text))))

 send_update(LiveSelect.Component, options: options, id: id)

 {:noreply, socket}
end

@impl true
def handle_event("clear", %{"id" => id}, socket) do
  send_update(LiveSelect.Component, options: [], id: id)

  {:noreply, socket}
end

2. With a fixed set of default options to be displayed when the text input is empty

Heex template:

<.live_select field={@form[:locations]} update_min_len={0} phx-focus="set-default" options={@default_locations} />

Live view:

@impl true
def mount(socket) do
  socket = assign(socket, default_locations: default_locations())
  
  {:ok, socket}
end

@impl true
def handle_event("live_select_change", %{"id" => id, "text" => text}, socket) do
  options =
    if text == "" do 
      socket.assigns.default_locations
    else
      retrieve_locations()
      |> Enum.filter(&(String.downcase(&1) |> String.contains?(String.downcase(text))))
    end
    
  send_update(LiveSelect.Component, options: options, id: id)
    
  {:noreply, socket}
end

@impl true
def handle_event("set-default", %{"id" => id}, socket) do
  send_update(LiveSelect.Component, options: socket.assigns.default_locations, id: id)

  {:noreply, socket}
end

Tailwind

Heex template

<.live_select
   field={@form[:my_field]}
   dropdown_extra_class="!top-full bottom-full" />

DaisyUI

Heex template

<.live_select
   field={@form[:my_field]}
   style={:daisyui}
   container_extra_class="dropdown-top" />

Display tags underneath the input field (Tailwind)

Heex template:

<.live_select
   field={@form[:my_field]}
   mode={:tags}
   container_extra_class="flex flex-col"
   dropdown_extra_class="top-11"
   tags_container_extra_class="order-last" />

Limit the height of the dropdown and make it scrollable (Tailwind)

Heex template:

<.live_select
   field={@form[:my_field]}
   dropdown_extra_class="max-h-60 overflow-y-scroll" />