Help with selecting the best widgets for appraoch

I have 2 selectbox and 2 text_input and perhaps this is not the best approach so I’m open to a better configuration.

I’m ultimately looking to clear my text_input and selectbox fields and using a form was my preferred choice as it clears everything on submit. Unfortunately, I need to enhance my selectbox dropdowns to show a subset of data. For example, if dropdown A and option A are selected, it would create a subset of options in dropdown B. This all works fine if a form is not used since it relies on the submit button to change this data but I need the data to change as the user is selecting different options before submitting. I decided to move away from forms and instead use text_input and selectbox with a basic st.button instead. Now I’m able to get my dropdown to work dynamically as explained above but I’m unable to clear the text area after I submit.

I get this error:

StreamlitAPIException: st.session_state.area cannot be modified after the widget with key area is instantiated.

I found this thread that goes over how to configure a text area with session_state:

My problem was not adding on_change to the text_input or adding on_click to my st.button and passing in a function that clears the state. Once I added this, it worked, BUT I don’t want to clear these values until a separate function is called that requests data from API. I want to clear these text areas when this call is a success. So, calling the function that clears my session states outside of this widget attributes(on_change or on_click) is not working and gets:

StreamlitAPIException: st.session_state.area cannot be modified after the widget with key area is instantiated.

I wanted to provide as much detail as possible in case there’s a better approach to what I’m trying to configure and there’s another widget that can help with this. I simply just want to clear my values and I’m finding harder than is probably supposed to be.

PLEASE, I’ve been banging my head on this for a bit. ANY advice is welcomed as I’m new to Streamlit.

Thank you in advance!

Hold on, stop banging your head, I will take a look on your issue.

You have a separate function that calls api. You have to show us a minimal sample code. I want to see how your widgets are ordered, etc.

  • Select item
  • text_input
  • button

Does the button calls the api? And the selected item and text input are query parameters to the api function?

Hi @ferdy

First and foremost, I want to express my sincere gratitude for taking the time to review my code. Your effort and time are greatly appreciated.

Here is a template of the code containing generic information. The code is configured this way from top to bottom. I had to rewrite it specifically for this demonstration, but it follows the same flow as the original:

payload = {}

get_accounts_query = """ACCOUNT QUERY HERE"""

global_mapping_query = """GLOBAL MAPPINGS QUERY HERE"""


def query_execute(query, db):
    df = pd.read_sql(text(query), db)
    return df

@st.cache_data
def get_accounts():
    df = query_execute(get_accounts_query, engine1)
    return df


def format_global_df_mappings(global_df):
    # FUNCTION TO FORMAT GLOBAL MAPPINGS DATAFRAME
    return global_df
    
def create_config_mapping(df, account, input_text_1, dropdown1, dropdown2, input_text_2, bool1, bool2):
    key = f"{dropdown1}_{dropdown2}"
    payload['account'] = f"{account}"
    payload['input_text_1'] = f"{input_text_1}"
    payload['dropdown1'] = f"{dropdown1}"
    payload['dropdown2'] = f"{dropdown2}"
    payload['bool1'] = bool1
    payload['bool2'] = bool2
    
    if not df['key'].str.contains(key, case=False, na=False).any():
        if input_text_2:
            payload["input_text_2"] = f"{input_text_2}"
            # response = requests.post(url, json=payload, headers=headers)
            # return response.text
            return 'success'
        else:
            return 'input_text_2 required'    
    else:
        # response = requests.post(url, json=payload, headers=headers)   
        # return response.text
        return 'success'
df = pd.read_csv('test.csv')

grouped_dropdowns = df.groupby('dropdown1')['dropdown2'].apply(list).to_dict()


accounts = get_accounts() 
selected_option = st.selectbox("Account:", accounts['name'], index=None, placeholder="Please Select an Account")
global_mappings = query_execute(global_mapping_query, engine2)

create_checkbox= st.checkbox("Create Check Box", key='create_checkbox')
if 'input_text_1' or 'input_text_2' not in st.session_state:
    st.session_state.input = ''
if 'area' not in st.session_state:
    st.session_state.area = ''
    
def submit():
    st.session_state.input_text_1 = st.session_state.area
    st.session_state.area = ''
    st.session_state.input_text_2 = st.session_state.area
    st.session_state.area = ''
if create_checkbox:

    col1, col2, col3, col4 = st.columns(4)
    with col1:
        st.session_state["input_text_1_value"] = input_text_1 = st.text_input("input_text_1", key="input_text_1")

    with col2:    
        dropdown1 = st.selectbox('Select dropdown1', list(grouped_dropdowns.keys()), key='dropdown1')
    with col3:    
        if dropdown1:
            st.session_state.selected_dropdown1 = dropdown1
            dropdown2 = st.selectbox('Select Service Level', grouped_dropdowns [dropdown1], key='dropdown2')
            if dropdown2:
                st.session_state.selected_dropdown2 = dropdown2
    with col4:
        st.session_state["input_text_2_value"] = input_text_2 = st.text_input("input_text_2", key="input_text_2")
        
    bool1 = st.checkbox("B1", key='b1')
    bool2 = st.checkbox("B2", key='b2')    
    if st.button("Submit", key = 'button_submit', on_click=submit):
        if selected_option is not None:
            selected_slug = accounts[accounts['name'] == selected_option]['slug'].values[0]
            if all([input_text_1, dropdown1, dropdown2]):
                formatted_global_df = format_global_df_mappings(global_mappings)
                response = create_config_mapping(formatted_global_df, selected_slug, input_text_1, dropdown1, dropdown2, input_text_2, bool1, bool2)
                if response == 'input_text_2 required':
                    st.error("input_text_2 is required. A Global Mapping was not found.")
                if response == 'success':

                    st.success("Mapping created successfully!")
            else:
                st.error("Please fill in all text inputs.")
        else:
            st.error("Please select an account from the dropdown.")

The input_text_1 does not make it through the check:
if all([input_text_1, dropdown1, dropdown2]):

Ideally, I’d want the input_text_1 and input_text_2 but I know it fails because I’m calling submit() when I click the submit button. If I place the submit function after I receive a response I get:

StreamlitAPIException: st.session_state.area cannot be modified after the widget with key area is instantiated.

as mentioned before. Hopefully, you can analyze my code and offer guidance on structuring it. I look forward to your feedback!

I cannot run this sample code. There are missing objects such as engine1, etc.

You should provide a minimal example code, something that we can run and see the behavior.

For example in ,

def query_execute(query, db):
    df = pd.read_sql(text(query), db)
    return df

Just give us the value of query_execute() directly. Do you have issues in querying the db? If so, then solve it first. Solve issues one at a time.

Also in,

@st.cache_data
def get_accounts():
    df = query_execute(get_accounts_query, engine1)
    return df

Give us the value of get_accounts(). Is this also part of the issue?


Other observations.

df = pd.read_csv('test.csv')

Can you provide test.csv?


This one is incorrect.

if 'input_text_1' or 'input_text_2' not in st.session_state:
    st.session_state.input = ''

If you want to use input key you should use.

if 'input' not in st.session_state:
    st.session_state.input = ''

Have a look on how to use session state.


Hi Ferdy,

My apologies, like I mentioned earlier I had to strip down my code since it has sensitive data and provide just the relevant parts. With this feedback I can refine what’s needed for you to analyze further. I appreciate the feedback and will respond within the next couple of days with the required information you suggested. Again, thank you for taking the time to review my code.

Hi @ferdy

Here is the updated code. I just provided the dataframe with test data so the database queries aren’t needed. You should be able to copy the code now and replicate my problem.

import streamlit as st
import pandas as pd



payload = {}




def format_global_df_mappings(global_df):
    global_df['key'] = global_df['ColumnA'] + '_' + global_df['ColumnB']
    return global_df
    
def create_config_mapping(df, account, input_text_1, dropdown1, dropdown2, input_text_2, bool1, bool2):
    key = f"{dropdown1}_{dropdown2}"
    payload['account'] = f"{account}"
    payload['input_text_1'] = f"{input_text_1}"
    payload['dropdown1'] = f"{dropdown1}"
    payload['dropdown2'] = f"{dropdown2}"
    payload['bool1'] = bool1
    payload['bool2'] = bool2
    
    if not df['key'].str.contains(key, case=False, na=False).any():
        if input_text_2:
            payload["input_text_2"] = f"{input_text_2}"
            # response = requests.post(url, json=payload, headers=headers)
            # return response.text
            return 'success'
        else:
            return 'input_text_2 required'    
    else:
        # response = requests.post(url, json=payload, headers=headers)   
        # return response.text
        return 'success'
df  = pd.DataFrame({
    'dropdown1': ['AA', 'AA', 'BB', 'BB', 'CC', 'DD'],
    'dropdown2': ['11', '22', '11', '33', '44', '55']
})
global_mappings = pd.DataFrame({
    'ColumnA': ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'],
    'ColumnB': ['Apricot', 'Blueberry', 'Cranberry', 'Dragonfruit', 'Fig']
})
grouped_dropdowns = df.groupby('dropdown1')['dropdown2'].apply(list).to_dict()


accounts = pd.DataFrame({'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'], 'slug': ['alice', 'bob', 'charlie', 'david', 'eve']})
selected_option = st.selectbox("Account:", accounts['name'], index=None, placeholder="Please Select an Account")

create_checkbox= st.checkbox("Create Check Box", key='create_checkbox')
if 'input_text_1' or 'input_text_2' not in st.session_state:
    st.session_state.input = ''
if 'area' not in st.session_state:
    st.session_state.area = ''
    
def submit():
    st.session_state.input_text_1 = st.session_state.area
    st.session_state.area = ''
    st.session_state.input_text_2 = st.session_state.area
    st.session_state.area = ''
if create_checkbox:

    col1, col2, col3, col4 = st.columns(4)
    with col1:
        st.session_state["input_text_1_value"] = input_text_1 = st.text_input("input_text_1", key="input_text_1")

    with col2:    
        dropdown1 = st.selectbox('Select dropdown1', list(grouped_dropdowns.keys()), key='dropdown1')
    with col3:    
        if dropdown1:
            st.session_state.selected_dropdown1 = dropdown1
            dropdown2 = st.selectbox('Select Service Level', grouped_dropdowns [dropdown1], key='dropdown2')
            if dropdown2:
                st.session_state.selected_dropdown2 = dropdown2
    with col4:
        st.session_state["input_text_2_value"] = input_text_2 = st.text_input("input_text_2", key="input_text_2")
        
    bool1 = st.checkbox("B1", key='b1')
    bool2 = st.checkbox("B2", key='b2')    
    if st.button("Submit", key = 'button_submit', on_click=submit):
        if selected_option is not None:
            selected_slug = accounts[accounts['name'] == selected_option]['slug'].values[0]
            if all([input_text_1, dropdown1, dropdown2]):
                formatted_global_df = format_global_df_mappings(global_mappings)
                response = create_config_mapping(formatted_global_df, selected_slug, input_text_1, dropdown1, dropdown2, input_text_2, bool1, bool2)
                if response == 'input_text_2 required':
                    st.error("input_text_2 is required. A Global Mapping was not found.")
                if response == 'success':

                    st.success("Mapping created successfully!")
            else:
                st.error("Please fill in all text inputs.")
        else:
            st.error("Please select an account from the dropdown.")

I also saw your last comment about the session state needing to be:

if 'input' not in st.session_state:
    st.session_state.input = ''

The reason I used input_text_1 is because that is the key name for the widget. I thought this was the right approach but I could be wrong here.

Just to reiterate. My code fails because the submit function is called through st.button. When that happens, the input text values are cleared(as expected) and fails on this check:
if all([input_text_1, dropdown1, dropdown2]):
If I call the submit function elsewhere I get:
StreamlitAPIException: st.session_state.area cannot be modified after the widget with key area is instantiated.

I would like for the input text values to pass into create_config_mapping function and clear the text values after this request gets a 200 response.

Hopefully, this will give you the information you need to analyze further. If there’s anything else you need from me, please let me know. Again, I appreciate the time and effort you’ve taken here.

I see the difficulties in achieving the goal of this sample code. One is the use of variables and two is the clearing of the value (ui-wise) of the text input widget.

There are two methods to get the value of a widget such as text input widget.

  1. Get the return value
input_text_1 = st.text_input("input_text_1", key="input_text_1")
print(input_text)
  1. Get the value via the key of the widget using session state.
st.text_input("input_text_1", key="input_text_1")
print(st.session_state.input_text_1)

For example,

def create_config_mapping(df, account, input_text_1,
        dropdown1, dropdown2, input_text_2, bool1, bool2):
    key = f"{dropdown1}_{dropdown2}"
    payload['account'] = f"{account}"
    payload['input_text_1'] = f"{input_text_1}"
    payload['dropdown1'] = f"{dropdown1}"
    payload['dropdown2'] = f"{dropdown2}"
    payload['bool1'] = bool1
    payload['bool2'] = bool2

The parameters input_text_1, etc, is not necessary if you are using method 2. You can just use,

payload['input_text_1'] = st.session_state.input_text_1

And remove that parameter.

def create_config_mapping(df, account,
    dropdown1, dropdown2, input_text_2, bool1, bool2):

You can do the same for other similar parameters.

Method 2 is superior as you can use the value anywhere. It also eliminates a single variable creation as in method 1.

Clearing the value in the text input widget may not be that clear. We will have to create a fresh widget by changing its key.

st.text_input("input_text_1", key="input_text_1")

Now if we type “test” in that widget, we cannot reset it to empty or “” unless we delete the value of the widget manually on the actual widget.

Programmatically to clear that widget value, we have to create a different key.

st.text_input("input_text_1", key="input_text_1_gtu")

This is the new key, input_text_1_gtu.

But there is an issue on this, because we cannot always use input_text_1_gtu`.

The solution is to create a random key so that if the call is a success we will create a new text input widget with empty value. For this we will use the uuid module of python.

We need to make the key of the widget a variable and create a new key only if response is successful because we are clearing the value of the widget.

Example:

import uuid

# make sure that `input_text_1_k` is not in session state.
if 'input_text_1_k' not in st.session_state:
    st.session_state.input_text_1_k = str(uuid.uuid4())

st.text_input("input_text_1", key=st.session_state.input_text_1_k)

The key is a variable.

Other changes:

  1. Remove the submit callback function as everything are updated under the button if condition.
    if st.button("Submit", key='button_submit'):
        if selected_option is not None:
            selected_slug = accounts[accounts['name'] == selected_option]['slug'].values[0]

            if all([st.session_state[st.session_state.input_text_1_k],
                    dropdown1, dropdown2]):
                formatted_global_df = format_global_df_mappings(global_mappings)
                response = create_config_mapping(formatted_global_df,
                                                 selected_slug,
                                                 dropdown1, dropdown2, bool1, bool2)
...

  1. Create new random keys if response is sucessful. Also rerun to update the ui with latest states.
                if response == 'success':
                    st.success("Mapping created successfully!")
                    time.sleep(1)  # see the success message for 1 sec

                    # Clear the text input widgets by creating new keys.
                    st.session_state.input_text_1_k = str(uuid.uuid4())
                    st.session_state.input_text_2_k = str(uuid.uuid4())
                    
                    # Reruns to update the ui.
                    st.rerun()
  1. In the condition:
if all([st.session_state[st.session_state.input_text_1_k],
                    dropdown1, dropdown2]):

The st.session_state.input_text_1_k is the key of the widget. It is a variable so that we can clear its value. The value of this widget is: st.session_state[st.session_state.input_text_1_k]

Complete code

import uuid
import time

import streamlit as st
import pandas as pd


st.set_page_config(layout='centered')


# Define the variables for the text input widget keys.
# Make sure that the keys we are creating are not existing in the session state.
if 'input_text_1_k' not in st.session_state:
    st.session_state.input_text_1_k = str(uuid.uuid4())
if 'input_text_2_k' not in st.session_state:
    st.session_state.input_text_2_k = str(uuid.uuid4())


payload = {}


def format_global_df_mappings(global_df):
    global_df['key'] = global_df['ColumnA'] + '_' + global_df['ColumnB']
    return global_df
    

def create_config_mapping(df, account, dropdown1, dropdown2, bool1, bool2):
    key = f"{dropdown1}_{dropdown2}"
    payload['account'] = f"{account}"
    payload['input_text_1'] = st.session_state[st.session_state.input_text_1_k]
    payload['dropdown1'] = f"{dropdown1}"
    payload['dropdown2'] = f"{dropdown2}"
    payload['bool1'] = bool1
    payload['bool2'] = bool2
    
    if not df['key'].str.contains(key, case=False, na=False).any():
        if st.session_state[st.session_state.input_text_2_k]:
            payload["input_text_2"] = st.session_state[st.session_state.input_text_2_k]
            # response = requests.post(url, json=payload, headers=headers)
            # return response.text
            return 'success'
        else:
            return 'input_text_2 required'    
    else:
        # response = requests.post(url, json=payload, headers=headers)   
        # return response.text
        return 'success'
    

df  = pd.DataFrame({
    'dropdown1': ['AA', 'AA', 'BB', 'BB', 'CC', 'DD'],
    'dropdown2': ['11', '22', '11', '33', '44', '55']
})

global_mappings = pd.DataFrame({
    'ColumnA': ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'],
    'ColumnB': ['Apricot', 'Blueberry', 'Cranberry', 'Dragonfruit', 'Fig']
})

grouped_dropdowns = df.groupby('dropdown1')['dropdown2'].apply(list).to_dict()

accounts = pd.DataFrame({
    'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
    'slug': ['alice', 'bob', 'charlie', 'david', 'eve']}
)
selected_option = st.selectbox("Account:", accounts['name'],
                               index=None, placeholder="Please Select an Account")

create_checkbox= st.checkbox("Create Check Box", key='create_checkbox')
# if 'input_text_1' or 'input_text_2' not in st.session_state:
#    st.session_state.input = ''
# if 'area' not in st.session_state:
#    st.session_state.area = ''

if create_checkbox:
    col1, col2, col3, col4 = st.columns(4)

    with col1:
        st.text_input("input_text_1", key=st.session_state.input_text_1_k)

    with col2:    
        dropdown1 = st.selectbox('Select dropdown1', list(grouped_dropdowns.keys()),
                                 key='dropdown1')
    with col3:    
        if dropdown1:
            st.session_state.selected_dropdown1 = dropdown1
            dropdown2 = st.selectbox('Select Service Level',
                                     grouped_dropdowns [dropdown1],
                                     key='dropdown2')
            # if dropdown2:
            #     st.session_state.selected_dropdown2 = dropdown2

    with col4:
        st.text_input("input_text_2", key=st.session_state.input_text_2_k)
        
    bool1 = st.checkbox("B1", key='b1')
    bool2 = st.checkbox("B2", key='b2')  

    if st.button("Submit", key='button_submit'):
        if selected_option is not None:
            selected_slug = accounts[accounts['name'] == selected_option]['slug'].values[0]

            if all([st.session_state[st.session_state.input_text_1_k],
                    dropdown1, dropdown2]):
                formatted_global_df = format_global_df_mappings(global_mappings)
                response = create_config_mapping(formatted_global_df,
                                                 selected_slug,
                                                 dropdown1, dropdown2, bool1, bool2)

                if response == 'input_text_2 required':
                    st.error("input_text_2 is required. A Global Mapping was not found.")

                if response == 'success':
                    st.success("Mapping created successfully!")
                    time.sleep(1)  # see the success message for 1 sec

                    # Clear the text input widgets by creating new keys.
                    st.session_state.input_text_1_k = str(uuid.uuid4())
                    st.session_state.input_text_2_k = str(uuid.uuid4())
                    
                    # Reruns to update the ui.
                    st.rerun()
            else:
                st.error("Please fill in all text inputs.")
        else:
            st.error("Please select an account from the dropdown.")

Sample output

Note in the function:

def create_config_mapping(df, account,
    dropdown1, dropdown2, bool1, bool2):

You can remove dropdown1, dropdown2, bool1, and bool2 in the parameter. Inside this function, you can use st.session_state.dropdown1 and similar.

Hi @ferdy

This works exactly as I was hoping. Definitely, I learned something new here, and thank you for sharing this approach with me.

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.