Skip to content

Commit dab2e32

Browse files
authored
Merge pull request #110 from Semsee/feature/add-disabled-options
Allow individual options to be disabled
2 parents 9e7e368 + 50bcbfa commit dab2e32

File tree

7 files changed

+184
-25
lines changed

7 files changed

+184
-25
lines changed

lib/live_select.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ defmodule LiveSelect do
5757
* _maps_: `%{label: label, value: value}` or `%{value: value}`
5858
* _keywords_: `[label: label, value: value]` or `[value: value]`
5959
60+
Options can also be disabled when passing in tuples, maps or keywords. Disabled options are displayed, but can't be selected.
61+
Maps and keywords need a `:disabled` key with a boolean value, and tuples should be a 3 element tuple of `{label, value, is_disabled}`
62+
6063
In the case of maps and keywords, if only `value` is specified, it will be used as both value and label for the option.
6164
6265
Because you can pass a list of tuples, you can use maps and keyword lists to pass the list of options, for example:
@@ -449,7 +452,7 @@ defmodule LiveSelect do
449452
doc:
450453
"Event to emit when the text input receives focus. The component id will be sent in the event's params"
451454

452-
@styling_options ~w(active_option_class available_option_class clear_button_class clear_button_extra_class clear_tag_button_class clear_tag_button_extra_class container_class container_extra_class dropdown_class dropdown_extra_class option_class option_extra_class text_input_class text_input_extra_class text_input_selected_class selected_option_class tag_class tag_extra_class tags_container_class tags_container_extra_class)a
455+
@styling_options ~w(active_option_class available_option_class unavailable_option_class clear_button_class clear_button_extra_class clear_tag_button_class clear_tag_button_extra_class container_class container_extra_class dropdown_class dropdown_extra_class option_class option_extra_class text_input_class text_input_extra_class text_input_selected_class selected_option_class tag_class tag_extra_class tags_container_class tags_container_extra_class)a
453456

454457
for attr_name <- @styling_options do
455458
Phoenix.Component.Declarative.__attr__!(

lib/live_select/component.ex

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -448,13 +448,21 @@ defmodule LiveSelect.Component do
448448
Enum.find_index(selection, fn %{label: label} -> label == option.label end)
449449
end
450450

451+
defp select(
452+
socket,
453+
%{disabled: true} = _selected,
454+
_extra_params
455+
) do
456+
socket
457+
end
458+
451459
defp select(
452460
%{assigns: %{selection: selection, max_selectable: max_selectable}} = socket,
453461
_selected,
454462
_extra_params
455463
)
456464
when max_selectable > 0 and length(selection) >= max_selectable do
457-
assign(socket, hide_dropdown: not quick_tags_mode?(socket))
465+
socket
458466
end
459467

460468
defp select(socket, selected, extra_params) do
@@ -587,33 +595,51 @@ defmodule LiveSelect.Component do
587595
)
588596
end
589597

590-
defp normalize_option(option) do
591-
case option do
592-
nil ->
593-
{:ok, nil}
594-
595-
"" ->
596-
{:ok, nil}
598+
defp normalize_option(option) when is_list(option) do
599+
if Keyword.keyword?(option) do
600+
Map.new(option)
601+
|> normalize_option()
602+
else
603+
:error
604+
end
605+
end
597606

607+
defp normalize_option(option) when is_map(option) do
608+
case option do
598609
%{key: key, value: _value} = option ->
599-
{:ok, Map.put_new(option, :label, key)}
610+
{:ok, Enum.into(option, %{label: key, disabled: false})}
600611

601612
%{value: value} = option ->
602-
{:ok, Map.put_new(option, :label, value)}
613+
{:ok, Enum.into(option, %{label: value, disabled: false})}
603614

604-
option when is_list(option) ->
605-
if Keyword.keyword?(option) do
606-
Map.new(option)
607-
|> normalize_option()
608-
else
609-
:error
610-
end
615+
_ ->
616+
:error
617+
end
618+
end
611619

620+
defp normalize_option(option) when is_tuple(option) do
621+
case option do
612622
{label, value} ->
613-
{:ok, %{label: label, value: value}}
623+
{:ok, %{label: label, value: value, disabled: false}}
624+
625+
{label, value, disabled} ->
626+
{:ok, %{label: label, value: value, disabled: disabled}}
627+
628+
_ ->
629+
:error
630+
end
631+
end
632+
633+
defp normalize_option(option) do
634+
case option do
635+
nil ->
636+
{:ok, nil}
637+
638+
"" ->
639+
{:ok, nil}
614640

615641
option when is_binary(option) or is_atom(option) or is_number(option) ->
616-
{:ok, %{label: option, value: option}}
642+
{:ok, %{label: option, value: option, disabled: false}}
617643

618644
_ ->
619645
:error
@@ -713,7 +739,8 @@ defmodule LiveSelect.Component do
713739
options
714740
|> Enum.with_index()
715741
|> Enum.reject(fn {opt, _} ->
716-
active_option == opt || (mode != :quick_tags && already_selected?(opt, selection))
742+
active_option == opt || (mode != :quick_tags && already_selected?(opt, selection)) ||
743+
Map.get(opt, :disabled)
717744
end)
718745
|> Enum.map(fn {_, idx} -> idx end)
719746
|> Enum.find(active_option, &(&1 > active_option))
@@ -738,7 +765,8 @@ defmodule LiveSelect.Component do
738765
|> Enum.with_index()
739766
|> Enum.reverse()
740767
|> Enum.reject(fn {opt, _} ->
741-
active_option == opt || (mode != :quick_tags && already_selected?(opt, selection))
768+
active_option == opt || (mode != :quick_tags && already_selected?(opt, selection)) ||
769+
Map.get(opt, :disabled)
742770
end)
743771
|> Enum.map(fn {_, idx} -> idx end)
744772
|> Enum.find(active_option, &(&1 < active_option || active_option == -1))

lib/live_select/component.html.heex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@
107107
<%= for {option, idx} <- Enum.with_index(@options) do %>
108108
<li class={
109109
cond do
110+
option.disabled ->
111+
class(@style, :unavailable_option, @unavailable_option_class)
112+
110113
already_selected?(option, @selection) ->
111114
class(@style, :selected_option, @selected_option_class)
112115

test/live_select_quick_tags_test.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,4 +572,16 @@ defmodule LiveSelectQuickTagsTest do
572572
%{label: "C", value: value3}
573573
])
574574
end
575+
576+
test "Disabled options can't be selected", %{live: live} do
577+
stub_options([{"A", 1, true}, {"B", 2, false}, {"C", 3, false}])
578+
579+
type(live, "ABC")
580+
581+
select_nth_option(live, 1, method: :click)
582+
refute_selected(live)
583+
584+
select_nth_option(live, 2, method: :click)
585+
assert_selected_multiple(live, [%{value: 2, label: "B"}])
586+
end
575587
end

test/live_select_tags_test.exs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,24 @@ defmodule LiveSelectTagsTest do
221221

222222
assert_selected_multiple_static(live, ~w(B D))
223223
end
224+
225+
test "Disabled Items stay disabled", %{live: live} do
226+
stub_options([{"A", 1, true}, {"B", 2, false}, {"C", 3, false}, {"D", 4, false}])
227+
228+
type(live, "ABC")
229+
select_nth_option(live, 2, method: :click)
230+
231+
type(live, "ABC")
232+
select_nth_option(live, 3, method: :click)
233+
assert_selected_multiple(live, [%{value: 2, label: "B"}, %{value: 3, label: "C"}])
234+
235+
unselect_nth_option(live, 2)
236+
assert_selected_multiple(live, [%{value: 2, label: "B"}])
237+
238+
type(live, "ABC")
239+
select_nth_option(live, 1, method: :click)
240+
assert_selected_multiple(live, [%{value: 2, label: "B"}])
241+
end
224242
end
225243

226244
test "can remove selected options by clicking on tag", %{live: live} do
@@ -548,4 +566,16 @@ defmodule LiveSelectTagsTest do
548566
%{label: "C", value: value3}
549567
])
550568
end
569+
570+
test "Can't select disabled options as tags", %{live: live} do
571+
stub_options([{"A", 1, true}, {"B", 2, false}, {"C", 3, false}])
572+
573+
type(live, "ABC")
574+
select_nth_option(live, 1, method: :click)
575+
refute_selected(live)
576+
577+
type(live, "ABC")
578+
select_nth_option(live, 2, method: :click)
579+
assert_selected_multiple(live, [%{label: "B", value: 2}])
580+
end
551581
end

test/live_select_test.exs

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ defmodule LiveSelectTest do
6565
stub_options([{"A", 1}, {"B", 2}, {"C", 3}])
6666

6767
{:ok, live, _html} = live(conn, "/")
68-
6968
type(live, "ABC")
7069

7170
assert_options(live, ["A", "B", "C"])
@@ -990,4 +989,88 @@ defmodule LiveSelectTest do
990989
end
991990
end
992991
end
992+
993+
describe "Disabled option tests" do
994+
test "Disabled Options are skipped by keydown events", %{conn: conn} do
995+
stub_options([{"A", 1, false}, {"B", 2, true}, {"C", 3, false}])
996+
997+
{:ok, live, _html} = live(conn, "/")
998+
999+
type(live, "ABC")
1000+
assert_options(live, ["A", "B", "C"])
1001+
1002+
select_nth_option(live, 2)
1003+
assert_selected(live, "C", 3)
1004+
end
1005+
1006+
test "Disabled Options are skipped by key up events", %{conn: conn} do
1007+
stub_options([{"A", 1, false}, {"B", 2, true}, {"C", 3, false}])
1008+
1009+
{:ok, live, _html} = live(conn, "/")
1010+
1011+
type(live, "ABC")
1012+
assert_options(live, ["A", "B", "C"])
1013+
1014+
# Navigate to C by going two down
1015+
navigate(live, 2, :down, [])
1016+
1017+
# Navigate back to A by going 1 up as we skip B because it's disabled.
1018+
# Then select the item we're on. It should be "A"
1019+
navigate(live, 1, :up, [])
1020+
keydown(live, "Enter", [])
1021+
1022+
assert_selected(live, "A", 1)
1023+
end
1024+
1025+
test "Supports disabling options filled from an enumerable of maps", %{conn: conn} do
1026+
stub_options([
1027+
%{label: "A", value: 1, disabled: false},
1028+
%{label: "B", value: 2, disabled: true},
1029+
%{label: "C", value: 3, disabled: false}
1030+
])
1031+
1032+
{:ok, live, _html} = live(conn, "/")
1033+
1034+
type(live, "ABC")
1035+
assert_options(live, ["A", "B", "C"])
1036+
1037+
select_nth_option(live, 1)
1038+
assert_selected(live, "A", 1)
1039+
1040+
type(live, "ABC")
1041+
assert_options(live, ["A", "B", "C"])
1042+
# The maps are sorted on their key value pairings on the showcase page
1043+
# before being sent to the LiveSelect component. This results in the
1044+
# disabled option "B" being sorted last.
1045+
select_nth_option(live, 3, method: :click)
1046+
assert_selected_static(live, "A", 1)
1047+
end
1048+
1049+
test "Disabled options can't be selected with mouseclick", %{conn: conn} do
1050+
stub_options([{"A", 1, false}, {"B", 2, true}, {"C", 3, false}])
1051+
1052+
{:ok, live, _html} = live(conn, "/")
1053+
type(live, "ABC")
1054+
1055+
assert_options(live, ["A", "B", "C"])
1056+
select_nth_option(live, 1)
1057+
assert_selected(live, "A", 1)
1058+
1059+
# Mouse clicks won't change the selected option
1060+
type(live, "ABC")
1061+
select_nth_option(live, 2, method: :click)
1062+
assert_selected_static(live, "A", 1)
1063+
end
1064+
1065+
test "If everything is disabled, nothing is saved", %{conn: conn} do
1066+
stub_options([{"A", 1, true}, {"B", 2, true}])
1067+
1068+
{:ok, live, _html} = live(conn, "/")
1069+
type(live, "ABC")
1070+
1071+
assert_options(live, ["A", "B"])
1072+
select_nth_option(live, 1)
1073+
refute_selected(live)
1074+
end
1075+
end
9931076
end

test/support/helpers.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,9 +289,9 @@ defmodule LiveSelect.TestHelpers do
289289
def normalize_selection(selection) do
290290
for element <- selection do
291291
if is_binary(element) || is_integer(element) || is_atom(element) do
292-
%{value: element, label: element}
292+
%{value: element, label: element, disabled: false}
293293
else
294-
element
294+
element |> Map.put_new(:disabled, false)
295295
end
296296
end
297297
end

0 commit comments

Comments
 (0)