diff --git a/lib/live_select.ex b/lib/live_select.ex index 4385fcc..3c99572 100644 --- a/lib/live_select.ex +++ b/lib/live_select.ex @@ -167,6 +167,16 @@ defmodule LiveSelect do 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 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)) + ``` + + In this case, only the values with a label longer than 3 characters will be kept in the selection. ## Examples diff --git a/lib/live_select/component.ex b/lib/live_select/component.ex index c08e452..aa36ac0 100644 --- a/lib/live_select/component.ex +++ b/lib/live_select/component.ex @@ -191,6 +191,25 @@ defmodule LiveSelect.Component do input_event: true, current_text: new_current_text_after_selection(socket) }) + |> scroll_to_active_option() + 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) + |> 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 @@ -396,6 +415,7 @@ defmodule LiveSelect.Component do :clear_button, :hide_dropdown, :value_mapper, + :update_selection, # for backwards compatibility :form ] @@ -565,6 +585,26 @@ defmodule LiveSelect.Component do 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 + 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) + 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 diff --git a/test/live_select_tags_test.exs b/test/live_select_tags_test.exs index cef3b65..ddfe158 100644 --- a/test/live_select_tags_test.exs +++ b/test/live_select_tags_test.exs @@ -411,6 +411,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 ab55024..4b42350 100644 --- a/test/live_select_test.exs +++ b/test/live_select_test.exs @@ -747,6 +747,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")