Form Button Not Calling Function

Summary

My main code has a button inside an expander that when you click calls a function that generates a form and that form contains inputs for an email. When I click the send button on the form the function isn’t called and the application just closes all of the expanders and the form.

If I position the form outside the expander, as a stand alone expander it works fine, like so.

with st.form(key="email-form"):
    st.write("Request Change for CSV")
    receiver_email = st.text_input(label="Receiver's Email Address", placeholder="Please enter the receiver's email address")
    subject = st.text_input(label="Subject", placeholder="Please enter the subject of your email")
    text = st.text_area(label="Email Text", placeholder="Please enter your text here")
    uploaded_file = st.file_uploader("Attachment")

    submit_button = st.form_submit_button("Send")
    if submit_button:
        sender_email = st.secrets["sender_email"]
        sender_password = st.secrets["sender_password"]
        smtp_server = st.secrets["smtp_server"]
        smtp_port = st.secrets["smtp_port"]
        send_email(sender_email, sender_password, receiver_email, smtp_server, smtp_port, text, subject, uploaded_file)

Steps to reproduce

Code snippet:

# SENDS EMAIL TO THE USER AFTER SUBMITTING THE FORM
def send_email(sender, password, receiver, smtp_server, smtp_port, email_message, subject, attachment=None):
    message = MIMEMultipart()
    message['To'] = Header(receiver)
    message['From'] = Header(sender)
    message['Subject'] = Header(subject)
    message.attach(MIMEText(email_message, 'plain', 'utf-8'))
    if attachment:
        att = MIMEApplication(attachment.read(), _subtype="txt")
        att.add_header('Content-Disposition', 'attachment', filename=attachment.name)
        message.attach(att)
    server = smtplib.SMTP_SSL(smtp_server, smtp_port)
    server.ehlo()
    server.login(sender, password)
    text = message.as_string()
    server.sendmail(sender, receiver, text)
    server.quit()
# CREATES THE FORM THAT IS TO BE SUBMITTED
def create_request_change_form(blob_name):
    with st.form(key=f"request-change-form-{blob_name}"):
        st.write(f"Request Change for {blob_name}")
        receiver_email = st.text_input(label="Receiver's Email Address", placeholder="Please enter the receiver's email address")
        subject = st.text_input(label="Subject", placeholder="Please enter the subject of your email")
        text = st.text_area(label="Email Text", placeholder="Please enter your text here")
        uploaded_file = st.file_uploader("Attachment")

        submit_button = st.form_submit_button("Send")
        if submit_button:
            sender_email = st.secrets["sender_email"]
            sender_password = st.secrets["sender_password"]
            smtp_server = st.secrets["smtp_server"]
            smtp_port = st.secrets["smtp_port"]
            send_email(sender_email, sender_password, receiver_email, smtp_server, smtp_port, text, subject, uploaded_file)
            st.success('This is a success message!', icon="✅")


for deal_id, files in deal_id_to_files.items():
    deal_name = deal_id_to_name.get(deal_id, 'Unknown Deal')
    expander = st.expander(f"Deal Name: {deal_name}")
    with expander:
        for blob in files:
            bucket_name = blob.bucket.name
            st.write(f"{bucket_name}: {blob.name}")
            col1, col2, col3 = st.columns([1, 1, 1])
            with col1:
                if blob.name.endswith('.csv'):
                    download_button = st.button("Download CSV", key=f"download-{blob.name}")
                    if download_button:
                        download_csv(blob)
                elif blob.name.endswith('.pdf'):
                    download_button = st.button("Download PDF", key=f"download-{blob.name}")
                    if download_button:
                        download_pdf(blob)
            with col2:
                if blob.name.endswith('.csv'):
                    approve_button = st.button("Approve CSV", key=f"approve-{blob.name}",
                        help="Click to approve the CSV and upload to approved bucket")
                    if approve_button:
                        approve_csv(blob, approved_csv_buckets, client)
                        update_blob_metadata(blob, "approved")
                elif blob.name.endswith('.pdf'):
                    approve_button = st.button("Approve PDF", key=f"approve-{blob.name}",
                        help="Click to approve the PDF and upload to approved bucket")
                    if approve_button:
                        approve_pdf(blob, approved_pdf_buckets, client)
                        update_blob_metadata(blob, "approved")
            with col3:
                request_change_button = st.button("Request Change", key=f"request-change-{blob.name}",
                    help="Click to request a change for this file")
                if request_change_button:
                    create_request_change_form(blob.name)
                    update_blob_metadata(blob, "requested_change")

Expected behavior:

After clicking the send button an st.success message should appear.

Actual behavior:

The function isn’t being called and the expanders just close along with the form.

You have nested buttons. A button does not have state. It will return True only on the page load resulting from its click, then go back to False. So if you have a form with its submit button nested inside some other parent button, the form will go away and not exist as soon as you click submit because the parent button will go back to False.

I’ve got a blog post covering it as a common issue of you want to check it out. It explains some options for how deal with it.

1 Like

I implemented the stateful buttons from extras however they don’t seem to accept custom keys:

for deal_id, files in deal_id_to_files.items():
    deal_name = deal_id_to_name.get(deal_id, 'Unknown Deal')
    expander = st.expander(f"Deal Name: {deal_name}")
    with expander:
        for blob in files:
            bucket_name = blob.bucket.name
            st.write(f"{bucket_name}: {blob.name}")
            col1, col2, col3 = st.columns([1, 1, 1])
            with col1:
                if blob.name.endswith('.csv'):
                    download_button = button("Download CSV", key=f"download-{uuid.uuid4()}")
                    if download_button:
                        download_csv(blob)
                elif blob.name.endswith('.pdf'):
                    download_button = button("Download PDF", key=f"download-{uuid.uuid4()}")
                    if download_button:
                        download_pdf(blob)
            with col2:
                if blob.name.endswith('.csv'):
                    approve_button = button("Approve CSV", key=f"approve-{uuid.uuid4()}",
                        help="Click to approve the CSV and upload to approved bucket")
                    if approve_button:
                        approve_csv(blob, approved_csv_buckets, client)
                        update_blob_metadata(blob, "approved")
                elif blob.name.endswith('.pdf'):
                    approve_button = button("Approve PDF", key=f"approve-{uuid.uuid4()}",
                        help="Click to approve the PDF and upload to approved bucket")
                    if approve_button:
                        approve_pdf(blob, approved_pdf_buckets, client)
                        update_blob_metadata(blob, "approved")
            with col3:
                request_change_button = button("Request Change", key=f"request-change-{uuid.uuid4()}",
                    help="Click to request a change for this file")
                if request_change_button:
                    create_request_change_form(blob.name)
                    update_blob_metadata(blob, "requested_change")
streamlit.errors.DuplicateWidgetID: There are multiple identical `st.button` widgets with the
same generated key.
When a widget is created, it's assigned an internal key based on
its structure. Multiple widgets with an identical structure will
result in the same internal key, which causes this error.
To fix this error, please pass a unique `key` argument to
`st.button`.