Skip to content

Commit 89647ef

Browse files
committed
feat: Improvements to selectize plugin defaults/updates
1 parent aea3413 commit 89647ef

File tree

3 files changed

+47
-83
lines changed

3 files changed

+47
-83
lines changed

shiny/ui/_input_select.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,6 @@ def _input_select_impl(
251251
if options is not None and selectize is False:
252252
raise Exception("Options can only be set when selectize is `True`.")
253253

254-
remove_button = _resolve_remove_button(remove_button, multiple)
255-
256254
resolved_id = resolve_id(id)
257255

258256
choices_ = _normalize_choices(choices)
@@ -264,7 +262,8 @@ def _input_select_impl(
264262
if options is None:
265263
options = {}
266264

267-
opts = _update_options(options, remove_button, multiple)
265+
plugins = _get_default_plugins(remove_button, multiple, choices_)
266+
options = _add_default_plugins(options, plugins)
268267

269268
choices_tags = _render_choices(choices_, selected)
270269

@@ -282,10 +281,10 @@ def _input_select_impl(
282281
(
283282
TagList(
284283
tags.script(
285-
dumps(opts),
284+
dumps(options),
286285
type="application/json",
287286
data_for=resolved_id,
288-
data_eval=dumps(extract_js_keys(opts)),
287+
data_eval=dumps(extract_js_keys(options)),
289288
),
290289
selectize_deps(),
291290
)
@@ -298,34 +297,35 @@ def _input_select_impl(
298297
)
299298

300299

301-
def _resolve_remove_button(remove_button: Optional[bool], multiple: bool) -> bool:
300+
def _get_default_plugins(
301+
remove_button: bool | None,
302+
multiple: bool,
303+
choices_: _SelectChoices,
304+
) -> tuple[str, ...]:
305+
if remove_button:
306+
# remove_button: remove _each_ item when multiple=True
307+
# clear_button: clear _all_ items when multiple=True
308+
return ("remove_button", "clear_button")
309+
302310
if remove_button is None:
303311
if multiple:
304-
return True
305-
else:
306-
return False
307-
return remove_button
312+
return ("remove_button", "clear_button")
313+
if "" in choices_:
314+
# Only show clear button if there is an empty choice
315+
return ("clear_button",)
316+
317+
return ()
308318

309319

310-
def _update_options(
311-
options: dict[str, Any], remove_button: bool, multiple: bool
320+
def _add_default_plugins(
321+
options: dict[str, Any], default_plugins: tuple[str, ...]
312322
) -> dict[str, Any]:
313323
opts = copy.deepcopy(options)
314-
plugins = opts.get("plugins", [])
315-
316-
if remove_button:
317-
if multiple:
318-
to_add = "remove_button"
319-
else:
320-
to_add = "clear_button"
321-
322-
if to_add not in plugins:
323-
plugins.append(to_add)
324-
325-
if not plugins:
326-
return options
327-
328-
opts["plugins"] = plugins
324+
p: list[str] = opts.get("plugins", [])
325+
if not isinstance(p, list):
326+
raise TypeError("`options['plugins']` must be a list.")
327+
p.extend(default_plugins)
328+
opts["plugins"] = p
329329
return opts
330330

331331

shiny/ui/_input_update.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@
3636
from ..types import ActionButtonValue
3737
from ._input_check_radio import ChoicesArg, _generate_options
3838
from ._input_date import _as_date_attr
39-
from ._input_select import SelectChoicesArg, _normalize_choices, _render_choices
39+
from ._input_select import (
40+
SelectChoicesArg,
41+
_add_default_plugins,
42+
_normalize_choices,
43+
_render_choices,
44+
)
4045
from ._input_slider import SliderStepArg, SliderValueArg, _as_numeric, _slider_type
4146
from ._utils import JSEval, _session_on_flush_send_msg, extract_js_keys
4247

@@ -719,6 +724,7 @@ def update_selectize(
719724
label: Optional[TagChild] = None,
720725
choices: Optional[SelectChoicesArg] = None,
721726
selected: Optional[str | list[str]] = None,
727+
remove_button: Optional[bool] = None,
722728
options: Optional[dict[str, str | float | JSEval]] = None,
723729
server: bool = False,
724730
session: Optional[Session] = None,
@@ -739,8 +745,11 @@ def update_selectize(
739745
``<optgroup>`` labels.
740746
selected
741747
The values that should be initially selected, if any.
748+
remove_button
749+
Whether to show a remove button for the select input.
742750
options
743-
Options to send to update, see `input_selectize` for details.
751+
Selectize.js options to customize the behavior of the select input.
752+
See <https://selectize.dev/docs/usage> for more details.
744753
server
745754
Whether to store choices on the server side, and load the select options
746755
dynamically on searching, instead of writing all choices into the page at once
@@ -760,7 +769,15 @@ def update_selectize(
760769

761770
session = require_active_session(session)
762771

772+
if remove_button:
773+
options = _add_default_plugins(options or {}, ("remove_button", "clear_button"))
774+
763775
if options is not None:
776+
if remove_button is None:
777+
# clear_button is problematic when multiple=False
778+
# and there is no empty choice
779+
options = _add_default_plugins(options, ("remove_button",))
780+
764781
cfg = tags.script(
765782
json.dumps(options),
766783
type="application/json",

tests/pytest/test_ui.py

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from htmltools import HTMLDocument, TagList, tags
44

55
from shiny import ui
6-
from shiny.ui._input_select import _update_options
6+
from shiny.ui._input_select import _add_remove_button
77

88

99
def test_panel_title():
@@ -136,56 +136,3 @@ def test_modal_footer():
136136
}</script>
137137
</div>"""
138138
)
139-
140-
141-
def test__update_options():
142-
# User does not supply options
143-
assert _update_options({}, remove_button=False, multiple=True) == {}
144-
assert _update_options({}, remove_button=True, multiple=True) == {
145-
"plugins": ["remove_button"]
146-
}
147-
assert _update_options({}, remove_button=True, multiple=False) == {
148-
"plugins": ["clear_button"]
149-
}
150-
151-
# User supplies other plugins
152-
d1 = {"plugins": ["foo", "bar"]}
153-
assert _update_options(d1, remove_button=True, multiple=True) == {
154-
"plugins": ["foo", "bar", "remove_button"]
155-
}
156-
assert _update_options(d1, remove_button=True, multiple=False) == {
157-
"plugins": ["foo", "bar", "clear_button"]
158-
}
159-
assert _update_options(d1, remove_button=False, multiple=False) == {
160-
"plugins": ["foo", "bar"]
161-
}
162-
163-
# User supplies non-plugin options
164-
d2 = {"other_key": "foo"}
165-
166-
assert _update_options(d2, remove_button=True, multiple=True) == {
167-
"other_key": "foo",
168-
"plugins": ["remove_button"],
169-
}
170-
assert _update_options(d2, remove_button=True, multiple=True) == {
171-
"other_key": "foo",
172-
"plugins": ["remove_button"],
173-
}
174-
assert _update_options(d2, remove_button=False, multiple=False) == d2
175-
176-
# User supplies clear button plugin
177-
d3 = {"plugins": ["clear_button"]}
178-
179-
assert _update_options(d3, remove_button=True, multiple=True) == {
180-
"plugins": ["clear_button", "remove_button"]
181-
}
182-
183-
assert _update_options(d3, remove_button=False, multiple=True) == d3
184-
assert _update_options(d3, remove_button=True, multiple=False) == d3
185-
186-
# User supplies both clear and remove button plugins
187-
d4 = {"plugins": ["clear_button", "remove_button"]}
188-
189-
assert _update_options(d4, remove_button=True, multiple=True) == d4
190-
assert _update_options(d4, remove_button=True, multiple=False) == d4
191-
assert _update_options(d4, remove_button=False, multiple=False) == d4

0 commit comments

Comments
 (0)