Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions streamlitfront/elements/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ def render_views(self):
view_runner() # runs the page with the state


def _clear_inputs(input_keys):
"""Forget the state of every widget under ``input_keys`` (the input itself, its view,
"None" toggle, ``*args`` count and fields), so each falls back to its default.

Meant as a button ``on_click``: Streamlit lets callbacks drop widget keys before the rerun.
"""
prefixes = tuple(f"{k}_" for k in input_keys)
for key in list(st.session_state.keys()):
if key in input_keys or key.startswith(prefixes):
del st.session_state[key]


class View(FrontContainerBase):
def render(self):
st.markdown(f"""## **{self.name}**""", help=self.tooltip)
Expand Down Expand Up @@ -152,6 +164,7 @@ def __init__(
on_submit: Callable[[Any], None] = None,
use_expander: bool = True,
submit_button_label: str = "Submit",
clear_button_label: str = None,
authentification: bool = False,
tooltip: str = None,
):
Expand All @@ -167,6 +180,7 @@ def __init__(
)
self.use_expander = use_expander
self.submit_button_label = submit_button_label
self.clear_button_label = clear_button_label
self.authentification = authentification
if authentification:
if on_submit is None:
Expand All @@ -192,12 +206,27 @@ def _render_section_content(self):
for uploader in _file_uploaders(self.children):
uploader._slot = None
inputs = self._render_inputs()
if self.clear_button_label:
st.button(
self.clear_button_label,
key=f"{self.name}_clear_all",
on_click=_clear_inputs,
args=(self._input_keys(),),
help="Reset every field to its default",
)
if self.auto_submit or st.button(self.submit_button_label):
try:
self._submit(inputs)
except ValidationError as e:
st.error(e)

def _input_keys(self) -> list:
return [
child.input_key
for child in self.children
if isinstance(child, InputBase) and child.input_key
]

def _render_inputs(self):
"""Render the inputs; a ``*args`` parameter gets a "+" button and one field per press."""
inputs = {}
Expand Down
38 changes: 38 additions & 0 deletions tests/test_clear_all.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""An opt-in "Clear all" button resets a view's fields to their defaults
(i2mint/streamlitfront#29). Runs the app headlessly with ``AppTest``."""

import pytest

pytest.importorskip("streamlit.testing.v1")
from streamlit.testing.v1 import AppTest

APP = """
from front import RENDERING_KEY
from streamlitfront import mk_app
def greet(who: str = "world", times: int = 1):
return (who + " ") * times
mk_app([greet], config={RENDERING_KEY: {"greet": {"execution": {"clear_button_label": "Clear all"}}}})()
"""

NO_CLEAR = APP.replace('"clear_button_label": "Clear all"', '"auto_submit": False')


def _button(at, label):
return next((b for b in at.button if b.label == label), None)


def test_clear_all_restores_defaults():
at = AppTest.from_string(APP, default_timeout=30).run()
assert not at.exception
at.text_input[0].set_value("moon").run()
at.number_input[0].set_value(4).run()
assert at.text_input[0].value == "moon"
_button(at, "Clear all").click().run()
assert not at.exception
assert at.text_input[0].value == "world"
assert at.number_input[0].value == 1


def test_no_clear_button_by_default():
at = AppTest.from_string(NO_CLEAR, default_timeout=30).run()
assert _button(at, "Clear all") is None
Loading