Current section

Files

Jump to
cinder lib cinder filters select.ex
Raw

lib/cinder/filters/select.ex

defmodule Cinder.Filters.Select do
@moduledoc """
Select dropdown filter implementation for Cinder tables.
Provides single-select filtering with configurable options and prompts.
"""
@behaviour Cinder.Filter
use Phoenix.Component
require Ash.Query
import Cinder.Filter
alias Phoenix.LiveView.JS
@impl true
def render(column, current_value, theme, assigns) do
filter_options = Map.get(column, :filter_options, [])
options = get_option(filter_options, :options, [])
prompt = get_option(filter_options, :prompt, "All #{column.label}")
current_value = current_value || ""
# Create a lookup map for labels
option_labels =
Enum.into(options, %{}, fn {label, value} -> {to_string(value), label} end)
# Create display text for the dropdown button
display_text =
if current_value == "" do
prompt
else
Map.get(option_labels, current_value, current_value)
end
# Sanitize field name for use in HTML attributes and CSS selectors
safe_field_name = Cinder.Filter.sanitized_field_name(column.field)
assigns = %{
column: column,
current_value: current_value,
options: options,
prompt: prompt,
theme: theme,
display_text: display_text,
dropdown_id: "select-dropdown-#{safe_field_name}",
target: Map.get(assigns, :target)
}
~H"""
<div class={@theme.filter_select_container_class} id={@dropdown_id}>
<!-- Main dropdown button that looks like a select input -->
<button
type="button"
class={[@theme.filter_select_input_class, "flex items-center justify-between"]}
{@theme.filter_select_input_data}
phx-click={JS.toggle(to: "##{@dropdown_id}-options")}
>
<span class={[if(@current_value == "", do: @theme.filter_placeholder_class, else: ""), "truncate"]}>{@display_text}</span>
<svg :if={@theme.filter_select_arrow_class != ""} class={@theme.filter_select_arrow_class} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<!-- Dropdown options (hidden by default) -->
<div
id={"#{@dropdown_id}-options"}
class={[@theme.filter_select_dropdown_class, "hidden"]}
{@theme.filter_select_dropdown_data}
phx-click-away={JS.hide(to: "##{@dropdown_id}-options")}
>
<label class={[@theme.filter_select_option_class, "flex items-center cursor-pointer"]} {@theme.filter_select_option_data}>
<input
type="radio"
name={field_name(@column.field)}
value=""
checked={@current_value == ""}
class="sr-only"
phx-click={JS.hide(to: "##{@dropdown_id}-options")}
/>
<span class={@theme.filter_select_label_class} {@theme.filter_select_label_data}>{@prompt}</span>
</label>
<label :for={{label, value} <- @options} class={[@theme.filter_select_option_class, "flex items-center cursor-pointer"]} {@theme.filter_select_option_data}>
<input
type="radio"
name={field_name(@column.field)}
value={to_string(value)}
checked={to_string(value) == @current_value}
class="sr-only"
phx-click={JS.hide(to: "##{@dropdown_id}-options")}
/>
<span class={@theme.filter_select_label_class} {@theme.filter_select_label_data}>{label}</span>
</label>
<div :if={Enum.empty?(@options)} class={@theme.filter_select_empty_class} {@theme.filter_select_empty_data}>
No options available
</div>
</div>
</div>
"""
end
@impl true
def process(raw_value, _column) when is_binary(raw_value) do
trimmed = String.trim(raw_value)
if trimmed == "" or trimmed == "all" do
nil
else
%{
type: :select,
value: trimmed,
operator: :equals
}
end
end
def process(_raw_value, _column), do: nil
@impl true
def validate(value) do
case value do
%{type: :select, value: val, operator: :equals} when is_binary(val) ->
val != ""
_ ->
false
end
end
@impl true
def default_options do
[
options: [],
prompt: nil
]
end
@impl true
def empty?(value) do
case value do
nil -> true
"" -> true
"all" -> true
%{value: ""} -> true
%{value: nil} -> true
%{value: "all"} -> true
_ -> false
end
end
@impl true
def build_query(query, field, filter_value) do
%{value: value} = filter_value
# Use the centralized helper which supports direct, relationship, and embedded fields
Cinder.Filter.Helpers.build_ash_filter(query, field, value, :equals)
end
@doc """
Handles selecting an option in the single-select filter.
This function should be called from the parent LiveView/LiveComponent
to handle the "select_option" event.
"""
def handle_select_option(socket, field, value) do
current_filters = Map.get(socket.assigns, :filters, %{})
updated_filters =
if value == "" do
Map.delete(current_filters, field)
else
Map.put(current_filters, field, value)
end
assign(socket, :filters, updated_filters)
end
end