Open CHerSun opened 11 months ago
@CHerSun ,i also use index in sac.steps,but when button click,the sac.steps will blinking,in future version,maybe add previous and next button in sac.steps.
import streamlit as st
import streamlit_antd_components as sac
st.title("Issue #25")
if 'index' not in st.session_state:
st.session_state['index'] = 0
def next_callback(max_index):
if st.session_state['index'] < max_index:
st.session_state['index'] += 1
def previous_callback():
if st.session_state['index'] > 0:
st.session_state['index'] -= 1
sac.steps(
[sac.StepsItem('step 1'),
sac.StepsItem('step 2'),
sac.StepsItem('step 3')],
index=st.session_state['index'],
)
st.button('previous', on_click=previous_callback)
st.button('next', on_click=next_callback, args=(2,))
st.write(st.session_state)
Yep, I use similar thing, but through key=f"some_fixed_key_{counter}"
and updating counter. Will try with index only.
Do you think it might be possible to do this similarly to streamlit native components? I.e. use the same key for component:
...
sac.steps (...,
key="some_fixed_key",
)
...
and just update st.session_state["some_fixed_key"]
with new value before rendering? And if component sees changed value - it updates that on JS side too?
in future version,maybe add previous and next button in sac.steps
not sure if that's needed tbh. At least for my case I want steps to be on top and buttons to be at the bottom of something like a form, so that user doesn't have to scroll up/down (thought even to use steps x2, but users are against that).
Full code in case you'll want to look into my part:
from collections.abc import Callable
from dataclasses import _MISSING_TYPE, MISSING
from functools import partial
from typing import Any, TypeVar, overload
import streamlit as st
from streamlit_antd_components import StepsItem, steps
from translations import no_translation
T_IN = TypeVar("T_IN")
T1 = TypeVar("T1")
T2 = TypeVar("T2")
T3 = TypeVar("T3")
T4 = TypeVar("T4")
T_OUT = TypeVar("T_OUT")
def __next_callback(key: str) -> None:
st.session_state[key + "current_step"] += 1
st.session_state[key + "counter"] += 1
def __prev_callback(key: str) -> None:
st.session_state[key + "current_step"] -= 1
st.session_state[key + "counter"] += 1
def __user_changed_step(key: str) -> None:
st.session_state[key + "current_step"] = st.session_state[key + "steps" + str(st.session_state[key + "counter"])]
@overload
def stepper( # noqa: PLR0913
actions: tuple[Callable[[T_IN], T_OUT|None]],
titles: list[str] | None = None,
descriptions: list[str] | None = None,
translations: Callable[[str], str] = no_translation,
key: str = "",
starting_value: T_IN|_MISSING_TYPE = MISSING,
) -> T_OUT|None:
...
@overload
def stepper( # noqa: PLR0913
actions: tuple[Callable[[T_IN], T1|None], Callable[[T1], T_OUT|None]],
titles: list[str] | None = None,
descriptions: list[str] | None = None,
translations: Callable[[str], str] = no_translation,
key: str = "",
starting_value: T_IN|_MISSING_TYPE = MISSING,
) -> T_OUT|None:
...
@overload
def stepper( # noqa: PLR0913
actions: tuple[Callable[[T_IN], T1|None], Callable[[T1], T2|None], Callable[[T2], T_OUT|None]],
titles: list[str] | None = None,
descriptions: list[str] | None = None,
translations: Callable[[str], str] = no_translation,
key: str = "",
starting_value: T_IN|_MISSING_TYPE = MISSING,
) -> T_OUT|None:
...
@overload
def stepper( # noqa: PLR0913
actions: tuple[Callable[[T_IN], T1|None], Callable[[T1], T2|None], Callable[[T2], T3|None], Callable[[T3], T_OUT|None]],
titles: list[str] | None = None,
descriptions: list[str] | None = None,
translations: Callable[[str], str] = no_translation,
key: str = "",
starting_value: T_IN|_MISSING_TYPE = MISSING,
) -> T_OUT|None:
...
@overload
def stepper( # noqa: PLR0913
actions: tuple[Callable[[T_IN], T1|None], Callable[[T1], T2|None], Callable[[T2], T3|None], Callable[[T3], T4|None], Callable[[T4], T_OUT|None]],
titles: list[str] | None = None,
descriptions: list[str] | None = None,
translations: Callable[[str], str] = no_translation,
key: str = "",
starting_value: T_IN|_MISSING_TYPE = MISSING,
) -> T_OUT|None:
...
def stepper( # noqa: PLR0913
actions: tuple[Callable, ...],
titles: list[str] | None = None,
descriptions: list[str] | None = None,
translations: Callable[[str], str] = no_translation,
key: str = "",
starting_value: Any = MISSING,
) -> Any|None:
"""Display a stepper, which activates next step only if current step gave result different than None
Args:
actions (tuple[Callable]): A collection of actions to be performed step-by-step
titles (list[str] | None, optional): Step titles. Must be of the same length as actions or None. Defaults to None.
descriptions (list[str] | None, optional): Step descriptions. Must be of the same length as actions. Defaults to None.
translations (Callable[[str], str], optional): Translations function to use. Defaults to no_translation.
key (str, optional): streamlit component key to use. Defaults to "".
starting_value (Any, optional): Value to give to the first function step. If not given - no value will be given on 1st step. \
Defaults to MISSING.
Returns:
Any: Result of the last step or None if not reached that yet.
NOTE:
* actions are performed step-by-step
* results of previous step are given to the next step as input
* first step either gets NO INPUT or given starting_value
"""
key = key + ".stepper."
current_step = st.session_state.setdefault(key + "current_step", 0)
previous_current_step_result = st.session_state.setdefault(key + str(current_step), None)
next_step_possible = previous_current_step_result is not None
# TODO: a better way to allow Next/Prev buttons and stepper
# Ugly workaround for inability to directly update selected step on SAC steps - a counter to force-refresh component
counter = st.session_state.setdefault(key + "counter", 0)
# Remove any results past this step:
for i in range(current_step+1, len(actions)):
st.session_state.pop(key + str(i), None)
# Prepare stepper items list
enabled_steps_till = current_step + int(next_step_possible)
step_items = [StepsItem(
titles[i] if titles else "",
description=descriptions[i] if descriptions else "",
disabled=i>enabled_steps_till,
)
for i in range(len(actions))]
# UI Render steps
steps(step_items, index=current_step, key=key + "steps" + str(counter), return_index=True,
on_change=partial(__user_changed_step, key), type="default")
st.divider()
# UI Render action and get its result
if current_step==0 and starting_value is MISSING:
current_step_result = actions[current_step]()
else:
previous_step_result = st.session_state[key + str(current_step - 1)]
current_step_result = actions[current_step](previous_step_result)
# Save current step result
st.session_state[key + str(current_step)] = current_step_result
if (current_step_result is None and previous_current_step_result is not None) or \
(current_step_result is not None and previous_current_step_result is None):
st.rerun()
st.divider()
# UI render buttons
cols = st.columns(2)
confirmed = False
# Render Next or Confirm
if current_step == len(actions)-1:
confirmed = cols[0].button(translations("Confirm"), type="primary", disabled=current_step_result is None, key=key + "finish",
use_container_width=True)
else:
cols[0].button(translations("Next"), type="primary", disabled=not next_step_possible, key=key + "next",
on_click=partial(__next_callback, key), use_container_width=True)
# Render back button
if current_step>0:
cols[1].button(translations("Back"), disabled=current_step==0, key=key + "prev",
on_click=partial(__prev_callback, key), use_container_width=True)
# Return None always, until Finish button is clicked on the last step
return current_step_result if confirmed else None
The idea is to take a collection of functions and chain their results from one to another, controlling the flow automatically (when to allow next step, cleaning data if user decided to go back, etc).
When user clicks on the stepper - everything looks to be working ok. But buttons make it to flicker, as you've written above.
@CHerSun ,hi, i update sac,now it support change value by streamlit session state,you can try it.
and just update st.session_state["some_fixed_key"] with new value before rendering? And if component sees changed value - it updates that on JS side too?
Btw, thanks your idea,i use react useEffect to achive this.
import streamlit as st
import streamlit_antd_components as sac
st.title("Issue #25")
def next_callback(max_index):
if st.session_state['steps'] < max_index:
st.session_state['steps'] += 1
def previous_callback():
if st.session_state['steps'] > 0:
st.session_state['steps'] -= 1
sac.steps(
[sac.StepsItem('step 1'),
sac.StepsItem('step 2'),
sac.StepsItem('step 3')],
key='steps', return_index=True
)
st.button('previous', on_click=previous_callback)
st.button('next', on_click=next_callback, args=(2,))
st.write(st.session_state)```
Cool! Thank you, you rock really. One of the best pieces for the Streamlit.
Could you advise on how to change current step programmatically?
I want to have
steps
on top (then a form with some inputs), andnext
,back
buttons at the bottom.Tried:
index
onsteps
st.session_state[steps_key] = new_value
The only variant I've found is to add a counter to key and update counter to force a full redraw, but that looks ugly. I'd appreciate a more neat solution.