Hello Fellas
So I was building as the title implies a search Select functionality for a project of mine and since I was utilizing the functionality in many places I tried to create a re-usable component.
Specs
- Should be able to provide a search select functionality and be able to work with on Forms
- Should be able to search both in memory as well as a database
my train of thought made my implement it as Phoenix.LiveComponent
defmodule MehungryWeb.SelectComponent do
@moduledoc """
A module to facilitate Select item widget of forms this module is tightly couppled with the Hooks.SelectComponent in the JS side of things
expects
items: %{id: String, name: String}
form: Form(The form that the widget is going to work on)
input_variable: Atom, and an input variable which is the Field of the Form that this widget is going to operate
"""
use MehungryWeb, :live_component
@impl true
def update(assigns, socket) do
label_function =
case Map.get(assigns, :label_function) do
nil ->
fn x -> x.name end
label_f ->
label_f
end
selected_items =
MehungryWeb.SelectComponentUtils.get_selected_items(
assigns.form.params,
assigns.input_variable,
label_function,
assigns
)
selected_items = Enum.filter(selected_items, fn x -> not is_nil(x) end)
items = Enum.map(assigns.items, fn x -> %{name: label_function.(x), id: x.id} end)
presenting_items = Enum.slice(items, 0..10)
socket =
socket
|> assign(:items, items)
|> assign(:presenting_items, presenting_items)
|> assign(:listing_open, Map.get(assigns, :initial_open, false))
|> assign(:selected_items, selected_items)
|> assign(:form, assigns.form)
|> assign(:input_variable, assigns.input_variable)
{:ok, socket}
end
def handle_event("handle-item-click", %{"id" => id}, socket) do
selected_item = Enum.find(socket.assigns.items, fn x -> x.id == id end)
selected_items = socket.assigns.selected_items ++ [selected_item]
socket =
socket
|> assign(:listing_open, false)
|> assign(:selected_items, selected_items)
items = Enum.map(selected_items, fn x -> x.id end)
# Pushes the message that the SelectComponent Hook is waiting for int he JS side
{:noreply,
push_event(
socket,
"selected_id" <> socket.assigns.input_variable,
%{id: items}
)}
end
def handle_event("handle-selected-item-click", %{"id" => id}, socket) do
{id, _} = Integer.parse(id)
selected_items = Enum.filter(socket.assigns.selected_items, fn x -> x.id != id end)
socket =
socket
|> assign(:selected_items, selected_items)
{:noreply, socket}
end
@impl true
def handle_event("validate", %{"search_input" => _search_string}, socket) do
socket =
socket
|> assign(:listing_open, true)
{:noreply, socket}
end
def handle_event("validate", _params, socket) do
{:noreply, socket}
end
def handle_event("window-blur", _, socket) do
socket =
socket
|> assign(:listing_open, false)
{:noreply, socket}
end
def handle_event("search_input_focus", _, socket) do
socket =
socket
|> assign(:listing_open, true)
{:noreply, socket}
end
def handle_event("toggle-listing", _, socket) do
socket =
socket
|> assign(:listing_open, !socket.assigns.listing_open)
{:noreply, socket}
end
def handle_event("close-listing", _, socket) do
socket =
socket
|> assign(:listing_open, false)
{:noreply, socket}
end
#----------------------------------------------------------------------------------Render --------------------------------------------------------------------------------------------->
@impl true
def render(assigns) do
~H"""
<div
class="col-span-2"
data-reference-id={@input_variable}
phx-hook="SelectComponent"
id={"select_component"<> @input_variable }
>
<.input
field={@form[String.to_atom(@input_variable)]}
type="select"
options={[]}
style="visibility: hidden; position: absolute;"
/>
<div
class="w-full max-w-lg "
phx-click-away="close-listing"
phx-target={@myself}
id={"select-item"<> @input_variable }
>
<!-- Start Component -->
<div class="relative">
<.list_selected
selected_items={@selected_items}
myself={@myself}
form={@form}
input_variable={@input_variable}
/>
<.list_search_result myself={@myself} listing_open={@listing_open} items={@items} />
<.arrow_down_svg myself={@myself} />
</div>
<!-- End Component -->
</div>
</div>
"""
end
# ------------------------------------------------------------------------------- List Selected ------------------------------------------------------------------------------------
def list_selected(assigns) do
~H"""
<div class="flex items-center justify-between px-1 border border-2 rounded-md relative pr-8 bg-white">
<ul class="flex flex-wrap items-center w-full">
<!-- Tags (Selected) -->
<%= for x <- @selected_items do %>
<.selected_item id={Map.get(x, :id)} myself={@myself} name={x.name} />
<% end %>
<!-- Search Input -->
<%= if Enum.empty?(@selected_items) do %>
<.input_search myself={@myself} form={@form} input_variable={@input_variable} />
<% end %>
<!-- Arrow Icon -->
</ul>
</div>
"""
end
defp selected_item(assigns) do
~H"""
<div>
<li
phx-click="handle-selected-item-click"
phx-value-id={@id}
phx-target={@myself}
tabindex="0"
class="relative m-1 px-2 py-1.5 border rounded-md cursor-pointer hover:bg-gray-100 after:content-['x'] after:ml-1.5 after:text-red-300 outline-none focus:outline-none ring-0 focus:ring-2 focus:ring-amber-300 ring-inset transition-all"
>
<%= @name %>
</li>
</div>
"""
end
defp input_search(assigns) do
~H"""
<.input
phx-focus="search_input_focus"
phx-target={@myself}
field={
@form[
String.to_atom("search_input" <> @input_variable)
]
}
type="text"
class="test flex-grow py-2 px-2 mx-1 my-1.5 outline-none focus:outline-none focus:ring-amber-300 focus:ring-2 ring-inset transition-all rounded-md w-full"
/>
"""
end
#----------------------------------------------------------------------------------------------------- END List Selected ------------------------------------------------------
#----------------------------------------------------------------------------------------------------- Search Result -----------------------------------------------------------
defp list_search_result(assigns) do
~H"""
<div>
<ul class="w-full list-none border-t-0 rounded-md focus:outline-none overflow-y-auto outline-none focus:outline-none bg-white absolute left-0 bottom-100 max-h-56 bg-white z-50">
<%= if @listing_open do %>
<%= for x <- @items do %>
<!-- Item Element -->
<.option_item myself={@myself} x={x} />
<!-- Empty Text -->
<div>
<li class="cursor-pointer px-2 py-2 text-gray-400"></li>
</div>
<% end %>
<% end %>
</ul>
</div>
"""
end
defp option_item(assigns) do
~H"""
<div class="relative z-50">
<div class="bg-white">
<li
class="hover:bg-amber-200 cursor-pointer px-2 py-2 bg-white"
phx-click="handle-item-click"
phx-value-id={@x.id}
id={@x.id}
phx-target={@myself}
>
<%= @x.name %>
</li>
</div>
</div>
"""
end
#----------------------------------------------------------------------------------------------------- Search Result -----------------------------------------------------------
defp arrow_down_svg(assigns) do
~H"""
<svg
phx-click="toggle-listing"
phx-target={@myself}
width="24"
height="24"
stroke-width="0"
fill="#ccc"
class="absolute right-2 top-1/2 -translate-y-1/2 cursor-pointer focus:outline-none"
tabindex="-1"
>
<path d="M12 17.414 3.293 8.707l1.414-1.414L12 14.586l7.293-7.293 1.414 1.414L12 17.414z" />
</svg>
"""
end
Recently trying to refactor/improve the code I stabled upon this in the documentation of the Phoenix.LiveComponent
You should also avoid using live components to provide abstract DOM components. As a guideline, a good LiveComponent encapsulates application concerns and not DOM functionality
And that make me wonder how would you fellas would go about implementing such a thing ?
Is this like a corner case where it’s both dom/functionality along with app concerns because also of the db-connection aspect that justifies utilizing LiveComponents like so ?