solop

solop

Autocomplete input component w/o Javascript

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..

Where Next?

Popular in Discussions Top

ben-pr-p
In general I’ve been sticking to this community style guide GitHub - christopheradams/elixir_style_guide: A community driven style guide ...
New
blackode
Elixir Upgrading is so Simple in Ubuntu and It worked for me Ubuntu 16.04 git clone https://github.com/elixir-lang/elixir.git cd elixir...
New
laiboonh
Hi all, I am trying to convince my team to use liveview over the current react. What are some of the points where one should consider us...
New
thojanssens1
It would be nice to be able to define a redirect from one route to another from the router.ex file. E.g.: redirect "/", UserController, ...
New
mmport80
I have put far too much effort into Dialyzer over the last year or so - and basically - I doubt it’s worth the effort. It’s not as easy ...
New
AstonJ
Are there any Elixir or Erlang libraries that help with this? I’ve been thinking how streaming services like twitch have exploded recentl...
New
Crowdhailer
I’ve been hearing much about the new formatter and it’s something I have been keen to try. I find examples buy far the most illuminating...
248 19365 150
New
PragTob
Hey everyone, this has been brewing in my head some time and it came up again while reading Adopting Elixir. GenServers, supervisors et...
New
opsb
We’re considering our architecture from a viewpoint of scaling our traffic heavily over the next 6 months. Our current deployment is runn...
New
axelson
Decided against including more info in the title, but the gist is that Plataformatec sponsored projects will continue with the assets bei...
New

Other popular topics Top

hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement