From 4ee02c9794e5e803d9a9abd75e1a59288d20feb3 Mon Sep 17 00:00:00 2001 From: Jefferson Queiroz Venerando Date: Sun, 9 Jun 2024 19:29:53 -0400 Subject: [PATCH 1/4] add update_selection/1 --- lib/live_select.ex | 20 ++++++++++++++-- lib/live_select/component.ex | 42 ++++++++++++++++++++++++++++++---- test/live_select_tags_test.exs | 39 +++++++++++++++++++++++++++++++ test/live_select_test.exs | 22 ++++++++++++++++++ 4 files changed, 116 insertions(+), 7 deletions(-) diff --git a/lib/live_select.ex b/lib/live_select.ex index 5d36c6d..f54d185 100644 --- a/lib/live_select.ex +++ b/lib/live_select.ex @@ -127,11 +127,27 @@ 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. 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`. + ## Dynamically updating the selection + + You can also update the selection dynamically by passing an 1 arity function that receives the current selection to `:update_selection`: + + ``` + send_update(LiveSelect.Component, id: live_select_id, update_selection: fn current_selection -> Enum.filter(current_selection, &String.length(&1.label) > 3)) + ``` + + In this case, only the values with a label longer than 3 characters will be kept in the selection. + + Another example that appends values to the current selection: + + ``` + values_to_append = [1, 2, 3] + send_update(LiveSelect.Component, id: live_select_id, update_selection: fn current_selection -> current_selection ++ values_to_append end) + ``` ## Examples diff --git a/lib/live_select/component.ex b/lib/live_select/component.ex index 749395e..2238a78 100644 --- a/lib/live_select/component.ex +++ b/lib/live_select/component.ex @@ -151,7 +151,7 @@ defmodule LiveSelect.Component do socket, :selection, fn selection, %{options: options, mode: mode, value_mapper: value_mapper} -> - update_selection( + set_selection( field.value, selection, options, @@ -168,7 +168,19 @@ defmodule LiveSelect.Component do 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) + set_selection(value, selection, options, mode, value_mapper) + end) + |> client_select(%{input_event: true}) + else + socket + end + + socket = + if Map.has_key?(assigns, :update_selection) do + update(socket, :selection, fn + selection, + %{update_selection: update_fn, options: options, mode: mode, value_mapper: value_mapper} -> + update_selection(update_fn, selection, options, mode, value_mapper) end) |> client_select(%{input_event: true}) else @@ -365,6 +377,7 @@ defmodule LiveSelect.Component do :clear_button, :hide_dropdown, :value_mapper, + :update_selection, # for backwards compatibility :form ] @@ -514,19 +527,38 @@ defmodule LiveSelect.Component do }) end - defp update_selection(nil, _current_selection, _options, _mode, _value_mapper), do: [] + defp set_selection(nil, _current_selection, _options, _mode, _value_mapper), do: [] - defp update_selection(value, current_selection, options, :single, value_mapper) do + defp set_selection(value, current_selection, options, :single, value_mapper) do List.wrap(normalize_selection_value(value, options ++ current_selection, value_mapper)) end - defp update_selection(value, current_selection, options, :tags, value_mapper) do + defp set_selection(value, current_selection, options, :tags, value_mapper) do value = if Enumerable.impl_for(value), do: value, else: [value] Enum.map(value, &normalize_selection_value(&1, options ++ current_selection, value_mapper)) |> Enum.reject(&is_nil/1) end + defp update_selection(update_fn, current_selection, options, _mode, value_mapper) + when is_function(update_fn, 1) do + new_selection = update_fn.(current_selection) + + {existing, new} = Enum.split_with(new_selection, &(&1 in current_selection)) + + new = + Enum.map(new, &normalize_selection_value(&1, options, value_mapper)) + |> Enum.reject(&is_nil/1) + + Enum.uniq(existing ++ new) + end + + defp update_selection(_update_fn, _current_selection, _options, _mode, _value_mapper) do + raise """ + Option for `:update_selection` must be a function with arity 1 + """ + 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..522627d 100644 --- a/test/live_select_tags_test.exs +++ b/test/live_select_tags_test.exs @@ -379,6 +379,45 @@ defmodule LiveSelectTagsTest do assert_selected_multiple(live, [%{label: "C", value: 3}, %{label: "E", value: 5}]) end + test "can dynamically change the selection - append example", %{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, update_selection: fn selection -> selection ++ ["B"] end) + + assert_selected_multiple(live, ~w(A B)) + + send_update(live, update_selection: fn selection -> selection ++ ["C"] end) + + assert_selected_multiple(live, ~w(A B C)) + + # Avoids duplicates + send_update(live, update_selection: fn selection -> selection ++ ["C"] end) + + assert_selected_multiple(live, ~w(A B C)) + end + + test "can dynamically change the selection - filter example", %{conn: conn} do + {:ok, live, _html} = live(conn, "/?mode=tags") + + send_update(live, value: ~w(A B)) + + assert_selected_multiple(live, ~w(A B)) + + send_update(live, + update_selection: fn selection -> Enum.filter(selection, &(&1.label == "A")) end + ) + + assert_selected_multiple(live, ~w(A)) + end + test "can render custom clear button", %{conn: conn} do {:ok, live, _html} = live(conn, "/live_component_test") diff --git a/test/live_select_test.exs b/test/live_select_test.exs index a54dba6..c8615be 100644 --- a/test/live_select_test.exs +++ b/test/live_select_test.exs @@ -621,6 +621,28 @@ defmodule LiveSelectTest do assert_selected(live, :D, 4) end + test "can dynamically update selection values", %{conn: conn} do + stub_options(A: 1) + + {:ok, live, _html} = live(conn, "/") + + send_update(live, value: 1, options: [A: 1]) + + assert_selected(live, :A, 1) + + send_update(live, update_selection: fn sel -> Enum.filter(sel, &(&1.value == 1)) end) + + assert_selected(live, :A, 1) + + send_update(live, update_selection: fn sel -> Enum.filter(sel, &(&1.value == 2)) end) + + refute_selected(live) + + send_update(live, update_selection: fn sel -> sel ++ [A: 1] end) + + assert_selected(live, :A, 1) + end + test "renders custom :option slots", %{conn: conn} do {:ok, live, _html} = live(conn, "/live_component_test") From 2d97b8e091778ec1083b31fed3f715317657781f Mon Sep 17 00:00:00 2001 From: Jefferson Venerando Date: Wed, 15 Apr 2026 15:56:37 -0400 Subject: [PATCH 2/4] address PR feedback and implement missing scroll fn --- lib/live_select.ex | 10 ++-------- lib/live_select/component.ex | 37 +++++++++++++++++++++--------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/lib/live_select.ex b/lib/live_select.ex index 87c374c..3c99572 100644 --- a/lib/live_select.ex +++ b/lib/live_select.ex @@ -169,7 +169,8 @@ defmodule LiveSelect do ## Dynamically updating the selection - You can also update the selection dynamically by passing an 1 arity function that receives the current selection to `:update_selection`: + You can dynamically update the selection by using the `:update_selection` assign. + `:update_selection` must be a 1-arity function that receives the current selection and returns the new one: ``` send_update(LiveSelect.Component, id: live_select_id, update_selection: fn current_selection -> Enum.filter(current_selection, &String.length(&1.label) > 3)) @@ -177,13 +178,6 @@ defmodule LiveSelect do In this case, only the values with a label longer than 3 characters will be kept in the selection. - Another example that appends values to the current selection: - - ``` - values_to_append = [1, 2, 3] - send_update(LiveSelect.Component, id: live_select_id, update_selection: fn current_selection -> current_selection ++ values_to_append end) - ``` - ## Examples These examples describe all the moving parts in detail. You can see these examples in action, see which messages and events are being sent, and play around diff --git a/lib/live_select/component.ex b/lib/live_select/component.ex index ab8cb83..1df9b2e 100644 --- a/lib/live_select/component.ex +++ b/lib/live_select/component.ex @@ -165,7 +165,7 @@ defmodule LiveSelect.Component do socket, :selection, fn selection, %{options: options, mode: mode, value_mapper: value_mapper} -> - set_selection( + update_selection( field.value, selection, options, @@ -183,13 +183,14 @@ defmodule LiveSelect.Component do socket = update(socket, :selection, fn selection, %{options: options, value: value, mode: mode, value_mapper: value_mapper} -> - set_selection(value, selection, options, mode, value_mapper) + update_selection(value, selection, options, mode, value_mapper) end) client_select(socket, %{ input_event: true, current_text: new_current_text_after_selection(socket) }) + |> scroll_to_active_option() else socket end @@ -201,7 +202,13 @@ defmodule LiveSelect.Component do %{update_selection: update_fn, options: options, mode: mode, value_mapper: value_mapper} -> update_selection(update_fn, selection, options, mode, value_mapper) end) - |> client_select(%{input_event: true}) + |> then(fn socket -> + client_select(socket, %{ + input_event: true, + current_text: new_current_text_after_selection(socket) + }) + end) + |> scroll_to_active_option() else socket end @@ -586,18 +593,7 @@ defmodule LiveSelect.Component do }) end - defp set_selection(nil, _current_selection, _options, _mode, _value_mapper), do: [] - - defp set_selection(value, current_selection, options, :single, value_mapper) do - List.wrap(normalize_selection_value(value, options ++ current_selection, value_mapper)) - end - - defp set_selection(value, current_selection, options, _mode, value_mapper) do - value = if Enumerable.impl_for(value), do: value, else: [value] - - Enum.map(value, &normalize_selection_value(&1, options ++ current_selection, value_mapper)) - |> Enum.reject(&is_nil/1) - end + defp update_selection(nil, _current_selection, _options, _mode, _value_mapper), do: [] defp update_selection(update_fn, current_selection, options, _mode, value_mapper) when is_function(update_fn, 1) do @@ -612,6 +608,17 @@ defmodule LiveSelect.Component do Enum.uniq(existing ++ new) end + defp update_selection(value, current_selection, options, :single, value_mapper) do + List.wrap(normalize_selection_value(value, options ++ current_selection, value_mapper)) + end + + defp update_selection(value, current_selection, options, _mode, value_mapper) do + value = if Enumerable.impl_for(value), do: value, else: [value] + + Enum.map(value, &normalize_selection_value(&1, options ++ current_selection, value_mapper)) + |> Enum.reject(&is_nil/1) + end + defp update_selection(_update_fn, _current_selection, _options, _mode, _value_mapper) do raise """ Option for `:update_selection` must be a function with arity 1 From f2f483dde1bf61bc105f53f19dc9c52b92e8004f Mon Sep 17 00:00:00 2001 From: Jefferson Venerando Date: Wed, 15 Apr 2026 16:02:40 -0400 Subject: [PATCH 3/4] remove leftover fn --- lib/live_select/component.ex | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/live_select/component.ex b/lib/live_select/component.ex index 1df9b2e..5259a1c 100644 --- a/lib/live_select/component.ex +++ b/lib/live_select/component.ex @@ -582,17 +582,6 @@ defmodule LiveSelect.Component do ) end - defp parent_event(socket, nil, _payload), do: socket - - defp parent_event(socket, event, payload) do - socket - |> push_event("parent_event", %{ - id: socket.assigns.id, - event: event, - payload: payload - }) - end - defp update_selection(nil, _current_selection, _options, _mode, _value_mapper), do: [] defp update_selection(update_fn, current_selection, options, _mode, value_mapper) From f95c9b6891448336b6c47d6f31179a32b403249e Mon Sep 17 00:00:00 2001 From: Jefferson Venerando Date: Wed, 15 Apr 2026 16:32:00 -0400 Subject: [PATCH 4/4] run format and move guar for functions without arity 1 up --- lib/live_select/component.ex | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/live_select/component.ex b/lib/live_select/component.ex index 5259a1c..aa36ac0 100644 --- a/lib/live_select/component.ex +++ b/lib/live_select/component.ex @@ -182,7 +182,8 @@ defmodule LiveSelect.Component do if Map.has_key?(assigns, :value) do socket = 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) @@ -597,6 +598,13 @@ defmodule LiveSelect.Component do Enum.uniq(existing ++ new) end + defp update_selection(update_fn, _current_selection, _options, _mode, _value_mapper) + when is_function(update_fn) do + raise """ + Option for `:update_selection` must be a function with arity 1 + """ + end + defp update_selection(value, current_selection, options, :single, value_mapper) do List.wrap(normalize_selection_value(value, options ++ current_selection, value_mapper)) end @@ -608,12 +616,6 @@ defmodule LiveSelect.Component do |> Enum.reject(&is_nil/1) end - defp update_selection(_update_fn, _current_selection, _options, _mode, _value_mapper) do - raise """ - Option for `:update_selection` must be a function with arity 1 - """ - end - defp normalize_selection_value(%Ecto.Changeset{action: :replace}, _options, _value_mapper), do: nil