From 09932c370aa4fa8b3923d40f2993a8ccd4bac968 Mon Sep 17 00:00:00 2001 From: Jefferson Queiroz Venerando Date: Wed, 1 May 2024 11:50:03 -0400 Subject: [PATCH 1/3] add :append option to add values to the selection --- lib/live_select.ex | 13 +++++++++ lib/live_select/component.ex | 47 ++++++++++++++++++++++++------ test/live_select_tags_test.exs | 52 ++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 9 deletions(-) diff --git a/lib/live_select.ex b/lib/live_select.ex index 5d36c6d..8bef8ee 100644 --- a/lib/live_select.ex +++ b/lib/live_select.ex @@ -129,9 +129,22 @@ defmodule LiveSelect do `new_selection` must be a single element in `:single` mode, a list in `:tags` mode. If it's `nil`, the selection will be cleared. After updating the selection, `LiveSelect` will trigger a change event in the form. + `new_selection` must be a single element in `:single` mode, a list in `:tags` mode. If it's `nil`, the selection will be cleared. + After updating the selection, `LiveSelect` will trigger a change event in the form. To set a custom id for the component to use with `Phoenix.LiveView.send_update/3`, you can pass the `id` assign to `live_select/1`. + ### Appending to the selection in `:tags` mode + + When you want to append to the current selection, you can use the `:append` key in the `send_update` call: + + ``` + send_update(LiveSelect.Component, id: live_select_id, append: values_to_append) + ``` + + `values_to_append` can be a single element or a list to append to the current selection. If the value to append already exists in the selection, it will be ignored. + + Note: This does not work in `:single` mode. Use `:value` instead to replace the selection. ## Examples diff --git a/lib/live_select/component.ex b/lib/live_select/component.ex index 749395e..a351369 100644 --- a/lib/live_select/component.ex +++ b/lib/live_select/component.ex @@ -164,15 +164,25 @@ defmodule LiveSelect.Component do socket end + socket = - if Map.has_key?(assigns, :value) do - update(socket, :selection, fn - selection, %{options: options, value: value, mode: mode, value_mapper: value_mapper} -> - update_selection(value, selection, options, mode, value_mapper) - end) - |> client_select(%{input_event: true}) - else - socket + cond do + Map.has_key?(assigns, :value) -> + update(socket, :selection, fn + selection, %{options: options, value: value, mode: mode, value_mapper: value_mapper} -> + update_selection(value, selection, options, mode, value_mapper) + end) + |> client_select(%{input_event: true}) + + Map.has_key?(assigns, :append) -> + update(socket, :selection, fn + selection, %{append: value, mode: mode, value_mapper: value_mapper} -> + append_selection(value, selection, mode, value_mapper) + end) + |> client_select(%{input_event: true}) + + true -> + socket end {:ok, socket} @@ -365,6 +375,7 @@ defmodule LiveSelect.Component do :clear_button, :hide_dropdown, :value_mapper, + :append, # for backwards compatibility :form ] @@ -376,7 +387,7 @@ defmodule LiveSelect.Component do |> Enum.sort_by(&String.jaro_distance(to_string(&1), to_string(assign))) |> List.last() - raise ~s(Invalid assign: "#{assign}". Did you mean "#{most_similar}" ?) + raise ~s(Invalid assign: "#{assign}". Did you mean "#{most_similar}"?) end end end @@ -527,6 +538,24 @@ defmodule LiveSelect.Component do |> Enum.reject(&is_nil/1) end + defp append_selection(_value, _current_selection, :single, _value_mapper) do + raise """ + Appending values only works in `:tags` mode + """ + end + + defp append_selection(nil, current_selection, _mode, _value_mapper), do: current_selection + + defp append_selection(value, current_selection, :tags, value_mapper) do + value = if Enumerable.impl_for(value), do: value, else: [value] + + normalized_value = Enum.map(value, &normalize_selection_value(&1, current_selection, value_mapper)) + + current_selection ++ normalized_value + |> Enum.reject(&is_nil/1) + |> Enum.uniq() + end + defp normalize_selection_value(%Ecto.Changeset{action: :replace}, _options, _value_mapper), do: nil diff --git a/test/live_select_tags_test.exs b/test/live_select_tags_test.exs index da6ecb2..5643659 100644 --- a/test/live_select_tags_test.exs +++ b/test/live_select_tags_test.exs @@ -379,6 +379,58 @@ defmodule LiveSelectTagsTest do assert_selected_multiple(live, [%{label: "C", value: 3}, %{label: "E", value: 5}]) end + test "can append values to the selection", %{conn: conn} do + {:ok, live, _html} = live(conn, "/?mode=tags") + + stub_options(~w(A B C)) + + type(live, "ABC") + + select_nth_option(live, 1) + + assert_selected_multiple(live, ~w(A)) + + send_update(live, append: ["B"]) + + assert_selected_multiple(live, ~w(A B)) + + send_update(live, append: "C") + + assert_selected_multiple(live, ~w(A B C)) + end + + test "does not duplicate selection when appending values", %{conn: conn} do + {:ok, live, _html} = live(conn, "/?mode=tags") + + stub_options(~w(A B C)) + + type(live, "ABC") + + select_nth_option(live, 1) + + assert_selected_multiple(live, ~w(A)) + + send_update(live, append: ~w(A)) + + assert_selected_multiple(live, ~w(A)) + end + + test "does not change the selection when appending nil values", %{conn: conn} do + {:ok, live, _html} = live(conn, "/?mode=tags") + + stub_options(~w(A B C)) + + type(live, "ABC") + + select_nth_option(live, 1) + + assert_selected_multiple(live, ~w(A)) + + send_update(live, append: nil) + + assert_selected_multiple(live, ~w(A)) + end + test "can render custom clear button", %{conn: conn} do {:ok, live, _html} = live(conn, "/live_component_test") From e4720681f8f42644bf6be457c1e6ce805828045d Mon Sep 17 00:00:00 2001 From: Jefferson Queiroz Venerando Date: Wed, 1 May 2024 11:56:33 -0400 Subject: [PATCH 2/3] remove duplicate line --- lib/live_select.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/live_select.ex b/lib/live_select.ex index 8bef8ee..bb21b7c 100644 --- a/lib/live_select.ex +++ b/lib/live_select.ex @@ -127,8 +127,6 @@ defmodule LiveSelect do send_update(LiveSelect.Component, id: live_select_id, value: new_selection) ``` - `new_selection` must be a single element in `:single` mode, a list in `:tags` mode. If it's `nil`, the selection will be cleared. - After updating the selection, `LiveSelect` will trigger a change event in the form. `new_selection` must be a single element in `:single` mode, a list in `:tags` mode. If it's `nil`, the selection will be cleared. After updating the selection, `LiveSelect` will trigger a change event in the form. From 809b4c33b68c8ece7e3fee96babec9245c9f75f9 Mon Sep 17 00:00:00 2001 From: Jefferson Queiroz Venerando Date: Wed, 1 May 2024 11:58:23 -0400 Subject: [PATCH 3/3] fix formatting --- lib/live_select/component.ex | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/live_select/component.ex b/lib/live_select/component.ex index a351369..4a70165 100644 --- a/lib/live_select/component.ex +++ b/lib/live_select/component.ex @@ -164,12 +164,12 @@ defmodule LiveSelect.Component do socket end - socket = cond do Map.has_key?(assigns, :value) -> update(socket, :selection, fn - selection, %{options: options, value: value, mode: mode, value_mapper: value_mapper} -> + selection, + %{options: options, value: value, mode: mode, value_mapper: value_mapper} -> update_selection(value, selection, options, mode, value_mapper) end) |> client_select(%{input_event: true}) @@ -549,9 +549,10 @@ defmodule LiveSelect.Component do defp append_selection(value, current_selection, :tags, value_mapper) do value = if Enumerable.impl_for(value), do: value, else: [value] - normalized_value = Enum.map(value, &normalize_selection_value(&1, current_selection, value_mapper)) + normalized_value = + Enum.map(value, &normalize_selection_value(&1, current_selection, value_mapper)) - current_selection ++ normalized_value + (current_selection ++ normalized_value) |> Enum.reject(&is_nil/1) |> Enum.uniq() end