Feature

Simple Typeahead with LiveView and no JS

Preview

A simple typeahead that is very easy to implement. Just start with typing a character and LiveView will return a list of matching products. No javascript required.

lib/phoenix_features_web/live/components/typeahead_simple.ex
defmodule PhoenixFeaturesWeb.Components.TypeaheadSimple do
  use PhoenixFeaturesWeb, :live_component

  alias PhoenixFeatures.Products

  @impl true
  def mount(socket) do
    {:ok,
      socket
      |> assign(:search_results, [])
      |> assign(:search_phrase, "")
      |> assign(:current_focus, -1)
    }
  end

  def search(""), do: []
  def search(search_phrase) do
    Products.list_products()
    |> Enum.map(& "#{&1.name} - $#{&1.price} ")
    |> Enum.filter(& matches?(&1, search_phrase))
    |> Enum.slice(0..4)
  end

  def matches?(first, second) do
    String.starts_with?(
      String.downcase(first), String.downcase(second)
    )
  end

  def format_search_result(search_result, search_phrase) do
    split_at = String.length(search_phrase)
    {selected, rest} = String.split_at(search_result, split_at)

    "<strong>#{selected}</strong>#{rest}"
  end

  @impl true
  def handle_event("search", %{"search_phrase" => search_phrase}, socket) do
    {:noreply,
      socket
      |> assign(:search_results, search(search_phrase))
      |> assign(:search_phrase, search_phrase)
    }
  end

  @impl true
  def handle_event("pick", %{"name" => search_phrase}, socket) do
    {:noreply,
      socket
      |> assign(:search_results, [])
      |> assign(:search_phrase, search_phrase)
    }
  end

  @impl true
  def handle_event("submit", _, socket), do: {:noreply, socket} # PREVENT FORM SUBMIT

  @impl true
  def handle_event("set-focus", %{"key" => "ArrowUp"}, socket) do
    current_focus =
      Enum.max([(socket.assigns.current_focus - 1), 0])

    {:noreply, assign(socket, current_focus: current_focus)}
  end

  @impl true
  def handle_event("set-focus", %{"key" => "ArrowDown"}, socket) do
    current_focus =
      Enum.min([(socket.assigns.current_focus + 1), (length(socket.assigns.search_results)-1)])

    {:noreply, assign(socket, current_focus: current_focus)}
  end

  @impl true
  def handle_event("set-focus", %{"key" => "Enter"}, socket) do
    case Enum.at(socket.assigns.search_results, socket.assigns.current_focus) do
      "" <> search_phrase -> handle_event("pick", %{"name" => search_phrase}, socket)
      _ -> {:noreply, socket}
    end
  end

  # FALLBACK FOR NON SUPPORTED KEY STROKES
  @impl true
  def handle_event("set-focus", _, socket), do: {:noreply, socket}
end