From f4e1fbbe0ddc32e209e3a6e96e57eeae9813d493 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:42:57 +0000 Subject: [PATCH] Opt-in 'Clear all' button on a function's form (#29) Co-Authored-By: Claude Sonnet 5.5 --- streamlitfront/elements/elements.py | 29 ++++++++++++++++++++++ tests/test_clear_all.py | 38 +++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/test_clear_all.py diff --git a/streamlitfront/elements/elements.py b/streamlitfront/elements/elements.py index b1e09a0..1088828 100644 --- a/streamlitfront/elements/elements.py +++ b/streamlitfront/elements/elements.py @@ -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) @@ -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, ): @@ -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: @@ -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 = {} diff --git a/tests/test_clear_all.py b/tests/test_clear_all.py new file mode 100644 index 0000000..2e73a68 --- /dev/null +++ b/tests/test_clear_all.py @@ -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