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 95c4e14..9f1c3d7 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 c0b6bfe..f159f10 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ 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", ], diff --git a/streamlit_searchbox/__init__.py b/streamlit_searchbox/__init__.py index 5c2ecaf..2ac7215 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 @@ -79,13 +80,17 @@ def _process_search( searchterm: str, rerun_on_update: bool, rerun_scope: Literal["app", "fragment"] = "app", + 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: @@ -95,10 +100,16 @@ def _process_search( st.session_state[key]["options_py"] = _list_to_options_py(search_results) if rerun_on_update: - # Only pass scope if the version is >= 1.37 + 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) + + # only pass scope if the version is >= 1.37 if st.__version__ >= "1.37": - rerun(scope=rerun_scope) # Pass scope if present + rerun(scope=rerun_scope) else: rerun() @@ -182,6 +193,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", rerun_scope: Literal["app", "fragment"] = "app", **kwargs, @@ -217,7 +230,13 @@ def st_searchbox( rerun_scope ("app", "fragment", optional): The scope in which to rerun the Streamlit app. Only applicable if Streamlit version >= 1.37. Defaults to "app". - + 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". @@ -236,6 +255,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"], ) @@ -264,7 +284,13 @@ def st_searchbox( # triggers rerun, no ops afterwards executed _process_search( - search_function, key, value, rerun_on_update, rerun_scope, **kwargs + search_function, + key, + value, + rerun_on_update, + rerun_scope=rerun_scope, + min_execution_time=min_execution_time, + **kwargs, ) if interaction == "submit": 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