Skip to content

Commit 117c867

Browse files
committed
Fields.MultiSelect: add support for option groups
Additionally, this fixes the `:options` field option not working when passed a list rather than a function.
1 parent 784423f commit 117c867

3 files changed

Lines changed: 86 additions & 31 deletions

File tree

demo/lib/demo_web/live/user_live.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,11 @@ defmodule DemoWeb.UserLive do
197197
permissions: %{
198198
module: Backpex.Fields.MultiSelect,
199199
label: "Permissions",
200-
options: fn _assigns -> [{"Delete", "delete"}, {"Edit", "edit"}, {"Show", "show"}] end
200+
options: [
201+
{"Can access admin panel", "can_access_admin_panel"},
202+
{"Item actions", [{"Delete", "delete"}, {"Edit", "edit"}, {"Show", "show"}]},
203+
{"Other actions", [{"Can send email", "can_send_email"}]}
204+
]
201205
}
202206
]
203207
end

lib/backpex/fields/multi_select.ex

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
defmodule Backpex.Fields.MultiSelect do
22
@config_schema [
33
options: [
4-
doc: "List of options or function that receives the assigns.",
5-
type: {:or, [{:list, :any}, {:fun, 1}]},
4+
doc: "List of possibly grouped options or function that receives the assigns.",
5+
type: {:or, [{:list, :any}, {:map, :any, :any}, {:fun, 1}]},
66
required: true
77
],
88
prompt: [
@@ -66,18 +66,41 @@ defmodule Backpex.Fields.MultiSelect do
6666
%{assigns: %{field_options: field_options} = assigns} = socket
6767

6868
options =
69-
assigns
70-
|> field_options.options.()
69+
case Map.get(field_options, :options) do
70+
options when is_function(options) -> options.(assigns)
71+
options -> options
72+
end
73+
74+
options =
75+
options
7176
|> Enum.map(fn {label, value} ->
72-
{to_string(label), to_string(value)}
77+
case value do
78+
value when is_list(value) or is_map(value) ->
79+
{to_string(label), Enum.map(value, fn {lab, val} -> {to_string(lab), to_string(val)} end)}
80+
81+
value ->
82+
{to_string(label), to_string(value)}
83+
end
7384
end)
7485

7586
assign(socket, :options, options)
7687
end
7788

89+
defp flatten_options(options) do
90+
Enum.map(options, fn {_label, value} = option ->
91+
case value do
92+
value when is_list(value) or is_map(value) -> value
93+
_value -> option
94+
end
95+
end)
96+
|> List.flatten()
97+
end
98+
7899
defp assign_selected(socket) do
79100
%{assigns: %{type: type, options: options, item: item, name: name} = assigns} = socket
80101

102+
options = flatten_options(options)
103+
81104
selected_ids =
82105
if type == :form do
83106
case PhoenixForm.input_value(assigns.form, name) do
@@ -99,6 +122,8 @@ defmodule Backpex.Fields.MultiSelect do
99122
defp maybe_assign_form(%{assigns: %{type: :form} = assigns} = socket) do
100123
%{selected: selected, options: options} = assigns
101124

125+
options = flatten_options(options)
126+
102127
show_select_all = length(selected) != length(options)
103128

104129
socket
@@ -156,21 +181,20 @@ defmodule Backpex.Fields.MultiSelect do
156181

157182
@impl Phoenix.LiveComponent
158183
def handle_event("toggle-option", %{"id" => id}, socket) do
159-
%{assigns: %{selected: selected, options: options, field_options: field_options}} = socket
184+
%{assigns: %{selected: selected, options: options}} = socket
185+
186+
options = flatten_options(options)
160187

161-
selected_item = Enum.find(selected, fn {_label, value} -> value == id end)
188+
clicked_item = Enum.find(options, fn {_label, value} -> value == id end)
162189

163190
new_selected =
164-
if selected_item do
165-
Enum.reject(selected, fn {_label, value} -> value == id end)
191+
if clicked_item in selected do
192+
selected -- [clicked_item]
166193
else
167-
selected
168-
|> Enum.reverse()
169-
|> Kernel.then(&[Enum.find(options, fn {_label, value} -> value == id end) | &1])
170-
|> Enum.reverse()
194+
[clicked_item] ++ selected
171195
end
172196

173-
show_select_all = length(new_selected) != length(field_options.options.(socket.assigns))
197+
show_select_all = length(new_selected) != length(options)
174198

175199
socket
176200
|> assign(:selected, new_selected)
@@ -180,13 +204,12 @@ defmodule Backpex.Fields.MultiSelect do
180204

181205
@impl Phoenix.LiveComponent
182206
def handle_event("search", params, socket) do
183-
%{assigns: %{name: name, field_options: field_options} = assigns} = socket
207+
socket = assign_options(socket)
208+
%{assigns: %{name: name, options: options}} = socket
184209

185210
search_input = Map.get(params, "change[#{name}]_search")
186211

187-
options =
188-
field_options.options.(assigns)
189-
|> maybe_apply_search(search_input)
212+
options = apply_search(options, search_input)
190213

191214
socket
192215
|> assign(:options, options)
@@ -196,27 +219,37 @@ defmodule Backpex.Fields.MultiSelect do
196219

197220
@impl Phoenix.LiveComponent
198221
def handle_event("toggle-select-all", _params, socket) do
199-
%{assigns: %{field_options: field_options, show_select_all: show_select_all} = assigns} = socket
222+
%{assigns: %{options: options, show_select_all: show_select_all}} = socket
223+
224+
options = flatten_options(options)
200225

201-
new_selected = if show_select_all, do: field_options.options.(assigns), else: []
226+
new_selected = if show_select_all, do: options, else: []
202227

203228
socket
204229
|> assign(:selected, new_selected)
205230
|> assign(:show_select_all, not show_select_all)
206231
|> noreply()
207232
end
208233

209-
defp maybe_apply_search(options, search_input) do
210-
if String.trim(search_input) == "" do
211-
options
212-
else
213-
search_input_downcase = String.downcase(search_input)
234+
defp apply_search(options, search_input) do
235+
Enum.map(options, fn {label, value} -> filter_option(label, value, search_input) end) |> Enum.filter(& &1)
236+
end
214237

215-
Enum.filter(options, fn {label, _value} ->
216-
String.downcase(label)
217-
|> String.contains?(search_input_downcase)
218-
end)
219-
end
238+
defp filter_option(label, value, search_input) when is_list(value) or is_map(value) do
239+
search_input_downcase = String.downcase(search_input)
240+
241+
filtered =
242+
Enum.filter(value, fn {_label, _value} -> String.downcase(label) |> String.contains?(search_input_downcase) end)
243+
244+
if not Enum.empty?(filtered), do: {label, filtered}
245+
end
246+
247+
defp filter_option(label, _value, search_input) do
248+
search_input_downcase = String.downcase(search_input)
249+
250+
label
251+
|> String.downcase()
252+
|> String.contains?(search_input_downcase)
220253
end
221254

222255
defp prompt(assigns, field_options) do

lib/backpex/html/form.ex

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,24 @@ defmodule Backpex.HTML.Form do
435435
attr :selected, :list, required: true
436436
attr :event_target, :any, required: true
437437

438+
defp multi_select_option(%{value: value} = assigns) when is_list(value) or is_map(value) do
439+
~H"""
440+
<div class="not-first:mt-2">
441+
<span class="font-medium">{@label}</span>
442+
<div class="ml-4">
443+
<.multi_select_option
444+
:for={{lab, val} <- @value}
445+
label={lab}
446+
value={val}
447+
event_target={@event_target}
448+
field={@field}
449+
selected={@selected}
450+
/>
451+
</div>
452+
</div>
453+
"""
454+
end
455+
438456
defp multi_select_option(assigns) do
439457
~H"""
440458
<label

0 commit comments

Comments
 (0)