diff --git a/demo/lib/demo/address.ex b/demo/lib/demo/address.ex index 1cc12d5dc..b07250278 100644 --- a/demo/lib/demo/address.ex +++ b/demo/lib/demo/address.ex @@ -11,7 +11,7 @@ defmodule Demo.Address do field :street, :string field :zip, :string field :city, :string - field :country, Ecto.Enum, values: [:de, :at, :ch] + field :country, Ecto.Enum, values: [:de, :at, :ch, :us, :ca] field :full_address, :string, virtual: true timestamps() diff --git a/demo/lib/demo_web/live/address_live.ex b/demo/lib/demo_web/live/address_live.ex index d83248a2a..dff473694 100644 --- a/demo/lib/demo_web/live/address_live.ex +++ b/demo/lib/demo_web/live/address_live.ex @@ -36,8 +36,11 @@ defmodule DemoWeb.AddressLive do country: %{ module: Backpex.Fields.Select, label: "Country", - options: [Germany: "de", Austria: "at", Switzerland: "ch"] - } + options: %{ + "Europe" => [Germany: "de", Austria: "at", Switzerland: "ch"], + "North America" => [USA: "us", Canada: "ca"] + } + }, ] end end diff --git a/demo/lib/demo_web/live/user_live.ex b/demo/lib/demo_web/live/user_live.ex index 463de73d1..4a0c1e905 100644 --- a/demo/lib/demo_web/live/user_live.ex +++ b/demo/lib/demo_web/live/user_live.ex @@ -196,7 +196,11 @@ defmodule DemoWeb.UserLive do permissions: %{ module: Backpex.Fields.MultiSelect, label: "Permissions", - options: fn _assigns -> [{"Delete", "delete"}, {"Edit", "edit"}, {"Show", "show"}] end + options: [ + {"Foo", "foo"}, + {"Item actions", [{"Delete", "delete"}, {"Edit", "edit"}, {"Show", "show"}]}, + {"Other actions", [{"Can send email", "can_send_email"}]} + ] } ] end diff --git a/lib/backpex/fields/multi_select.ex b/lib/backpex/fields/multi_select.ex index 441fbf2c8..19939073c 100644 --- a/lib/backpex/fields/multi_select.ex +++ b/lib/backpex/fields/multi_select.ex @@ -1,8 +1,8 @@ defmodule Backpex.Fields.MultiSelect do @config_schema [ options: [ - doc: "List of options or function that receives the assigns.", - type: {:or, [{:list, :any}, {:fun, 1}]}, + doc: "List of possibly grouped options or function that receives the assigns.", + type: {:or, [{:list, :any}, {:map, :any, :any}, {:fun, 1}]}, required: true ], prompt: [ @@ -66,43 +66,55 @@ defmodule Backpex.Fields.MultiSelect do %{assigns: %{field_options: field_options} = assigns} = socket options = - assigns - |> field_options.options.() + case Map.get(field_options, :options) do + options when is_function(options) -> options.(assigns) + options -> options + end + + options = + options |> Enum.map(fn {label, value} -> - {to_string(label), to_string(value)} + case value do + value when is_list(value) or is_map(value) -> + {to_string(label), Enum.map(value, fn {lab, val} -> {to_string(lab), to_string(val)} end)} + + _ -> + {to_string(label), to_string(value)} + end end) assign(socket, :options, options) end + defp flatten_options(options) do + Enum.map(options, fn {_label, value} = option -> + case value do + value when is_list(value) or is_map(value) -> value + _ -> option + end + end) + |> List.flatten() + end + defp assign_selected(socket) do %{assigns: %{type: type, options: options, item: item, name: name} = assigns} = socket + options = flatten_options(options) + selected_ids = if type == :form do - values = - case PhoenixForm.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) + case PhoenixForm.input_value(assigns.form, name) do + value when is_binary(value) -> [value] + value when is_list(value) -> value + _value -> [] + end else - value = Map.get(item, name) - - if value, do: value, else: [] + Map.get(item, name, []) || [] end - selected = - Enum.reduce(options, [], fn {_label, value} = option, acc -> - if value in selected_ids do - [option | acc] - else - acc - end - end) - |> Enum.reverse() + selected_ids = Enum.map(selected_ids, &to_string/1) + + selected = Enum.filter(options, fn {_label, value} -> value in selected_ids end) assign(socket, :selected, selected) end @@ -110,6 +122,8 @@ defmodule Backpex.Fields.MultiSelect do defp maybe_assign_form(%{assigns: %{type: :form} = assigns} = socket) do %{selected: selected, options: options} = assigns + options = flatten_options(options) + show_select_all = length(selected) != length(options) socket @@ -167,21 +181,20 @@ defmodule Backpex.Fields.MultiSelect do @impl Phoenix.LiveComponent def handle_event("toggle-option", %{"id" => id}, socket) do - %{assigns: %{selected: selected, options: options, field_options: field_options}} = socket + %{assigns: %{selected: selected, options: options}} = socket + + options = flatten_options(options) - selected_item = Enum.find(selected, fn {_label, value} -> value == id end) + clicked_item = Enum.find(options, fn {_label, value} -> value == id end) new_selected = - if selected_item do - Enum.reject(selected, fn {_label, value} -> value == id end) + if clicked_item in selected do + selected -- [clicked_item] else - selected - |> Enum.reverse() - |> Kernel.then(&[Enum.find(options, fn {_label, value} -> value == id end) | &1]) - |> Enum.reverse() + selected ++ [clicked_item] end - show_select_all = length(new_selected) != length(field_options.options.(socket.assigns)) + show_select_all = length(new_selected) != length(options) socket |> assign(:selected, new_selected) @@ -191,13 +204,12 @@ defmodule Backpex.Fields.MultiSelect do @impl Phoenix.LiveComponent def handle_event("search", params, socket) do - %{assigns: %{name: name, field_options: field_options} = assigns} = socket + socket = assign_options(socket) + %{assigns: %{name: name, options: options} = assigns} = socket search_input = Map.get(params, "change[#{name}]_search") - options = - field_options.options.(assigns) - |> maybe_apply_search(search_input) + options = maybe_apply_search(options, search_input) socket |> assign(:options, options) @@ -207,9 +219,11 @@ defmodule Backpex.Fields.MultiSelect do @impl Phoenix.LiveComponent def handle_event("toggle-select-all", _params, socket) do - %{assigns: %{field_options: field_options, show_select_all: show_select_all} = assigns} = socket + %{assigns: %{options: options, show_select_all: show_select_all}} = socket + + options = flatten_options(options) - new_selected = if show_select_all, do: field_options.options.(assigns), else: [] + new_selected = if show_select_all, do: options, else: [] socket |> assign(:selected, new_selected) @@ -223,10 +237,19 @@ defmodule Backpex.Fields.MultiSelect do else search_input_downcase = String.downcase(search_input) - Enum.filter(options, fn {label, _value} -> - String.downcase(label) - |> String.contains?(search_input_downcase) + keep? = fn label -> String.downcase(label) |> String.contains?(search_input_downcase) end + + Enum.map(options, fn {label, value} -> + case value do + value when is_list(value) or is_map(value) -> + filtered = Enum.filter(value, fn {_lab, val} -> keep?.(val) end) + if not Enum.empty?(filtered), do: {label, filtered} + + _ -> + if keep?.(label), do: {label, value} + end end) + |> Enum.filter(& &1) end end diff --git a/lib/backpex/fields/select.ex b/lib/backpex/fields/select.ex index 6366bb712..3782db934 100644 --- a/lib/backpex/fields/select.ex +++ b/lib/backpex/fields/select.ex @@ -1,8 +1,8 @@ defmodule Backpex.Fields.Select do @config_schema [ options: [ - doc: "List of options or function that receives the assigns.", - type: {:or, [{:list, :any}, {:fun, 1}]}, + doc: "List of possibly grouped options or function that receives the assigns.", + type: {:or, [{:list, :any}, {:map, :any, :any}, {:fun, 1}]}, required: true ], prompt: [ @@ -127,6 +127,15 @@ defmodule Backpex.Fields.Select do end defp get_label(value, options) do + options = + Enum.map(options, fn {_label, value} = option -> + case value do + value when is_list(value) or is_map(value) -> value + _ -> option + end + end) + |> List.flatten() + case Enum.find(options, fn option -> value?(option, value) end) do nil -> value {label, _value} -> label diff --git a/lib/backpex/html/form.ex b/lib/backpex/html/form.ex index fa07e377e..93a293977 100644 --- a/lib/backpex/html/form.ex +++ b/lib/backpex/html/form.ex @@ -282,25 +282,14 @@ defmodule Backpex.HTML.Form do
- + label={label} + value={value} + event_target={@event_target} + field={@field} + selected={@selected} + />