Skip to content

Commit

Permalink
keep state on defocus; reset everything after submit;
Browse files Browse the repository at this point in the history
  • Loading branch information
m-wrzr committed Feb 2, 2024
1 parent 4d87018 commit 55a1d5b
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 26 deletions.
11 changes: 8 additions & 3 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__,
),
Expand All @@ -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")],
Expand Down
46 changes: 29 additions & 17 deletions streamlit_searchbox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import functools
import logging
import os
import time
from typing import Any, Callable, List

import streamlit as st
Expand Down Expand Up @@ -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]],
Expand Down Expand Up @@ -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(
Expand All @@ -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,
)

Expand All @@ -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
Expand Down
41 changes: 35 additions & 6 deletions streamlit_searchbox/frontend/src/Searchbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -25,7 +31,10 @@ export function streamlitReturn(interaction: string, value: any): void {
}

class Searchbox extends StreamlitComponentBase<State> {
public state = { menu: false };
public state = {
menu: false,
option: null,
};

private style = new SearchboxStyle(this.props.theme!);
private ref: any = React.createRef();
Expand All @@ -37,6 +46,13 @@ class Searchbox extends StreamlitComponentBase<State> {
* @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 });
Expand All @@ -51,7 +67,7 @@ class Searchbox extends StreamlitComponentBase<State> {
* @param option
* @returns
*/
private onInputSelection(option: any): void {
private onInputSelection(option: Option): void {
// clear selection (X)
if (option === null) {
this.callbackReset();
Expand All @@ -67,6 +83,7 @@ class Searchbox extends StreamlitComponentBase<State> {
private callbackReset(): void {
this.setState({
menu: false,
option: null,
});
streamlitReturn("reset", null);
}
Expand All @@ -75,14 +92,21 @@ class Searchbox extends StreamlitComponentBase<State> {
* 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,
},
});
}
}
Expand All @@ -98,6 +122,7 @@ class Searchbox extends StreamlitComponentBase<State> {
<div style={this.style.label}>{this.props.args.label}</div>
) : null}
<Select
value={this.state.option}
inputId={this.props.args.label || "searchbox-input-id"}
// dereference on clear
ref={this.ref}
Expand All @@ -114,8 +139,12 @@ class Searchbox extends StreamlitComponentBase<State> {
}}
// handlers
filterOption={(_, __) => true}
onChange={(e) => this.onInputSelection(e)}
onInputChange={(e, a) => this.onSearchInput(e, a)}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onChange={(e: any) => this.onInputSelection(e)}
onInputChange={(e, a) => {
// ignore menu close or blur/unfocus events
if (a.action === "input-change") this.onSearchInput(e, a);
}}
onMenuOpen={() => this.setState({ menu: true })}
onMenuClose={() => this.setState({ menu: false })}
menuIsOpen={this.props.args.options && this.state.menu}
Expand Down

0 comments on commit 55a1d5b

Please sign in to comment.