defmodule PineUi.Select do @moduledoc """ Provides select components for dropdown selections. The Select module offers three main variants: - `basic/1` - Standard select dropdown - `grouped/1` - Select with option groups - `searchable/1` - Enhanced select with search functionality """ use Phoenix.Component @doc """ Renders a basic select dropdown component. ## Examples <.basic id="country" label="Country" options={[{"us", "United States"}, {"ca", "Canada"}]} selected="us" /> ## Options * `:id` - The ID for the select element (required) * `:name` - The name attribute (optional, defaults to ID) * `:label` - Label text (optional) * `:options` - List of {value, label} tuples for the options (required) * `:selected` - The currently selected value (optional) * `:placeholder` - Placeholder text for empty selection (optional) * `:hint` - Help text displayed below the select (optional) * `:error` - Error message displayed below the select (optional) * `:required` - Whether the field is required (optional, defaults to false) * `:disabled` - Whether the field is disabled (optional, defaults to false) * `:phx_change` - Phoenix change event name (optional) * `:class` - Additional CSS classes for the select element (optional) * `:container_class` - CSS classes for the container div (optional) """ def basic(assigns) do assigns = assigns |> assign_new(:container_class, fn -> "" end) |> assign_new(:class, fn -> "" end) |> assign_new(:name, fn -> assigns.id end) |> assign_new(:required, fn -> false end) |> assign_new(:disabled, fn -> false end) |> assign_new(:phx_change, fn -> nil end) |> assign_new(:selected, fn -> nil end) ~H"""
<%= if Map.get(assigns, :label, nil) do %> <% end %> <%= if Map.get(assigns, :hint, nil) do %>

<%= @hint %>

<% end %> <%= if Map.get(assigns, :error, nil) do %>

<%= @error %>

<% end %>
""" end @doc """ Renders a select dropdown with option groups. ## Examples <.grouped id="continent" label="Country" option_groups={[ {"North America", [{"us", "United States"}, {"ca", "Canada"}]}, {"Europe", [{"fr", "France"}, {"de", "Germany"}]} ]} /> ## Options * `:id` - The ID for the select element (required) * `:name` - The name attribute (optional, defaults to ID) * `:label` - Label text (optional) * `:option_groups` - List of {group_label, options} tuples (required) * `:selected` - The currently selected value (optional) * `:placeholder` - Placeholder text for empty selection (optional) * `:hint` - Help text displayed below the select (optional) * `:error` - Error message displayed below the select (optional) * `:required` - Whether the field is required (optional, defaults to false) * `:disabled` - Whether the field is disabled (optional, defaults to false) * `:phx_change` - Phoenix change event name (optional) * `:class` - Additional CSS classes for the select element (optional) * `:container_class` - CSS classes for the container div (optional) """ def grouped(assigns) do assigns = assigns |> assign_new(:container_class, fn -> "" end) |> assign_new(:class, fn -> "" end) |> assign_new(:name, fn -> assigns.id end) |> assign_new(:required, fn -> false end) |> assign_new(:disabled, fn -> false end) |> assign_new(:phx_change, fn -> nil end) |> assign_new(:selected, fn -> nil end) ~H"""
<%= if Map.get(assigns, :label, nil) do %> <% end %> <%= if Map.get(assigns, :hint, nil) do %>

<%= @hint %>

<% end %> <%= if Map.get(assigns, :error, nil) do %>

<%= @error %>

<% end %>
""" end @doc """ Renders a searchable select dropdown with filtering. ## Examples <.searchable id="country" label="Country" options={[{"us", "United States"}, {"ca", "Canada"}]} placeholder="Search..." /> ## Options * `:id` - The ID for the select element (required) * `:name` - The name attribute (optional, defaults to ID) * `:label` - Label text (optional) * `:options` - List of {value, label} tuples for the options (required) * `:selected` - The currently selected value (optional) * `:selected_label` - The label for the selected value (optional) * `:placeholder` - Placeholder text for the search (optional) * `:hint` - Help text displayed below the select (optional) * `:error` - Error message displayed below the select (optional) * `:required` - Whether the field is required (optional, defaults to false) * `:disabled` - Whether the field is disabled (optional, defaults to false) * `:phx_change` - Phoenix change event name (optional) * `:class` - Additional CSS classes for the select element (optional) * `:container_class` - CSS classes for the container div (optional) """ def searchable(assigns) do assigns = assigns |> assign_new(:container_class, fn -> "" end) |> assign_new(:class, fn -> "" end) |> assign_new(:name, fn -> assigns.id end) |> assign_new(:placeholder, fn -> "Select an option" end) |> assign_new(:required, fn -> false end) |> assign_new(:disabled, fn -> false end) |> assign_new(:phx_change, fn -> nil end) |> assign_new(:selected, fn -> nil end) |> assign_new(:selected_label, fn -> nil end) options_json = Jason.encode!(Enum.map(assigns.options, fn {value, label} -> %{value: value, label: label} end)) ~H"""
option.label.toLowerCase().includes(this.search.toLowerCase())); }, onEscape() { this.open = false; }, onArrowUp() { this.highlight = (this.highlight - 1 + this.filteredOptions().length) % this.filteredOptions().length; }, onArrowDown() { if (this.open) { this.highlight = (this.highlight + 1) % this.filteredOptions().length; } else { this.open = true; } }, onEnter() { if (this.highlight >= 0 && this.filteredOptions().length > 0) { const option = this.filteredOptions()[this.highlight]; this.selectOption(option.value, option.label); } } }"} x-init="() => { $watch('search', () => { highlight = -1; if (search === '') { selectedValue = ''; selected = ''; } }); }" class={@container_class} > <%= if Map.get(assigns, :label, nil) do %> <% end %>
"_search"} placeholder={@placeholder} x-model="search" x-on:focus="open = true" x-on:click="open = true" x-on:keydown.escape="onEscape()" x-on:keydown.arrow-up.prevent="onArrowUp()" x-on:keydown.arrow-down.prevent="onArrowDown()" x-on:keydown.enter.prevent="onEnter()" class={"block w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm #{@class}"} required={@required} disabled={@disabled} />
<%= if Map.get(assigns, :hint, nil) do %>

<%= @hint %>

<% end %> <%= if Map.get(assigns, :error, nil) do %>

<%= @error %>

<% end %>
""" end end