diff --git a/README.md b/README.md index f0039cc..5de86de 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,18 @@ style_absolute: bool = False Will position the searchbox as an absolute element. *NOTE:* this will affect all searchbox instances and should either be set for all boxes or none. See [#46](https://github.com/m-wrzr/streamlit-searchbox/issues/46) for inital workaround by [@JoshElgar](https://github.com/JoshElgar). +```python +debounce: int = 0 +``` + +Delay executing the callback from the react component by `x` milliseconds to avoid too many / redudant requests, i.e. during fast typing. + +```python +min_execution_time: int = 0 +``` + +Delay execution after the search function finished to reach a minimum amount of `x` milliseconds. This can be used to avoid fast consecutive reruns, which can cause resets of the component in some streamlit versions `>=1.35`. + ```python key: str = "searchbox" ``` diff --git a/example.py b/example.py index 7d61249..7fa8f16 100644 --- a/example.py +++ b/example.py @@ -98,6 +98,22 @@ def search_kwargs(searchterm: str, **kwargs) -> List[str]: clear_on_submit=False, key=search.__name__, ), + dict( + search_function=search, + default=None, + label=f"{search.__name__}_debounce_250ms", + clear_on_submit=False, + debounce=250, + key=f"{search.__name__}_debounce_250ms", + ), + dict( + search_function=search, + default=None, + label=f"{search.__name__}_min_execution_time_500ms", + clear_on_submit=False, + min_execution_time=500, + key=f"{search.__name__}_min_execution_time_500ms", + ), dict( search_function=search_rnd_delay, default=None, diff --git a/setup.py b/setup.py index 1e8a2f3..f159f10 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="streamlit-searchbox", - version="0.1.14", + version="0.1.15", author="m-wrzr", description="Autocomplete Searchbox", long_description="Streamlit searchbox that dynamically updates " @@ -14,9 +14,9 @@ classifiers=[], python_requires=">=3.8, !=3.9.7", install_requires=[ - # version 1.37 reruns lead to constant iFrame resets + # version 1.37 reruns can lead to constant iFrame # version 1.35/1.36 also have reset issues but less frequent - "streamlit >= 1.0, != 1.37.0", + "streamlit >= 1.0", ], extras_require={ "tests": [ diff --git a/streamlit_searchbox/__init__.py b/streamlit_searchbox/__init__.py index c4e097f..d7dc37c 100644 --- a/streamlit_searchbox/__init__.py +++ b/streamlit_searchbox/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations +import datetime import functools import logging import os @@ -14,7 +15,7 @@ import streamlit.components.v1 as components try: - from streamlit import rerun as rerun # type: ignore + from streamlit import rerun # type: ignore except ImportError: # conditional import for streamlit version <1.27 from streamlit import experimental_rerun as rerun # type: ignore @@ -78,13 +79,17 @@ def _process_search( key: str, searchterm: str, rerun_on_update: bool, + min_execution_time: int = 0, **kwargs, ) -> None: # nothing changed, avoid new search if searchterm == st.session_state[key]["search"]: - return st.session_state[key]["result"] + return st.session_state[key]["search"] = searchterm + + ts_start = datetime.datetime.now() + search_results = search_function(searchterm, **kwargs) if search_results is None: @@ -94,6 +99,13 @@ def _process_search( st.session_state[key]["options_py"] = _list_to_options_py(search_results) if rerun_on_update: + ts_stop = datetime.datetime.now() + execution_time_ms = (ts_stop - ts_start).total_seconds() * 1000 + + # wait until minimal execution time is reached + if execution_time_ms < min_execution_time: + time.sleep((min_execution_time - execution_time_ms) / 1000) + rerun() @@ -176,6 +188,8 @@ def st_searchbox( edit_after_submit: Literal["disabled", "current", "option", "concat"] = "disabled", style_absolute: bool = False, style_overrides: StyleOverrides | None = None, + debounce: int = 0, + min_execution_time: int = 0, key: str = "searchbox", **kwargs, ) -> Any: @@ -207,6 +221,13 @@ def st_searchbox( searchboxes and should be passed to every element. Defaults to False. style_overrides (StyleOverrides, optional): CSS styling passed directly to the react components. Defaults to None. + debounce (int, optional): + Time in milliseconds to wait before sending the input to the search function + to avoid too many requests, i.e. during fast keystrokes. Defaults to 0. + min_execution_time (int, optional): + Minimal execution time for the search function in milliseconds. This is used + to avoid fast consecutive reruns, where fast reruns can lead to resets + within the component in some streamlit versions. Defaults to 0. key (str, optional): Streamlit session key. Defaults to "searchbox". @@ -225,6 +246,7 @@ def st_searchbox( label=label, edit_after_submit=edit_after_submit, style_overrides=style_overrides, + debounce=debounce, # react return state within streamlit session_state key=st.session_state[key]["key_react"], ) @@ -251,8 +273,14 @@ def st_searchbox( if default_use_searchterm: st.session_state[key]["result"] = value - # triggers rerun, no ops afterwards executed - _process_search(search_function, key, value, rerun_on_update, **kwargs) + _process_search( + search_function, + key, + value, + rerun_on_update, + min_execution_time=min_execution_time, + **kwargs, + ) if interaction == "submit": st.session_state[key]["result"] = ( diff --git a/streamlit_searchbox/frontend/package.json b/streamlit_searchbox/frontend/package.json index d4d39f1..f6df814 100644 --- a/streamlit_searchbox/frontend/package.json +++ b/streamlit_searchbox/frontend/package.json @@ -6,7 +6,8 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-select": "^5.8.0", - "streamlit-component-lib": "^2.0.0" + "streamlit-component-lib": "^2.0.0", + "lodash": "^4.17.21" }, "devDependencies": { "@types/lodash": "^4.14.150", diff --git a/streamlit_searchbox/frontend/src/Searchbox.tsx b/streamlit_searchbox/frontend/src/Searchbox.tsx index 8b09dd2..9403a85 100644 --- a/streamlit_searchbox/frontend/src/Searchbox.tsx +++ b/streamlit_searchbox/frontend/src/Searchbox.tsx @@ -7,6 +7,7 @@ import React, { ReactNode } from "react"; import SearchboxStyle from "./styling"; import Select, { InputActionMeta, components } from "react-select"; +import { debounce } from "lodash"; type Option = { value: string; @@ -45,6 +46,18 @@ class Searchbox extends StreamlitComponentBase { ); private ref: any = React.createRef(); + constructor(props: any) { + super(props); + + // bind the search function and debounce to avoid too many requests + if (props.args.debounce && props.args.debounce > 0) { + this.callbackSearch = debounce( + this.callbackSearch.bind(this), + props.args.debounce, + ); + } + } + /** * new keystroke on searchbox * @param input