Current section
Files
Jump to
Current section
Files
lib/backpex/filters/select.ex
defmodule Backpex.Filters.Select do
@moduledoc """
The select filter renders a select box for the implemented `options/1` and `prompt/0` callbacks. The `prompt/0` callback defines the key for the `nil` value added as first option.
See the following example for an implementation of an event status filter.
defmodule MyAppWeb.Filters.EventStatusSelect do
use Backpex.Filters.Select
@impl Backpex.Filter
def label, do: "Event status"
@impl Backpex.Filters.Select
def prompt, do: "Select an option..."
@impl Backpex.Filters.Select
def options(_assigns), do: [
{"Open", :open},
{"Close", :close},
]
@impl Backpex.Filter
def query(query, attribute, value) do
where(query, [x], field(x, ^attribute) == ^value)
end
end
> #### `use Backpex.Filters.Select` {: .info}
>
> When you `use Backpex.Filters.Select`, the `Backpex.Filters.Select` module will set `@behavior Backpex.Filters.Select`.
> In addition it will add a `render` and `render_form` function in order to display the corresponding filter.
"""
use BackpexWeb, :filter
@doc """
The select's default option.
"""
@callback prompt :: String.t() | atom()
@doc """
The list of options for the select filter.
"""
@callback options(assigns :: map()) :: [{String.t() | atom(), String.t() | atom()}]
defmacro __using__(_opts) do
quote do
@behaviour Backpex.Filters.Select
use BackpexWeb, :filter
use Backpex.Filter
alias Backpex.Filters.Select, as: SelectFilter
@impl Backpex.Filter
def type(_assigns), do: :string
@impl Backpex.Filter
def changeset(changeset, field, assigns) do
SelectFilter.changeset(changeset, field, options(assigns))
end
@impl Backpex.Filter
defdelegate query(query, attribute, value, assigns), to: SelectFilter
@impl Backpex.Filter
def render(assigns) do
assigns = assign(assigns, :options, options(assigns))
SelectFilter.render(assigns)
end
@impl Backpex.Filter
def render_form(assigns) do
assigns =
assigns
|> assign(:options, options(assigns))
|> assign(:prompt, prompt())
SelectFilter.render_form(assigns)
end
defoverridable type: 1, changeset: 3, query: 4, render: 1, render_form: 1
end
end
attr :value, :any, required: true
attr :options, :list, required: true
def render(assigns) do
assigns = assign(assigns, :label, option_value_to_label(assigns.options, assigns.value))
~H"""
{@label}
"""
end
attr :form, :any, required: true
attr :field, :atom, required: true
attr :value, :any, required: true
attr :options, :list, required: true
attr :prompt, :string, required: true
attr :errors, :list, default: []
def render_form(assigns) do
~H"""
<select
id={@form[@field].id}
name={@form[@field].name}
class={[
"select select-sm",
@errors != [] && "select-error bg-error/10"
]}
aria-label={@prompt}
>
<button>
<selectedcontent></selectedcontent>
</button>
<option value="">{@prompt}</option>
{Phoenix.HTML.Form.options_for_select(@options, selected(@value))}
</select>
<.error :for={msg <- @errors} class="mt-1">{msg}</.error>
"""
end
@doc """
Converts empty string to nil, returns other values as-is.
## Examples
iex> Backpex.Filters.Select.selected("")
nil
iex> Backpex.Filters.Select.selected("open")
"open"
iex> Backpex.Filters.Select.selected(:open)
:open
iex> Backpex.Filters.Select.selected(1)
1
"""
def selected(""), do: nil
def selected(value), do: value
@doc """
Validates that the selected value exists in the options list.
Returns the changeset unchanged if the value is valid, or adds an error if not found in options.
Empty string is allowed as it represents the "no selection" state.
"""
def changeset(changeset, field, options) do
valid_values = ["" | Enum.map(options, fn {_label, value} -> to_string(value) end)]
Ecto.Changeset.validate_inclusion(changeset, field, valid_values, message: "is not a valid option")
end
def query(query, attribute, value, _assigns) do
where(query, [x], field(x, ^attribute) == ^value)
end
@doc """
Finds the label for a given option value.
Returns nil if the value is not found.
## Examples
iex> options = [{"Open", :open}, {"Closed", :closed}]
iex> Backpex.Filters.Select.option_value_to_label(options, "open")
"Open"
iex> options = [{"Open", :open}]
iex> Backpex.Filters.Select.option_value_to_label(options, :open)
"Open"
iex> options = [{"Open", :open}]
iex> Backpex.Filters.Select.option_value_to_label(options, "unknown")
nil
iex> options = [{"Active", "active"}, {"Inactive", "inactive"}]
iex> Backpex.Filters.Select.option_value_to_label(options, "active")
"Active"
iex> options = [{"Active", "active"}]
iex> Backpex.Filters.Select.option_value_to_label(options, :active)
"Active"
iex> Backpex.Filters.Select.option_value_to_label([], "value")
nil
"""
def option_value_to_label(options, value) do
Enum.find_value(options, fn {option_label, option_value} ->
if to_string(option_value) == to_string(value), do: option_label
end)
end
end