diff --git a/demo/lib/demo_web/live/user_live.ex b/demo/lib/demo_web/live/user_live.ex index 871831e24..2df765a2d 100644 --- a/demo/lib/demo_web/live/user_live.ex +++ b/demo/lib/demo_web/live/user_live.ex @@ -193,9 +193,10 @@ defmodule DemoWeb.UserLive do ] }, permissions: %{ - module: Backpex.Fields.MultiSelect, + module: Backpex.Fields.CheckboxGroup, label: "Permissions", - options: fn _assigns -> [{"Delete", "delete"}, {"Edit", "edit"}, {"Show", "show"}] end + options: fn _assigns -> [{"Delete", "delete"}, {"Edit", "edit"}, {"Show", "show"}] end, + columns: 3 } ] end diff --git a/lib/backpex/fields/checkbox_group.ex b/lib/backpex/fields/checkbox_group.ex new file mode 100644 index 000000000..c61c3487c --- /dev/null +++ b/lib/backpex/fields/checkbox_group.ex @@ -0,0 +1,281 @@ +defmodule Backpex.Fields.CheckboxGroup do + @config_schema [ + options: [ + doc: "List of options or function that receives the assigns.", + type: {:or, [{:list, :any}, {:fun, 1}]}, + required: true + ], + columns: [ + doc: "Number of columns to display checkboxes (1-4). Defaults to 2.", + type: :integer, + default: 2 + ], + prompt: [ + doc: "The text to be displayed when no option is selected or function that receives the assigns.", + type: {:or, [:string, {:fun, 1}]} + ], + not_found_text: [ + doc: """ + The text to be displayed when no options are found. + + The default value is `"No options found"`. + """, + type: :string + ] + ] + + @moduledoc """ + A field for handling multiple selections with checkboxes. + + ## Field-specific options + + See `Backpex.Field` for general field options. + + #{NimbleOptions.docs(@config_schema)} + + ## Example + + @impl Backpex.LiveResource + def fields do + [ + categories: %{ + module: Backpex.Fields.CheckboxGroup, + label: "Categories", + options: fn _assigns -> + Repo.all(Category) + |> Enum.map(fn category -> {category.name, category.id} end) + end, + columns: 2 + } + ] + end + """ + use Backpex.Field, config_schema: @config_schema + alias Backpex.HTML.Form + require Backpex + + @impl Phoenix.LiveComponent + def update(assigns, socket) do + socket + |> Phoenix.Component.assign(assigns) + |> Phoenix.Component.assign( + :not_found_text, + assigns.field_options[:not_found_text] || Backpex.__("No options found", assigns.live_resource) + ) + |> Phoenix.Component.assign(:prompt, prompt(assigns, assigns.field_options)) + |> assign_options() + |> assign_selected() + |> ok() + end + + defp prompt(assigns, field_options) do + case Map.get(field_options, :prompt) do + nil -> Backpex.__("Select options...", assigns.live_resource) + prompt when is_function(prompt) -> prompt.(assigns) + prompt -> prompt + end + end + + defp assign_options(socket) do + %{assigns: %{field_options: field_options} = assigns} = socket + + options = + case field_options[:options] do + options_fun when is_function(options_fun, 1) -> options_fun.(assigns) + options when is_list(options) -> options + end + |> Enum.map(fn {label, value} -> + {to_string(label), to_string(value)} + end) + + Phoenix.Component.assign(socket, :options, options) + end + + defp assign_selected(socket) do + %{assigns: %{type: type, options: options, item: item, name: name} = assigns} = socket + + selected_ids = + if type == :form do + values = + case Phoenix.HTML.Form.input_value(assigns.form, name) do + value when is_binary(value) -> [value] + value when is_list(value) -> value + _value -> [] + end + + Enum.map(values, &to_string/1) + else + value = Map.get(item, name) + + if value, do: value, else: [] + end + + selected = + Enum.reduce(options, [], fn {_label, value} = option, acc -> + if value in selected_ids do + [option | acc] + else + acc + end + end) + |> Enum.reverse() + + Phoenix.Component.assign(socket, :selected, selected) + end + + defp get_column_class(columns) when is_integer(columns) and columns >= 1 and columns <= 4 do + case columns do + 1 -> "" + 2 -> "grid-cols-2" + 3 -> "grid-cols-3" + 4 -> "grid-cols-4" + end + end + defp get_column_class(_), do: "" + + @impl Backpex.Field + def render_value(assigns) do + selected_labels = Enum.map(assigns.selected, fn {label, _value} = _option -> label end) + + assigns = Phoenix.Component.assign(assigns, :selected_labels, selected_labels) + + ~H""" +
+ <%= Backpex.HTML.pretty_value(item) %> +
+ <%= if index < length(@selected_labels) - 1 do %> + , + <% end %> + <% end %> +