How to create recurring forms with unique keys?

Hi,

Iā€™m creating a simple web app, which sort of is like google forms. Based on a variable, say x, from needs to be submitted x times. Hereā€™s a toy code snippet:

x = 10

def my_form(count):
    st.session_state.a_list = []
    with st.form(key=f"{count}"):
        # Question 1 - text_input - st.session_state.a_list.append(q1)
        ....
        # Question 5 - radio button st.session_state.a_list.append(q5)
        
        st.form_submit_button(label="Next")

# I could use a loop like this
for i in range(1, x+1):
    my_form(i)
# But it would print out all of the form, x times in a single page.

When the Next button is clicked in the form, itā€™s supposed to clear the page and repeat all the questions.

Iā€™ve been reading about Store Information Across App Interactions | Session State, Session State - Streamlit Docs and Multi-page App with Session State,

So I tried to use callback on the same function, something like this:

st.session_state.a_list = []

def my_form(count):
    if count <= 1:
        return

    with st.form(key=f"{count}"):
        ....
        
        st.form_submit_button(label="Next", onclick=my_form, args=(count-1,))

But I couldnā€™t figure out how to do implement it correctly, it would end up creating form (or sub components) with same keys.

Please note the way Iā€™m taking x is via streamlit itself (with st.number_input), so that component should disappear once I receive the x value, and open up a page with form questions.

Hereā€™s the source code.

Hey @joe733,

What is your use case for this? Do you imagine one individual having to run through your form 10 times answering the same questions? What is the end goal of your app?

Happy Streamlit-ing!
Marisa

1 Like

Hi @Marisa_Smith, yea thatā€™s true, Iā€™m creating a data collection form, so one form submission corresponds to one member of the family. The questions are repeated for each member of the family.

Hey @joe733,

If I understand correctly what you are trying to do I donā€™t think you want/need this counter. The design of st.form will do what youā€™re looking for without this.

It seems you are creating a dataset, from users who go to your app and input themselves and their familyā€™s into your database.

To truly do this you will need to set up a connection to a database system where you can store all the data as a new line/row in your data frame. I think the easiest way is to use google sheets (either a public or private one), there are tutorials in our docs for both here:
https://docs.streamlit.io/en/stable/tutorial/databases.html

Then, when the person fills in their data and hits the st.form_submit_button you can actually have streamlit submit their entry to the google sheet and then clear the data from the form so they can easily start over with another family member by simply using the clear_on_submit paramter like so:

st.form("my_form", clear_on_submit=True)

Check out our docs on this here: API reference ā€” Streamlit 0.85.1 documentation

Happy Streamlit-ing!
Marisa

1 Like

Yup, thatā€™s I want to do. Iā€™ll elaborate.

  • One person from each family enters the survey.
  • On page 1, they enter family name and number of members.
  • On clicking Nextā€¦
    1. I plan to use the DriveAPI to create a folder for this family, say FR 001 Family Name.
    2. Based on the number of family members, say n, n sub-folders are created.
    3. And the questions in the form are asked n times.
  • After which, a summary is tabulated with a Submit button below. The tabulated summary is saved as a google sheet.

Yea, I can use clear_on_submit butā€¦

  1. I canā€™t clear the page after asking the first two questions (family name & number of members)
  2. In case some data needs to be edited (while filling out the form), itā€™s good to have a way to go back to the previous memberā€™s entries.

Oohā€¦ I just had a thought. I could potentially do this, if I can add a navbar / radio buttons dynamically in the sidebarā€¦ something like this:

Sorry for my bad wire-frame, not a frontend developer yet. Which is why Streamlit, it handles the UI fantastically.

Soā€¦ is that possible with the current state of Streamlit? I can do the programming, but I want to know,

Thank you.

Hey @joe733! Sorry for my delay yesterday was super busy!

OOOooOOOooooOOO gotcha!

kkkkk yes let me think about thisā€¦

I think the best method for this is by using st.session_state. I do think itā€™s going to be slightly tricky to get done but between the two of us, Iā€™m sure we can get this to work!

The first page would be a form with just the 2 questions where you would save those values in your st.session_state so you can access them later.

Then depending on the number of family members, you can create a loop (or radio buttons as you mocked up) so that the person inputs the details for each family member into a different form. Once their details have been added, you can save them to their own folder as a .txt file or CSV (whatever your preference). You will need to be careful and check if the file paths exist already so youā€™re not overwriting any data. These parts would be done with the os package, and with some quick searching, I found a very helpful like about using os to create directories through python scripts!

Doing the loop solution is slightly more tricky, I started mocking up a solution for you (but got interrupted with how busy my week has been). Iā€™m going to add my code below so you have an example if you were curious, but its not complete yet!

import streamlit as st
import os

st.title("Family form")
# container to hold the 1st form
container_1 = st.empty()

# the counter that you will use to compare with the number of people
#  in the family will reset this to 0 to finish/restart process
if "counter" not in st.session_state:
    st.session_state["counter"] = 0
# form 1 submission tracker reset this to finish/restart process
if "form_1" not in st.session_state:
    st.session_state["form_1"] = False

#first form appears if counter is set or reset to 0
if st.session_state["counter"] == 0:
    with container_1:
        name_form = st.form("Family details", clear_on_submit=True)
        with name_form:
# having keys for each input in the form will auto save them to our state!
            family = st.text_input("Family Name", key="fam_name")
            fam_num = st.number_input("Number of Family Members", 1, key="fam_num")
            fam_submit = st.form_submit_button("Next")

st.session_state #prints state to our app while we are creating the solution

if fam_submit:
    container_1.empty() #clears the 1st form!
# we will use "Form_1" entry in state while we enter individuals data
    st.session_state["form_1"] = True
    ####
    # add in solution using the os package that will create
    # a directory based on family name

if st.session_state["form_1"]: # have to have done the 1st form
    while st.session_state["counter"] < fam_num:
        st.session_state["counter"] += 1
        form_title = "family member {} of {}".format(st.session_state["counter"], fam_num)
        details = st.form(form_title, clear_on_submit=True)

        with details:
            first_name = st.text_input("First Name", key="Fist_name")
            age = st.number_input("age",30, key="age")
            ### etc...
            detail_submit = st.form_submit_button("Finish")

        if detail_submit:
            ## here make sub directories with the name and save
            # a file with the info for later!  

# when finished with famliy members reset counter to 0
# and the form_1 submission tracker to false
    st.session_state
    if st.session_state["counter"] == fam_num:
        st.session_state["counter"] = 0
        st.session_state["form_1"] = False

As I said itā€™s not finished but itā€™s a good start. Right now on this line:

if fam_submit:
    container_1.empty() #clears the 1st form!

You will likely see an error Bad message format:'setIn' cannot be called on an ElementNode. This is already flagged with the team and we are currently working on a solution. Right now the way I am getting around it is just by hitting the ā€œdoneā€ button and the app continues (cause there is no ā€œrealā€ error here because we are using values we have stored in our state which are not forgotten on the next run).

Hopefully, this will be pushed before your app needs to go public. BUT in case it isnā€™t, the next best solution (I think) is to put your 1st form in the sidebar, and then once itā€™s been filled out, the app follows the same logic and the 2nd form appears! (itā€™s exactly the same logic just the form appears in a slightly different place while we sort that error message out!)

In this example, I was trying to do it the more complicated way but the radio buttons in the sidebar are a great solution too! And now with state, you can even have a ā€œnextā€ option button that will advance the radio buttons for you!

Here is a mock-up of that:

option_names = ["a", "b", "c"]

output_container = st.empty()

next = st.button("Next/save")

if next:
    if st.session_state["radio_option"] == 'a':
        st.session_state.radio_option = 'b'
    elif st.session_state["radio_option"] == 'b':
        st.session_state.radio_option = 'c'
    else:
        st.session_state.radio_option = 'a'

option = st.sidebar.radio("Pick an option", option_names , key="radio_option")
st.session_state

if option == 'a':
    output_container.write("You picked 'a' :smile:")
elif option == 'b':
    output_container.write("You picked 'b' :heart:")
else:
    output_container.write("You picked 'c' :rocket:")

Hopefully, this helps! :smiling_face_with_three_hearts:
Happy Streamlit-ing!
Marisa

1 Like

Hey @joe733! Sorry for my delay yesterday was super busy!

Itā€™s totally fine, I understand.

I think the best method for this is by using st.session_state . I do think itā€™s going to be slightly tricky to get done but between the two of us, Iā€™m sure we can get this to work!

The first page would be a form with just the 2 questions where you would save those values in your st.session_state so you can access them later.
ā€¦

Wow, thatā€™s a good headstart! Need to wrap my head around a few thingsā€¦ They told me the format of the form has some changes. Iā€™m not sure how much.

But thank you so much! Iā€™ll go through them, considering those the changes and would update here If I need more help. Thanks a lot!

1 Like

Of course @joe733 no problem!

Happy Streamlit-ing!
Marisa

1 Like

Hi @Marisa_Smith, Iā€™m almost thereā€¦ but with a few hiccups

Motivated by https://www.youtube.com/watch?v=nSw96qUbK9o I have to following setup.

- main_fd.py
- multiapp.py
- memberX.py # Individual form

When main_df.py is run it creates n copies of memberX.py and adds them to mutiapp.py which in turn executes a function called app() defined in each memberX.py (X = 1, 2, 3...n).

But when I click on the option say Member 3, the right side goes blank, instead of the expected form (or content as shown in the YouTube video).

Before clicking Member 2 option

After clicking Member 2 option

Also since the number of options in the sidebar are dynamically created I cannot do thisā€¦

if option == 'a':
   output_container.write("You picked 'a' :smile:")
elif option == 'b':
   output_container.write("You picked 'b' :heart:")
else:
   output_container.write("You picked 'c' :rocket:")

Here are the files:

Where do you think Iā€™m going wrong?