Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion demo/lib/demo/address.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 5 additions & 2 deletions demo/lib/demo_web/live/address_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 5 additions & 1 deletion demo/lib/demo_web/live/user_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
109 changes: 66 additions & 43 deletions lib/backpex/fields/multi_select.ex
Original file line number Diff line number Diff line change
@@ -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: [
Expand Down Expand Up @@ -66,50 +66,64 @@ 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

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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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

Expand Down
13 changes: 11 additions & 2 deletions lib/backpex/fields/select.ex
Original file line number Diff line number Diff line change
@@ -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: [
Expand Down Expand Up @@ -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
Expand Down
72 changes: 54 additions & 18 deletions lib/backpex/html/form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -282,25 +282,14 @@ defmodule Backpex.HTML.Form do
<input type="hidden" name={@field.name} value="" />

<div class="my-2 w-full">
<label
<.multiselect_option
:for={{label, value} <- @options}
class="mt-2 flex space-x-2"
phx-click="toggle-option"
phx-value-id={value}
phx-target={@event_target}
>
<input
type="checkbox"
name={@field.name<> "[]"}
class="checkbox checkbox-sm checkbox-primary"
checked={selected?(value, @selected)}
checked_value={value}
value={value}
/>
<span class="text-md cursor-pointer">
{label}
</span>
</label>
label={label}
value={value}
event_target={@event_target}
field={@field}
selected={@selected}
/>
</div>

<button
Expand All @@ -320,6 +309,53 @@ defmodule Backpex.HTML.Form do
"""
end

attr :event_target, :any, required: true, doc: "the target that handles the events of this component"
attr :field, :any, required: true, doc: "form field the select should be for"
attr :selected, :list, required: true, doc: "the selected values"
attr :label, :string, required: true
attr :value, :string, required: true

defp multiselect_option(%{value: value} = assigns) when is_list(value) or is_map(value) do
~H"""
<div class="not-first:mt-2">
<span class="font-medium">{@label}</span>
<div class="ml-4">
<.multiselect_option
:for={{lab, val} <- @value}
label={lab}
value={val}
event_target={@event_target}
field={@field}
selected={@selected}
/>
</div>
</div>
"""
end

defp multiselect_option(assigns) do
~H"""
<label
class="mt-2 flex space-x-2"
phx-click="toggle-option"
phx-value-id={@value}
phx-target={@event_target}
>
<input
type="checkbox"
name={@field.name<> "[]"}
class="checkbox checkbox-sm checkbox-primary"
checked={selected?(@value, @selected)}
checked_value={@value}
value={@value}
/>
<span class="text-md cursor-pointer">
{@label}
</span>
</label>
"""
end

def form_errors?(false, _form), do: false
def form_errors?(true = _show_errors, form), do: form.errors != []

Expand Down