Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.1.15 fragment support; js debounce; min search execution time #56

Merged
merged 14 commits into from
Aug 26, 2024
Merged
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```
Expand Down Expand Up @@ -182,3 +194,4 @@ We welcome contributions from everyone. Here are a few ways you can help:
- [@JoshElgar](https://github.com/JoshElgar) absolute positioning workaround
- [@dopc](https://github.com/dopc) bugfix for [#15](https://github.com/m-wrzr/streamlit-searchbox/issues/15)
- [@Jumitti](https://github.com/Jumitti) `st.rerun` compatibility
- [@salmanrazzaq-94](https://github.com/salmanrazzaq-94) `st.fragment` support
71 changes: 69 additions & 2 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -203,8 +219,14 @@ def search_kwargs(searchterm: str, **kwargs) -> List[str]:
]


searchboxes, visual_ref, form_example, manual_example = st.tabs(
["Searchboxes", "Visual Reference", "Form Example", "Manual Example"]
searchboxes, visual_ref, form_example, manual_example, fragment_example = st.tabs(
[
"Searchboxes",
"Visual Reference",
"Form Example",
"Manual Example",
"Fragment Example",
]
)

with searchboxes:
Expand Down Expand Up @@ -292,3 +314,48 @@ def search_kwargs(searchterm: str, **kwargs) -> List[str]:
)

st.write(manual)

with fragment_example:
if st.__version__ < "1.37":
st.write(f"streamlit >=1.37 needed for this example. version={st.__version__}")
st.stop()

if "app_runs" not in st.session_state:
st.session_state.app_runs = 0
st.session_state.fragment_runs = 0

@st.fragment # type: ignore - code not reached in older streamlit versions
def _fragment():
st.session_state.fragment_runs += 1
st.button("Run Fragment")

selected_value_fragment = st_searchbox(
search_wikipedia_ids,
key="wiki_searchbox_fragment",
rerun_on_update=True,
rerun_scope="fragment",
)

if selected_value_fragment:
st.write(selected_value_fragment)

st.write(f"Fragment says it ran {st.session_state.fragment_runs} times.")

st.session_state.app_runs += 1

_fragment()

st.button("Rerun full app")

selected_value_app = st_searchbox(
search_wikipedia_ids,
key="wiki_searchbox_full_app",
rerun_on_update=True,
rerun_scope="app",
)

if selected_value_app:
st.write(selected_value_app)

st.write(f"Full app says it ran {st.session_state.app_runs} times.")
st.write(f"Full app sees that fragment ran {st.session_state.fragment_runs} times.")
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand All @@ -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 resets
# version 1.35/1.36 also have reset issues but less frequent
"streamlit >= 1.0, != 1.37.0",
"streamlit >= 1.0",
],
extras_require={
"tests": [
Expand Down
59 changes: 54 additions & 5 deletions streamlit_searchbox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from __future__ import annotations

import datetime
import functools
import logging
import os
Expand All @@ -14,12 +15,18 @@
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


# default milliseconds for the search function to run, this is used to avoid
# fast consecutive reruns. possibly remove this in later versions
# see: https://github.com/streamlit/streamlit/issues/9002
MIN_EXECUTION_TIME_DEFAULT = 250 if st.__version__ >= "1.35" else 0


# point to build directory
parent_dir = os.path.dirname(os.path.abspath(__file__))
build_dir = os.path.join(parent_dir, "frontend/build")
Expand Down Expand Up @@ -78,13 +85,18 @@ def _process_search(
key: str,
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:
Expand All @@ -94,7 +106,18 @@ def _process_search(
st.session_state[key]["options_py"] = _list_to_options_py(search_results)

if rerun_on_update:
rerun()
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) # type: ignore
else:
rerun()


def _set_defaults(
Expand Down Expand Up @@ -176,7 +199,10 @@ def st_searchbox(
edit_after_submit: Literal["disabled", "current", "option", "concat"] = "disabled",
style_absolute: bool = False,
style_overrides: StyleOverrides | None = None,
debounce: int = 150,
min_execution_time: int = MIN_EXECUTION_TIME_DEFAULT,
key: str = "searchbox",
rerun_scope: Literal["app", "fragment"] = "app",
**kwargs,
) -> Any:
"""
Expand Down Expand Up @@ -207,6 +233,16 @@ 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.
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".

Expand All @@ -225,6 +261,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"],
)
Expand Down Expand Up @@ -252,7 +289,15 @@ def st_searchbox(
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,
rerun_scope=rerun_scope,
min_execution_time=min_execution_time,
**kwargs,
)

if interaction == "submit":
st.session_state[key]["result"] = (
Expand All @@ -266,7 +311,11 @@ def st_searchbox(
_set_defaults(key, default, default_options)

if rerun_on_update:
rerun()
# only pass scope if the version is >= 1.37
if st.__version__ >= "1.37":
rerun(scope=rerun_scope) # type: ignore
else:
rerun()

return default

Expand Down
4 changes: 2 additions & 2 deletions streamlit_searchbox/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions streamlit_searchbox/frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "streamlit_searchbox",
"version": "0.1.14",
"version": "0.1.15",
"private": true,
"dependencies": {
"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",
Expand Down
13 changes: 13 additions & 0 deletions streamlit_searchbox/frontend/src/Searchbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -45,6 +46,18 @@ class Searchbox extends StreamlitComponentBase<State> {
);
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
Expand Down
15 changes: 13 additions & 2 deletions streamlit_searchbox/frontend/src/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,19 @@ This file contains the icons used in the searchbox component. See https://lucide
export const DropdownIcon: React.FC<React.SVGProps<SVGSVGElement>> = (
props,
) => (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12.7071 15.2929L17.1464 10.8536C17.4614 10.5386 17.2383 10 16.7929 10L7.20711 10C6.76165 10 6.53857 10.5386 6.85355 10.8536L11.2929 15.2929C11.6834 15.6834 12.3166 15.6834 12.7071 15.2929Z"></path>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
{...props}
>
<path d="m6 9 6 6 6-6" />
</svg>
);

Expand Down
2 changes: 1 addition & 1 deletion streamlit_searchbox/frontend/src/styling.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class SearchboxStyle {
// streamlit has fixed icon sizes at 15x15
width={15}
height={15}
fill={this.theme.textColor}
stroke={this.theme.textColor}
transform={menu && overrides && overrides.rotate ? "rotate(180)" : ""}
{...overrides}
/>
Expand Down
Loading