From 55a1d5b0cc50d478845cc28186748cf596ad9383 Mon Sep 17 00:00:00 2001 From: Martin Wurzer Date: Fri, 2 Feb 2024 18:09:30 +0100 Subject: [PATCH] keep state on defocus; reset everything after submit; --- example.py | 11 +++-- streamlit_searchbox/__init__.py | 46 ++++++++++++------- .../frontend/src/Searchbox.tsx | 41 ++++++++++++++--- 3 files changed, 72 insertions(+), 26 deletions(-) diff --git a/example.py b/example.py index 1bb36b4..c8260a3 100644 --- a/example.py +++ b/example.py @@ -85,21 +85,19 @@ def search_empty_list(_: str): label=search_wikipedia_ids.__name__, default="SOME DEFAULT", clear_on_submit=False, - clearable=True, key=search_wikipedia_ids.__name__, ), dict( search_function=search, default=None, label=search.__name__, - clear_on_submit=True, + clear_on_submit=False, key=search.__name__, ), dict( search_function=search_rnd_delay, default=None, clear_on_submit=False, - clearable=True, label=search_rnd_delay.__name__, key=search_rnd_delay.__name__, ), @@ -121,6 +119,13 @@ def search_empty_list(_: str): key=f"{search.__name__}_default_options", label=f"{search.__name__}_default_options", ), + dict( + search_function=search, + default="initial", + default_options=["inital", "list", "of", "options"], + key=f"{search.__name__}_default_options_all", + label=f"{search.__name__}_default_options_all", + ), dict( search_function=search, default_options=[("inital", "i"), ("list", "l")], diff --git a/streamlit_searchbox/__init__.py b/streamlit_searchbox/__init__.py index 761f040..d8c30c0 100644 --- a/streamlit_searchbox/__init__.py +++ b/streamlit_searchbox/__init__.py @@ -6,6 +6,7 @@ import functools import logging import os +import time from typing import Any, Callable, List import streamlit as st @@ -94,6 +95,27 @@ def _process_search( rerun() +def _set_defaults( + key: str, + default: Any, + default_options: List[Any] | None = None, +) -> None: + st.session_state[key] = { + # updated after each selection / reset + "result": default, + # updated after each search keystroke + "search": "", + # updated after each search_function run + "options_js": [], + # key that is used by react component, use time suffix to reload after clear + "key_react": f"{key}_react_{str(time.time())}", + } + + if default_options: + st.session_state[key]["options_js"] = _list_to_options_js(default_options) + st.session_state[key]["options_py"] = _list_to_options_py(default_options) + + @wrap_inactive_session def st_searchbox( search_function: Callable[[str], List[Any]], @@ -128,22 +150,8 @@ def st_searchbox( any: based on user selection """ - # key without prefix used by react component - key_react = f"{key}_react" - if key not in st.session_state: - st.session_state[key] = { - # updated after each selection / reset - "result": default, - # updated after each search keystroke - "search": "", - # updated after each search_function run - "options_js": [], - } - - if default_options: - st.session_state[key]["options_js"] = _list_to_options_js(default_options) - st.session_state[key]["options_py"] = _list_to_options_py(default_options) + _set_defaults(key, default, default_options) # everything here is passed to react as this.props.args react_state = _get_react_component( @@ -152,7 +160,7 @@ def st_searchbox( placeholder=placeholder, label=label, # react return state within streamlit session_state - key=key_react, + key=st.session_state[key]["key_react"], **kwargs, ) @@ -174,7 +182,11 @@ def st_searchbox( return st.session_state[key]["result"] if interaction == "reset": - st.session_state[key]["result"] = default + _set_defaults(key, default, default_options) + + if rerun_on_update: + rerun() + return default # no new react interaction happened diff --git a/streamlit_searchbox/frontend/src/Searchbox.tsx b/streamlit_searchbox/frontend/src/Searchbox.tsx index 0959079..cbbd383 100644 --- a/streamlit_searchbox/frontend/src/Searchbox.tsx +++ b/streamlit_searchbox/frontend/src/Searchbox.tsx @@ -8,8 +8,14 @@ import Select from "react-select"; import SearchboxStyle from "./styling"; +type Option = { + value: string; + label: string; +}; + interface State { menu: boolean; + option: Option | null; } interface StreamlitReturn { @@ -25,7 +31,10 @@ export function streamlitReturn(interaction: string, value: any): void { } class Searchbox extends StreamlitComponentBase { - public state = { menu: false }; + public state = { + menu: false, + option: null, + }; private style = new SearchboxStyle(this.props.theme!); private ref: any = React.createRef(); @@ -37,6 +46,13 @@ class Searchbox extends StreamlitComponentBase { * @returns */ private onSearchInput = (input: string, _: any): void => { + this.setState({ + option: { + value: input, + label: input, + }, + }); + // happens on selection if (input.length === 0) { this.setState({ menu: false }); @@ -51,7 +67,7 @@ class Searchbox extends StreamlitComponentBase { * @param option * @returns */ - private onInputSelection(option: any): void { + private onInputSelection(option: Option): void { // clear selection (X) if (option === null) { this.callbackReset(); @@ -67,6 +83,7 @@ class Searchbox extends StreamlitComponentBase { private callbackReset(): void { this.setState({ menu: false, + option: null, }); streamlitReturn("reset", null); } @@ -75,14 +92,21 @@ class Searchbox extends StreamlitComponentBase { * submitted selection, clear optionally * @param option */ - private callbackSubmit(option: any) { + private callbackSubmit(option: Option) { streamlitReturn("submit", option.value); if (this.props.args.clear_on_submit) { - this.ref.current.select.clearValue(); + this.setState({ + menu: false, + option: null, + }); } else { this.setState({ menu: false, + option: { + value: option.value, + label: option.label, + }, }); } } @@ -98,6 +122,7 @@ class Searchbox extends StreamlitComponentBase {
{this.props.args.label}
) : null}