Skip to content

Commit

Permalink
feat: add mandatory argument for ToggleButtons (#277)
Browse files Browse the repository at this point in the history
* mandatory attribute and typing for ToggleButtons

* Implementing suggestions from code review

Co-authored-by: Maarten Breddels <[email protected]>

Implementing rest of the changes from code review

Further code review

Co-authored-by: Maarten Breddels <[email protected]>

Implementing suggestions from code review
  • Loading branch information
iisakkirotko committed Sep 12, 2023
1 parent 348ab5a commit 1235904
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 9 deletions.
65 changes: 56 additions & 9 deletions solara/components/togglebuttons.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Callable, Dict, List, Optional, TypeVar, Union, cast
from typing import Callable, Dict, List, Optional, TypeVar, Union, cast, overload

import ipyvuetify as v
import reacton
from typing_extensions import Literal

import solara
from solara.alias import rv
Expand All @@ -19,13 +20,44 @@ def _get_button_value(button: reacton.core.Element):
return value


@overload
@solara.value_component(None)
def ToggleButtonsSingle(
value: Union[None, T, solara.Reactive[T]] = None,
value: Union[T, solara.Reactive[T]],
values: List[T] = ...,
children: List[reacton.core.Element] = ...,
on_value: Optional[Callable[[T], None]] = ...,
dense: bool = ...,
mandatory: Literal[True] = ...,
classes: List[str] = ...,
style: Union[str, Dict[str, str], None] = ...,
) -> reacton.core.ValueElement[v.BtnToggle, T]:
...


@overload
@solara.value_component(None)
def ToggleButtonsSingle(
value: Union[Optional[T], solara.Reactive[Optional[T]]] = None,
values: List[T] = ...,
children: List[reacton.core.Element] = ...,
on_value: Optional[Callable[[Optional[T]], None]] = ...,
dense: bool = ...,
mandatory: Literal[False] = ...,
classes: List[str] = ...,
style: Union[str, Dict[str, str], None] = ...,
) -> reacton.core.ValueElement[v.BtnToggle, T]:
...


@solara.value_component(None)
def ToggleButtonsSingle(
value: Union[None, T, Optional[T], solara.Reactive[T], solara.Reactive[Optional[T]]] = None,
values: List[T] = [],
children: List[reacton.core.Element] = [],
on_value: Optional[Callable[[T], None]] = None,
dense: bool = False,
mandatory: bool = True,
classes: List[str] = [],
style: Union[str, Dict[str, str], None] = None,
) -> reacton.core.ValueElement[v.BtnToggle, T]:
Expand Down Expand Up @@ -77,35 +109,46 @@ def Page():
* `children`: List of buttons to use as values.
* `on_value`: Callback to call when the value changes.
* `dense`: Whether to use a dense (smaller) style.
* `mandatory`: Whether a choice is mandatory.
* `style`: CSS style to apply to the top level element.
* `classes`: List of CSS classes to be applied to the top level element.
"""
class_ = _combine_classes(classes)
style_flat = solara.util._flatten_style(style)
# TODO: make type safe
# typing is ignored below due to an issue with the typing; The combination of value being T and on_value being of type Callback[[Optional[T]], None] is
# not allowed to be passed to use_reactive. We also do not allow this by using our overloads, but this information seems lost at this point by
# the typechecker
reactive_value = solara.use_reactive(value, on_value) # type: ignore
children = [solara.Button(label=str(value)) for value in values] + children
values = values + [_get_button_value(button) for button in children] # type: ignore
index, set_index = solara.use_state_or_update(values.index(reactive_value.value) if reactive_value.value is not None else 0, key="index")
# When mandatory = True, index should not be None, but we are letting the front-end take care of setting index to 0 because of a bug
# (see https://github.com/widgetti/solara/issues/282)
# TODO: set index to 0 on python side (after #282 is resolved)
index, set_index = solara.use_state_or_update(values.index(reactive_value.value) if reactive_value.value is not None else None, key="index")

def on_index(index):
set_index(index)
value = values[index]
if mandatory:
value = values[index]
else:
value = values[index] if index is not None else None
reactive_value.set(value)

return cast(
reacton.core.ValueElement[v.BtnToggle, T],
rv.BtnToggle(children=children, multiple=False, mandatory=True, v_model=index, on_v_model=on_index, dense=dense, class_=class_, style_=style_flat),
rv.BtnToggle(children=children, multiple=False, mandatory=mandatory, v_model=index, on_v_model=on_index, dense=dense, class_=class_, style_=style_flat),
)


@solara.value_component(None)
def ToggleButtonsMultiple(
value: Union[List[T], solara.Reactive[List[T]]],
value: Union[List[T], solara.Reactive[List[T]]] = [],
values: List[T] = [],
children: List[reacton.core.Element] = [],
on_value: Callable[[List[T]], None] = None,
on_value: Union[Callable[[List[T]], None], None] = None,
dense: bool = False,
mandatory: bool = False,
classes: List[str] = [],
style: Union[str, Dict[str, str], None] = None,
) -> reacton.core.ValueElement[v.BtnToggle, List[T]]:
Expand Down Expand Up @@ -133,12 +176,14 @@ def Page():
* `children`: List of buttons to use as values.
* `on_value`: Callback to call when the value changes.
* `dense`: Whether to use a dense (smaller) style.
* `mandatory`: Whether selecting at least one element is mandatory.
* `style`: CSS style to apply to the top level element.
* `classes`: List of CSS classes to be applied to the top level element.
"""
class_ = _combine_classes(classes)
style_flat = solara.util._flatten_style(style)
reactive_value = solara.use_reactive(value, on_value)
# See comment regarding typing issue in ToggleButtonsSingle
reactive_value = solara.use_reactive(value, on_value) # type: ignore
children = [solara.Button(label=str(value)) for value in values] + children
allvalues = values + [_get_button_value(button) for button in children]
indices, set_indices = solara.use_state_or_update([allvalues.index(k) for k in reactive_value.value], key="index")
Expand All @@ -150,5 +195,7 @@ def on_indices(indices):

return cast(
reacton.core.ValueElement[v.BtnToggle, List[T]],
rv.BtnToggle(children=children, multiple=True, mandatory=False, v_model=indices, on_v_model=on_indices, dense=dense, class_=class_, style_=style_flat),
rv.BtnToggle(
children=children, multiple=True, mandatory=mandatory, v_model=indices, on_v_model=on_indices, dense=dense, class_=class_, style_=style_flat
),
)
36 changes: 36 additions & 0 deletions tests/unit/toggle_button_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import Optional

import solara


def test_toggle_buttons_single():
value: solara.Reactive[Optional[str]] = solara.reactive(None)

@solara.component
def Test():
with solara.ToggleButtonsSingle("noot", on_value=value.set):
solara.Button("Aap", value="aap")
solara.Button("Noot", value="noot")
solara.Button("Mies", value="mies")

group, rc = solara.render_fixed(Test())
assert group.v_model == 1
group.v_model = 2
assert value.value == "mies"


def test_toggle_buttons_multiple():
value: solara.Reactive[Optional[str]] = solara.reactive(None)

@solara.component
def Test():
with solara.ToggleButtonsMultiple(["noot"], on_value=value.set):
solara.Button("Aap", value="aap")
solara.Button("Noot", value="noot")
solara.Button("Mies", value="mies")

group, rc = solara.render_fixed(Test())
assert group.v_model == [1]
group.v_model = [0, 2]
assert value.value is not None
assert value.value == ["aap", "mies"]

0 comments on commit 1235904

Please sign in to comment.