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")