After clicking a submit button, create another submit button and do something else when that button is clicked

Summary

Basically, once I click on a submit button, I want to create another submit button that does something if someone clicks on the following submit button. code below

if(st.button('Submit')):
    text_outputs = get_sentences_associated_with_phrase(nl_query)  # this returns a list
    with st.form("my_form"):#, clear_on_submit=True):
        for text_output in text_outputs:
            clicked = st.checkbox(text_output) # this creates check boxes with what was in text_boxes
            
        submitted = st.form_submit_button("Submit labels") 
        if submitted: # here is where I am stuck. This submit button doesn't work because it's nested
            st.write(clicked)

As you can see, I nested the second submit button

 if submitted: # here is where I am stuck. This submit button doesn't work because it's nested
        st.write(clicked)

And if I click on that submit button, the desired action can’t happen because it’s nested. I’m not sure how to achieve this. any help would be greatly appreciated. Also, let me know if anything wasn’t clear

Buttons won’t hold their states (they will only be true if they were the last thing touched). So if you want to nest things with buttons, you can create something else in session state to reference rather than the button values directly.

if 'stage' not in st.session_state:
    st.session_state.stage = 0

def set_stage(stage):
    st.session_state.stage = stage

# Some code
st.button('First Button', on_click=set_stage, args=(1,))

if st.session_state.stage > 0:
    # Some code
    st.button('Second Button', on_click=set_stage, args=(2,))
if st.session_state.stage > 1:
    # More code, etc
    st.button('Third Button', on_click=set_stage, args=(3,))
if st.session_state.stage > 2:
    st.write('The end')
st.button('Reset', on_click=set_stage, args=(0,))

2 Likes

So there is no way to keep track of the clicked checkboxes in the line

clicked = st.checkbox(text_output)

if it’s nested?

Checkboxes do have a state, so you could nest those just fine. (Though if you unchecked a parent, the children would be destroyed and wouldn’t remember their previous state upon recreation.) It’s just that with buttons, there’s no persistent state so you have to rely on something else in session_state if you’re going to nest them.

Ok I think I understand that. What I’m still stuck on is if I need to loop through a list, and create checkboxes with sentences in them. Example below

import streamlit as st

li = ['this is sentence 1', 'this is sentence 2']

with st.form("my_form"):
    for sentence in li:
        x = st.checkbox('good') # I know if make these different variables, this will work.  
        x = st.checkbox('bad') # but I can't do that because I'm looping
    submitted = st.form_submit_button("Submit labels")

if submitted:
    if some x was clicked:
        st.write(the x values that were clicked)

You’ll need to assign keys to things in your form so you have access to the information and save Streamlit from getting confused about identical widgets.

I have an example of collecting user information from a list of pictures that you may find some inspiration from. This example doesn’t use a form and collects the feedback into a dataframe with each user click.

https://mathcatsand-examples.streamlit.app/relabeling_images

Here’s something with a form. I included something conditional on the submit button click directly, as well as provided a way to record the submit status persistently for comparison.

import streamlit as st

if 'submitted' not in st.session_state:
    st.session_state.submitted = False

st.session_state

li = ['this is sentence 1', 'this is sentence 2']

def record_submitted():
    st.session_state.submitted = True

def reset():
    st.session_state.submitted = False

with st.form("my_form", clear_on_submit=True):
    for i in range(len(li)):
        st.write(li[i])
        st.radio("Choose",['Good','Bad'],key=f'sentence{i}', horizontal=True)
    submit = st.form_submit_button("Submit labels", on_click=record_submitted)

st.button('Do Nothing')

if submit:
    st.write('The last thing clicked was the submit button.')

if st.session_state.submitted:
    st.write('The last submission was:')
    for i in range(len(li)):
        st.write(f'{li[i]} : {st.session_state[f"sentence{i}"]}')

st.button('Reset',on_click=reset)

(edit to change app url)

1 Like

So just to clarify, there is absolutely no way to nest buttons to perform sequential operations? In the first example

submitted = st.form_submit_button("Submit labels")

was the last thing touched when the

 if submitted: # here is where I am stuck. This submit button doesn't work because it's nested
        st.write(clicked)

So not sure why the variable isn’t found

You can nest buttons, as long as you use something else along with them to track since they don’t themselves keep their clicked state as you go along. The code I provided first can be nested. The conditionals are based on the ‘stage’ which is derived from the button clicks. You can indent subsequent conditionals and it will work fine. You can also record a separate state for each button individually if you want to.

if 'stage' not in st.session_state:
    st.session_state.stage = 0

def set_stage(stage):
    st.session_state.stage = stage

# Some code
st.button('First Button', on_click=set_stage, args=(1,))

if st.session_state.stage > 0:
    # Some code
    st.button('Second Button', on_click=set_stage, args=(2,))
    if st.session_state.stage > 1:
        # More code, etc
        st.button('Third Button', on_click=set_stage, args=(3,))
        if st.session_state.stage > 2:
            st.write('The end')
st.button('Reset', on_click=set_stage, args=(0,))

Sorry, I don’t think the above use case really solves what I am trying to do. Below is my code

clicked = {}

if 'stage' not in st.session_state:
    st.session_state.stage = 0
    
def set_stage(stage):
    st.session_state.stage = stage
    
li = ['this is sentence 1', 'this is sentence 2']

if(st.button('Submit', on_click=set_stage, args=(1,))):
    with st.form("my_form"):#, clear_on_submit=True):
        for e, text_output in enumerate(text_outputs):

            clicked[e] = st.checkbox(text_output) # here I successfully create checkboxes with what's in the li list
        submitted = st.form_submit_button("Submit labels") #I then create a submit button for this respective form. Note this submit button is the last thing being called
        
        
if st.session_state.stage == 1:       
    print('do something') # this works 
    if submitted: # this is where it fails because it doesn't recognize submitted
        print('sub')

in the above example
if submitted: # this is where it fails because it doesn't recognize submitted
I don’t understand why submitted isn’t recognized.

The page reloads when you interact with anything, so when you click the second submit button, the page reloads, you skip over that section of code that assigns a value to submitted (because it only shows immediately after a click on the first button), and thus submitted it is not recognized.

This is why:

  1. You click Submit labels.
  2. The application reruns inmediately.
  3. The Submit button returns False.
  4. The code under the second if is not executed.
  5. submitted is not defined.
  6. Evaluating submitted raises a NameError.

So is there a way to save checkbox data being True or False without having to re run everything?

Not sure what you mean by that. If you store the data in session_state it will stay there for the whole session unless your code explicitly changes or deletes it.

Yes, but if I click on a text box, instated below

st.session_state.clicked = {}
st.session_state.clicked[0] = st.checkbox(text_output)

The value for it being clicked doesn’t seem to change

Make sure any instantiation is in a conditional so that it doesn’t reinitialize.

if 'clicked' not in st.session_state:
    st.session_state.clicked = {}
st.session_state.clicked[0] = st.checkbox(text_output)

This is exactly what my code is doing, yet when I print out st.session_state.clicked, it returns a dictionary with all the values set to False. Again I believe this is due to saving it inside of the st.session_state

I don’t know what you mean by “the value of being clicked” either. When st.session_state.clicked[0] = st.checkbox("some text") is executed, the value of the checkbox is assigned to st.session_state.clicked[0].

To clarify, this does not happen when you click the widget but when the assignment is executed. You click the checkbox, its value changes, the application is rerun, st.checkbox(“some text”) will return the new value the next time it is executed.

There are ways to store widget values in session_state right after you interact with them, before the application is rerun, like specifying a key for the widget or using callbacks. Unfortunately I don’t quite understand what you are tying to do here, so I am not sure how to adapt any of this to your use case.

below is my code

if 'stage' not in st.session_state:
    st.session_state.stage = 0

if 'clicked' not in st.session_state:
    st.session_state.clicked = {}
    
def set_stage(stage):
    st.session_state.stage = stage
    

if(st.button('Submit', on_click=set_stage, args=(1,))):
    outputs, text_outputs = get_sentences_associated_with_phrase(nl_query) # this calls a function that receives a list of sentences, such as ['sentence 1', 'sentence 2'']
    with st.form("my_form"):
        for e, text_output in enumerate(text_outputs):
            
            st.session_state.clicked[e] = st.checkbox(text_output) # st.session_state.clicked[e] is a dictionary holding the key with the value associated with it being true or false(clicked or not clicked)
            
        submitted = st.form_submit_button("Submit labels", on_click=set_stage, args=(2,))

# I'll talk about the below code at the end
if st.session_state.stage == 2:       
    st.write(st.session_state.clicked)

So as the hashtags state, st.session_state.clicked[e] is a dictionary holding the key with the value associated with it being true or false(clicked or not clicked). So it should look like below if nothing is clicked

{
0 : False
1: False
}

or if the first sentence is clicked, if I print it out, it should look like

{
0 : True
1: False
}

but If I, for example click on one of the text boxes, and print it out using below code, it

if st.session_state.stage == 2:       
    st.write(st.session_state.clicked)

All of the values in the dictionary are always false, no matter which ones were clicked

{
0 : False
1: False
}

Right. Inside a form it works in a different way. The widgets do not return the new values until the submit button is presed. But, in your case, neither the form nor the wigdets are there when the application reruns, so your dictionary is never assigned with the changed values.

One way to store the changed values is associating a unique key with each checkbox. Instead of assigning to clicked[e], use a key f"clicked_{e}".

At a minimum you also need to store the lenght of text_outputs so that later you know which keys you are interseted in. In my code I stored the whole list because I guess you want to use the strings in succesive reruns.

def set_stage(stage):
    st.session_state.stage = stage
    

if "stage" not in st.session_state:
    st.session_state.stage = 0

if(st.button('Submit', on_click=set_stage, args=(1,))):
    st.session_state.text_outputs = ["sentences", "associated", "with", "phrase"]
    with st.form("my_form"):
        for e, text_output in enumerate(st.session_state.text_outputs):
            st.checkbox(text_output, key=f"clicked_{e}")
        submitted = st.form_submit_button("Submit labels", on_click=set_stage, args=(2,))

if st.session_state["stage"] == 2:
    clicked = {
        e: st.session_state[f"clicked_{e}"]
        for e in range(len(st.session_state.text_outputs))
    }
    st.write(clicked)

Thanks, I think this solved it