Skip to content

Commit

Permalink
form recovery
Browse files Browse the repository at this point in the history
  • Loading branch information
maxmarcon committed Dec 5, 2023
1 parent d2cbfae commit af56055
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 65 deletions.
7 changes: 7 additions & 0 deletions assets/js/live_select.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export default {
}
})
this.handleEvent("select", ({id, selection, mode, input_event, parent_event}) => {
this.selection = selection
console.log("selection: ", this.selection)
if (this.el.id === id) {
if (mode === "single") {
const label = selection.length > 0 ? selection[0].label : null
Expand Down Expand Up @@ -117,6 +119,11 @@ export default {
updated() {
this.maybeStyleClearButton()
this.attachDomEventHandlers()
},
reconnected() {
if (this.selection && this.selection.length > 0) {
this.pushEventTo(this.el, "options_recovery", this.selection)
}
}
}
}
77 changes: 48 additions & 29 deletions lib/live_select/component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ defmodule LiveSelect.Component do
socket =
if Map.has_key?(assigns, :value) do
update(socket, :selection, fn
selection, %{options: options, mode: mode, value: value} ->
selection, %{options: options, value: value, mode: mode} ->
update_selection(value, selection, options, mode)
end)
|> client_select(%{input_event: true})
Expand Down Expand Up @@ -209,6 +209,24 @@ defmodule LiveSelect.Component do
{:noreply, socket}
end

@impl true
def handle_event("options_recovery", options, socket) do
options =
for %{"label" => label, "value" => value} <- options do
%{label: label, value: value}
end

{:noreply,
assign(socket,
options: options,
selection:
Enum.map(socket.assigns.selection, fn %{value: value} ->
Enum.find(options, fn %{value: option_value} -> option_value == value end)
end)
|> Enum.filter(& &1)
)}
end

@impl true
def handle_event("options_clear", _params, socket) do
socket =
Expand Down Expand Up @@ -367,7 +385,7 @@ defmodule LiveSelect.Component do
extra_params
)
when is_binary(current_text) do
{:ok, option} = normalize(current_text)
{:ok, option} = normalize_option(current_text)

if already_selected?(option, socket.assigns.selection) do
socket
Expand Down Expand Up @@ -478,30 +496,24 @@ defmodule LiveSelect.Component do
defp update_selection(nil, _current_selection, _options, _mode), do: []

defp update_selection(value, current_selection, options, :single) do
if option = Enum.find(options ++ current_selection, fn %{value: val} -> value == val end) do
[option]
else
case normalize(value) do
{:ok, option} -> List.wrap(option)
:error -> invalid_option(value, :selection)
end
end
List.wrap(normalize_selection_value(value, options ++ current_selection))
end

defp update_selection(value, current_selection, options, :tags) do
value = if Enumerable.impl_for(value), do: value, else: [value]

value
|> Enum.map(
&if option = Enum.find(options ++ current_selection, fn %{value: value} -> value == &1 end) do
option
else
case normalize(&1) do
{:ok, option} -> option
:error -> invalid_option(&1, :selection)
end
Enum.map(value, &normalize_selection_value(&1, options ++ current_selection))
end

defp normalize_selection_value(selection_value, options) do
if option = Enum.find(options, fn %{value: value} -> selection_value == value end) do
option
else
case normalize_option(selection_value) do
{:ok, option} -> option
:error -> %{label: "", value: selection_value}
end
)
end
end

defp normalize_options(options) when is_map(options) do
Expand All @@ -511,27 +523,34 @@ defmodule LiveSelect.Component do
defp normalize_options(options) do
options
|> Enum.map(
&case normalize(&1) do
&case normalize_option(&1) do
{:ok, option} -> option
:error -> invalid_option(&1, :option)
:error -> invalid_option(&1)
end
)
end

defp normalize(option_or_selection) do
case option_or_selection do
defp normalize_option(option) do
case option do
nil ->
{:ok, nil}

"" ->
{:ok, nil}

%{key: key, value: _value} = option ->
{:ok, Map.put_new(option, :label, key)}

%{value: value} = option ->
{:ok, Map.put_new(option, :label, value)}

option when is_list(option) ->
Map.new(option)
|> normalize()
if Keyword.keyword?(option) do
Map.new(option)
|> normalize_option()
else
:error
end

{label, value} ->
{:ok, %{label: label, value: value}}
Expand All @@ -544,10 +563,10 @@ defmodule LiveSelect.Component do
end
end

defp invalid_option(option, what) do
defp invalid_option(option) do
raise """
invalid #{if what == :selection, do: "element in selection", else: "element in options"}: #{inspect(option)}
elements of #{what} can be:
invalid element in options: #{inspect(option)}
elements can be:
atoms, strings or numbers
maps or keywords with keys: (:label, :value) or (:key, :value) and an optional key :tag_label
Expand Down
3 changes: 2 additions & 1 deletion lib/support/live_select_web/live/showcase_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ defmodule LiveSelectWeb.ShowcaseLive do
"submit" ->
mode = socket.assigns.settings_form.data.mode

selected = get_in(params,~w(my_form city_search))
selected = get_in(params, ~w(my_form city_search))
selected_text = get_in(params, ~w(my_form city_search_text_input))

{cities, locations} = extract_cities_and_locations(mode, selected_text, selected)
Expand All @@ -364,6 +364,7 @@ defmodule LiveSelectWeb.ShowcaseLive do
"change" ->
params =
update_in(params, ~w(my_form city_search), fn
nil -> nil
selection when is_list(selection) -> Enum.map(selection, &decode/1)
selection -> decode(selection)
end)
Expand Down
2 changes: 1 addition & 1 deletion priv/static/live_select.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 16 additions & 17 deletions test/live_select/component_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,6 @@ defmodule LiveSelect.ComponentTest do

assert_selected_static(component, "C", 3)
end

@tag source: %{"city_search" => [{"B", 1}]}
test "raises if initial selection is in the wrong format", %{form: form} do
assert_raise RuntimeError, ~r/invalid element in selection/, fn ->
render_component(&LiveSelect.live_select/1,
field: form[:city_search]
)
end
end
end

describe "in tags mode" do
Expand Down Expand Up @@ -284,15 +275,23 @@ defmodule LiveSelect.ComponentTest do
%{label: "C", value: 3}
])
end
end

@tag source: %{"city_search" => [%{B: 1, C: 2}]}
test "raises if initial selection is in the wrong format", %{form: form} do
assert_raise RuntimeError, ~r/invalid element in selection/, fn ->
render_component(&LiveSelect.live_select/1,
mode: :tags,
field: form[:city_search]
)
end
test "raises if options are passed in the wrong format (1)", %{form: form} do
assert_raise RuntimeError, ~r/invalid element in option/, fn ->
render_component(&LiveSelect.live_select/1,
field: form[:city_search],
options: [[10, 20]]
)
end
end

test "raises if options are passed in the wrong format (2)", %{form: form} do
assert_raise RuntimeError, ~r/invalid element in option/, fn ->
render_component(&LiveSelect.live_select/1,
field: form[:city_search],
options: [%{x: 10, y: 20}]
)
end
end

Expand Down
20 changes: 20 additions & 0 deletions test/live_select_tags_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -476,4 +476,24 @@ defmodule LiveSelectTagsTest do
%{value: 4, label: "D"}
])
end

test "form recovery", %{conn: conn} do
{:ok, live, _html} = live(conn, "/?mode=tags")

values = [value1 = [10, 20], value2 = %{"x" => 10, "y" => 20}, value3 = "C"]

render_change(live, "change", %{"my_form" => %{"city_search" => Jason.encode!(values)}})

render_hook(element(live, selectors()[:container]), "options_recovery", [
%{label: "A", value: value1},
%{label: "B", value: value2},
%{label: "C", value: value3}
])

assert_selected_multiple_static(live, [
%{label: "A", value: value1},
%{label: "B", value: value2},
%{label: "C", value: value3}
])
end
end
49 changes: 38 additions & 11 deletions test/live_select_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -639,24 +639,51 @@ defmodule LiveSelectTest do
assert tag =~ "with custom slot"
end

test "selection can be updated from the form", %{conn: conn} do
stub_options(%{
"A" => 1,
"B" => 2,
"C" => 3
})
test "selection can be cleared from the form", %{conn: conn} do
{:ok, live, _html} = live(conn, "/")

render_change(live, "change", %{"my_form" => %{"city_search" => ""}})

assert_clear_static(live)
end

test "form recovery (1)", %{conn: conn} do
{:ok, live, _html} = live(conn, "/")

type(live, "ABC")
value = [10, 20]
render_change(live, "change", %{"my_form" => %{"city_search" => Jason.encode!(value)}})

select_nth_option(live, 2)
render_hook(element(live, selectors()[:container]), "options_recovery", [
%{label: "A", value: value}
])

assert_selected(live, "B", 2)
assert_selected_static(live, "A", value)
end

test "form recovery (2)", %{conn: conn} do
{:ok, live, _html} = live(conn, "/")

value = %{"x" => 10, "y" => 20}
render_change(live, "change", %{"my_form" => %{"city_search" => Jason.encode!(value)}})

render_hook(element(live, selectors()[:container]), "options_recovery", [
%{label: "A", value: value}
])

assert_selected_static(live, "A", value)
end

test "form recovery (3)", %{conn: conn} do
{:ok, live, _html} = live(conn, "/")

value = "A"
render_change(live, "change", %{"my_form" => %{"city_search" => value}})

render_change(live, "change", %{"my_form" => %{"city_search" => 1}})
render_hook(element(live, selectors()[:container]), "options_recovery", [
%{label: "A", value: value}
])

assert_selected_static(live, "A", 1)
assert_selected_static(live, "A", value)
end

for style <- [:daisyui, :tailwind, :none, nil] do
Expand Down
16 changes: 10 additions & 6 deletions test/support/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,16 @@ defmodule LiveSelect.TestHelpers do
end

def assert_clear(live, input_event \\ true) do
assert_clear_static(live)

assert_push_event(live, "select", %{
id: @component_id,
selection: [],
input_event: ^input_event
})
end

def assert_clear_static(live) do
assert live
|> element(@selectors[:text_input])
|> render()
Expand All @@ -397,12 +407,6 @@ defmodule LiveSelect.TestHelpers do
|> render()
|> Floki.parse_fragment!()
|> Floki.attribute("value") == []

assert_push_event(live, "select", %{
id: @component_id,
selection: [],
input_event: ^input_event
})
end

def assert_option_removeable(live, n) do
Expand Down

0 comments on commit af56055

Please sign in to comment.