Skip to content

Commit

Permalink
Add tags_mode option and multiple_select variant
Browse files Browse the repository at this point in the history
The multiple select option for the new tags_mode makes it possible to select
multiple options at once without needing to search or force the dropdown
to appear. It is a kind of hybrid of the search variant and a normal
multi select input input.
  • Loading branch information
ringvold committed Aug 16, 2024
1 parent d1a0f52 commit f4d4e24
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 7 deletions.
5 changes: 5 additions & 0 deletions lib/live_select.ex
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,11 @@ defmodule LiveSelect do
default: Component.default_opts()[:mode],
doc: "either `:single` (for single selection), or `:tags` (for multiple selection using tags)"

attr :tags_mode, :atom,
values: [:default, :multiple_select],
default: Component.default_opts()[:tags_mode],
doc: "whether to use default tags mode or multi-select tags mode. Multi-select mode enables selecting multiple options without the options dropdown being hidden"

attr :options, :list,
doc:
~s(initial available options to select from. Note that, after the initial rendering of the component, options can only be updated using `Phoenix.LiveView.send_update/3` - See the "Options" section for details)
Expand Down
65 changes: 61 additions & 4 deletions lib/live_select/component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ defmodule LiveSelect.Component do
text_input_extra_class: nil,
text_input_selected_class: nil,
update_min_len: 1,
value: nil
value: nil,
tags_mode: :default
]

@styles [
Expand Down Expand Up @@ -427,6 +428,26 @@ defmodule LiveSelect.Component do

defp maybe_select(%{assigns: %{active_option: -1}} = socket, _extra_params), do: socket

defp maybe_select(
%{assigns: %{active_option: active_option, options: options, selection: selection}} =

Check warning on line 432 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.14.0 otp 25.0

variable "options" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 432 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.14.0 otp 25.0

variable "options" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 432 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.15.0 otp 26.0

variable "options" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 432 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.15.0 otp 26.0

variable "options" is unused (if the variable is not meant to be used, prefix it with an underscore)
socket,
extra_params
)
when active_option >= 0 do
option = Enum.at(socket.assigns.options, active_option)

if already_selected?(option, selection) do
pos = selection_index(option, selection)
unselect(socket, pos)
else
select(socket, Enum.at(socket.assigns.options, socket.assigns.active_option), extra_params)
end
end

defp selection_index(option, selection) do
Enum.find_index(selection, fn %{label: label} -> label == option.label end)
end

defp maybe_select(socket, extra_params) do

Check warning on line 451 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.14.0 otp 25.0

clauses with the same name and arity (number of arguments) should be grouped together, "defp maybe_select/2" was previously defined (lib/live_select/component.ex:384)

Check warning on line 451 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.14.0 otp 25.0

clauses with the same name and arity (number of arguments) should be grouped together, "defp maybe_select/2" was previously defined (lib/live_select/component.ex:384)

Check warning on line 451 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.15.0 otp 26.0

clauses with the same name and arity (number of arguments) should be grouped together, "defp maybe_select/2" was previously defined (lib/live_select/component.ex:384)

Check warning on line 451 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.15.0 otp 26.0

clauses with the same name and arity (number of arguments) should be grouped together, "defp maybe_select/2" was previously defined (lib/live_select/component.ex:384)
select(socket, Enum.at(socket.assigns.options, socket.assigns.active_option), extra_params)
end
Expand All @@ -443,9 +464,10 @@ defmodule LiveSelect.Component do

socket
|> assign(
active_option: -1,
active_option:
if(multi_select_mode?(socket), do: socket.assigns.active_option, else: -1),
selection: selection,
hide_dropdown: true
hide_dropdown: not multi_select_mode?(socket)
)
|> maybe_save_selection()
|> client_select(Map.merge(%{input_event: true}, extra_params))
Expand Down Expand Up @@ -662,10 +684,18 @@ defmodule LiveSelect.Component do

defp encode(value), do: Jason.encode!(value)

defp already_selected?(option, selection) do
def already_selected?(idx, selection) when is_integer(idx) do
Enum.at(selection, idx) != nil
end

def already_selected?(option, selection) do
option.label in Enum.map(selection, & &1.label)
end

defp multi_select_mode?(socket) do
socket.assigns.mode == :tags && socket.assigns.tags_mode == :multiple_select
end

defp next_selectable(%{
selection: selection,
active_option: active_option,
Expand All @@ -674,6 +704,19 @@ defmodule LiveSelect.Component do
when max_selectable > 0 and length(selection) >= max_selectable,
do: active_option

defp next_selectable(%{
options: options,
active_option: active_option,
selection: selection,

Check warning on line 710 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.14.0 otp 25.0

variable "selection" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 710 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.14.0 otp 25.0

variable "selection" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 710 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.15.0 otp 26.0

variable "selection" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 710 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.15.0 otp 26.0

variable "selection" is unused (if the variable is not meant to be used, prefix it with an underscore)
tags_mode: :multiple_select
}) do
options
|> Enum.with_index()
|> Enum.reject(fn {opt, _} -> active_option == opt end)
|> Enum.map(fn {_, idx} -> idx end)
|> Enum.find(active_option, &(&1 > active_option))
end

defp next_selectable(%{options: options, active_option: active_option, selection: selection}) do
options
|> Enum.with_index()
Expand All @@ -690,6 +733,20 @@ defmodule LiveSelect.Component do
when max_selectable > 0 and length(selection) >= max_selectable,
do: active_option

defp prev_selectable(%{
options: options,
active_option: active_option,
selection: selection,

Check warning on line 739 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.14.0 otp 25.0

variable "selection" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 739 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.14.0 otp 25.0

variable "selection" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 739 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.15.0 otp 26.0

variable "selection" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 739 in lib/live_select/component.ex

View workflow job for this annotation

GitHub Actions / Build and test with elixir 1.15.0 otp 26.0

variable "selection" is unused (if the variable is not meant to be used, prefix it with an underscore)
tags_mode: :multiple_select
}) do
options
|> Enum.with_index()
|> Enum.reverse()
|> Enum.reject(fn {opt, _} -> active_option == opt end)
|> Enum.map(fn {_, idx} -> idx end)
|> Enum.find(active_option, &(&1 < active_option || active_option == -1))
end

defp prev_selectable(%{options: options, active_option: active_option, selection: selection}) do
options
|> Enum.with_index()
Expand Down
4 changes: 2 additions & 2 deletions lib/live_select/component.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,12 @@
)
)
}
data-idx={unless already_selected?(option, @selection), do: idx}
data-idx={idx}
>
<%= if @option == [] do %>
<%= option.label %>
<% else %>
<%= render_slot(@option, option) %>
<%= render_slot(@option, {option, already_selected?(option, @selection)}) %>
<% end %>
</div>
</li>
Expand Down
24 changes: 23 additions & 1 deletion lib/support/live_select_web/live/showcase_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ defmodule LiveSelectWeb.ShowcaseLive do
field(:max_selectable, :integer, default: Component.default_opts()[:max_selectable])
field(:user_defined_options, :boolean)
field(:mode, Ecto.Enum, values: [:single, :tags], default: Component.default_opts()[:mode])
field(:tags_mode, Ecto.Enum, values: [:default, :multiple_select], default: Component.default_opts()[:tags_mode])
field(:new, :boolean, default: true)
field(:placeholder, :string, default: "Search for a city")
field(:search_delay, :integer, default: 10)
Expand All @@ -96,6 +97,7 @@ defmodule LiveSelectWeb.ShowcaseLive do
:max_selectable,
:user_defined_options,
:mode,
:tags_mode,
:options,
:selection,
:placeholder,
Expand Down Expand Up @@ -134,6 +136,15 @@ defmodule LiveSelectWeb.ShowcaseLive do
(settings.mode != :single && option == :allow_clear)
end)
|> Keyword.new()
|> then(&maybe_set_classes_for_multiselect/1)
end

defp maybe_set_classes_for_multiselect(opts) do
if LiveSelectWeb.ShowcaseLive.multiple_select?(opts) |> dbg do
Keyword.put(opts, :selected_option_class, "cursor-pointer font-bold hover:bg-gray-400 rounded")
else
opts
end
end

def has_style_errors?(%Ecto.Changeset{errors: errors}) do
Expand Down Expand Up @@ -299,9 +310,15 @@ defmodule LiveSelectWeb.ShowcaseLive do
{:ok, settings} ->
socket.assigns

attrs = if settings.tags_mode == :multiple_select do
%{selected_option_class: "cursor-pointer font-bold hover:bg-gray-400 rounded"}
else
%{}
end

socket =
socket
|> assign(:settings_form, Settings.changeset(settings, %{}) |> to_form)
|> assign(:settings_form, Settings.changeset(settings, attrs) |> to_form)
|> update(:schema_module, fn _, %{settings_form: settings_form} ->
if settings_form[:mode].value == :single, do: CitySearchSingle, else: CitySearchMany
end)
Expand Down Expand Up @@ -488,6 +505,11 @@ defmodule LiveSelectWeb.ShowcaseLive do
{:noreply, socket}
end

def multiple_select?(assigns) do
assigns[:mode] == :tags && Keyword.has_key?(assigns, :tags_mode) &&
assigns[:tags_mode] == :multiple_select
end

defp value_mapper(%City{name: name} = value), do: %{label: name, value: Map.from_struct(value)}

defp value_mapper(value), do: value
Expand Down
6 changes: 6 additions & 0 deletions lib/support/live_select_web/live/showcase_live.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@
class: "select select-sm select-bordered text-xs"
) %>
</div>
<div :if={@settings_form[:mode].value == :tags} class="form-control max-w-sm">
<%= label(@settings_form, :tags_mode, "Tags mode:", class: "label label-text font-semibold") %>
<%= select(@settings_form, :tags_mode, [:default, :multiple_select],
class: "select select-sm select-bordered text-xs"
) %>
</div>
<div class="form-control max-w-sm">
<%= label(@settings_form, :max_selectable, "Max selectable:",
class: "label label-text font-semibold"
Expand Down

0 comments on commit f4d4e24

Please sign in to comment.