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""" <.error :for={msg <- @errors} class="mt-1">{msg} """ 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