Button to add new row of inputs

Summary

I’m working on a budget app and I want to be able to click an ‘add’ button and add more inputs for the user below the existing.

Steps to reproduce

Code snippet:

def tab2_layout():
    col1, col2, col3, col4 = st.columns(4)

    def add_more():
        with col1:
            st.write('text')
        with col2:
            st.write('text')
        with col3:
            st.write('text')
        with col4:
            st.write('text')

    with col1:
        st.write('text')
    with col2:
        st.write('text')
    with col3:
        st.write('text')
    with col4:
        st.write('text')

    if st.button('add'):
        add_more()

tab1, tab2 = st.tabs(['Expenses', 'Savings'])
with tab1:
    tab1_layout()

with tab2:
    tab2_layout()

Expected behavior:

I expected this to add a new row of ‘text’ each time I click on add.

Actual behavior:

It does add 1 new row of ‘text’ but after that it doesn’t continue to add any additional rows.

Debug info

  • Python version: 3.10
  • OS version: Windows 11
  • Browser version: Firefox

Every time you click a button or interact with a widget, the page is going to reload and rerun your script from the top. If you have any changes that you want to accumulate, you will have to use session_state to give streamlit some memory into previous executions of the script.

Here are two solutions, one using a dataframe which is easier to work with and another just keeping track of the info in strings. In both cases, there is a form to accept the new data and when the button is clicked, a callback function is used to record the info into session_state so that it can survive the whole script being rerun from top to bottom.

https://mathcatsand-streamlit-mechanics-examples-home-hgd8b9.streamlit.app/add_data

import streamlit as st
import pandas as pd

st.write('# Solution using a dataframe')

if 'data' not in st.session_state:
    data = pd.DataFrame({'colA':[],'colB':[],'colC':[],'colD':[]})
    st.session_state.data = data

data = st.session_state.data

st.dataframe(data)

def add_dfForm():
    row = pd.DataFrame({'colA':[st.session_state.input_colA],
            'colB':[st.session_state.input_colB],
            'colC':[st.session_state.input_colC],
            'colD':[st.session_state.input_colD]})
    st.session_state.data = pd.concat([st.session_state.data, row])


dfForm = st.form(key='dfForm')
with dfForm:
    dfColumns = st.columns(4)
    with dfColumns[0]:
        st.text_input('colA', key='input_colA')
    with dfColumns[1]:
        st.text_input('colB', key='input_colB')
    with dfColumns[2]:
        st.text_input('colC', key='input_colC')
    with dfColumns[3]:
        st.text_input('colD', key='input_colD')
    st.form_submit_button(on_click=add_dfForm)
import streamlit as st

st.write('# Solution without using a dataframe')

if 'col1' not in st.session_state:
    st.session_state.col1 = ''
if 'col2' not in st.session_state:
    st.session_state.col2 = ''
if 'col3' not in st.session_state:
    st.session_state.col3 = ''
if 'col4' not in st.session_state:
    st.session_state.col4 = ''

dataColumns = st.columns(4)
with dataColumns[0]:
    st.write('#### col1')
    st.session_state.col1
with dataColumns[1]:
    st.write('#### col2')
    st.session_state.col2
with dataColumns[2]:
    st.write('#### col3')
    st.session_state.col3
with dataColumns[3]:
    st.write('#### col4')
    st.session_state.col4

def add_txtForm():
    st.session_state.col1 += (st.session_state.input_col1 + '  \n')
    st.session_state.col2 += (st.session_state.input_col2 + '  \n')
    st.session_state.col3 += (st.session_state.input_col3 + '  \n')
    st.session_state.col4 += (st.session_state.input_col4 + '  \n')

txtForm = st.form(key='txtForm')
with txtForm:
    txtColumns = st.columns(4)
    with txtColumns[0]:
        st.text_input('col1', key='input_col1')
    with txtColumns[1]:
        st.text_input('col2', key='input_col2')
    with txtColumns[2]:
        st.text_input('col3', key='input_col3')
    with txtColumns[3]:
        st.text_input('col4', key='input_col4')
    st.form_submit_button(on_click=add_txtForm)    

I used the previous example hoping it would be enough to wrap my head around what I needed to do but I’m a bit more confused now :smiley:

Here is what it really looks like right now. Similar issue as mentioned where it only adds 1 row but I want it to be input fields for the users to be added each time. I think what you shared is still needed I just need to wrap my head around how to implement that into this.

def add_rows():
    a = 0
    dataColumns = st.columns(4)
    with dataColumns[0]:
        st.text_input(label='Expense Name',
                      label_visibility='hidden', placeholder='Expense Name', key=f'{a+1}')

    with dataColumns[1]:
        amount = st.number_input(label='Amount Spend',
                                 label_visibility='hidden', min_value=0, key=f'{a+2}')

    with dataColumns[2]:
        budget = st.number_input(
            label='Budgeted', label_visibility='hidden', min_value=0, key=f'{a+3}')

    with dataColumns[3]:
        variance = budget-amount
        st.text_input(label='Variance Data', label_visibility='hidden',
                      placeholder=f'{variance}', key=f'{a+4}', disabled=True)


def tab1_layout():
    if st.button('Add Row'):
        add_rows()

    dataColumns = st.columns(4)
    with dataColumns[0]:
        st.subheader('Expenses')
        st.text_input(label='Expense Name',
                      label_visibility='hidden', placeholder='Expense Name')

    with dataColumns[1]:
        st.subheader('Amount Spent')
        amount = st.number_input(label='Amount Spend',
                                 label_visibility='hidden', min_value=0)

    with dataColumns[2]:
        st.subheader('Budgeted')
        budget = st.number_input(
            label='Budgeted', label_visibility='hidden', min_value=0,)

    with dataColumns[3]:
        variance = budget-amount
        st.subheader('Variance')
        st.text_input(label='Variance Data', label_visibility='hidden',
                      placeholder=f'{variance}', disabled=True)


tab1_layout()

Here’s what you’ve posted, simplified down to just one item, with a completely empty button added to help illustrate:

import streamlit as st

# This button has no logic or action associated to it
st.button('Do Nothing')

def add_rows():
    st.text_input(label='Expense Name', label_visibility='hidden', 
                  placeholder='Expense Name', key='1')

def tab1_layout():
    # This says to show another input if the 'Add Row' button was the last thing clicked, 
    # but this is not cumulative. This is a single widget with an on/off switch so it will 
    # show or hide; it will not persist.
    if st.button('Add Row'):
        add_rows()

    st.subheader('Expenses')
    st.text_input(label='Expense Name',
                      label_visibility='hidden', placeholder='Expense Name')

tab1_layout()

There is no place for the new information to go. It’s not getting recorded into a file or saved into a variable. The reason it kind of looks like it “works the first time” is that all input widgets have a “built-in memory” since their value gets recorded via their key into session_state as a core functionality in Streamlit. (I see there is also a bug getting triggered where you can see a ‘phantom’ or ‘ghost’ of a previous text entry. If you notice this, it is not intended behavior.)

So when you interact with something on the page, it reloads. You will see widgets remembering what you last entered, but the information hasn’t been recorded or added to anything. In this simplified example, your page has two input widgets. The one inside tab1_layout will always show, and have a memory of what was last entered into it, although it’s not doing anything with the entry. The second is the one generated from the add_rows function, which will show if the last interaction with the page was the Add Rows button and will not show otherwise. (This is why I added the Do Nothing button so you can see that second input disappear if you interact with anything else on the page at all.

Didn’t mean to delete that but how do I go about resolving this type of problem? I see what you mean, I tried this code and ran into the same issue.

import streamlit as st
import random
import string

st.session_state

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

dataColumns = st.columns(2)

with dataColumns[0]:
    st.text_input('label1', key='la1')

with dataColumns[1]:
    st.text_input('label2', key='la2')


def add_row():

    with dataColumns[0]:
        st.text_input(
            'label1', key=f'{random.choice(string.ascii_lowercase)+str(random.randint(0,999))}')

    with dataColumns[1]:
        st.text_input(
            'label2', key=f'{random.choice(string.ascii_lowercase)+str(random.randint(0,999))}')


if st.button('Add'):
    st.session_state.count += 1
    #add_row()
    if st.session_state.count > 1:
        for i in range(st.session_state.count-1):
            add_row()

Let’s start with the basics. What kind of object would you like to hold this data that users will add to? I recommend a dataframe as the easiest, but we can work with text/strings if that’s your need or preference.

You’ll need to initiate that (data) object and store it in session_state so we can add on to it.

This was the part of the code I used in my first answer to do that. It creates an empty dataframe the first time the page is opened but skips that creation to use whatever data already exists if the user has already entered some data.

I tried running just this and get an error each time:

AttributeError: st.session_state has no attribute "data". Did you forget to initialize it?

The thought of the app is that you’d have a text input for the expense name, and 3 number inputs (2 to enter ints, 1 to display the variance, it will be disabled).

The user will need more than 1 row of Expense name, amount, budget, and variance though which is what I was trying to accomplish. They would click ‘add item’ and another row of inputs would appear to enter the data for a new expense.

If I can get the code you posted to work I’ll try to figure this out.

Ok, here are two ways: a further clarification of what I was suggesting before, more tailored to your case, and another one based on your further clarification.

First Case

You have a dataframe that starts off empty, each click of the Submit button records a new line into that dataframe (housed safely in session_state so that it survives). So even though the page reloads with each interaction from the user, it’s grabbing the information from session_state to cumulatively build the data.

import streamlit as st
import pandas as pd

st.write('# Solution using a dataframe')

# initialize the empty data frame on first page load
if 'data' not in st.session_state:
    data = pd.DataFrame({'Expense':[],'Amount':[],'Budget':[],'Variance':[]})
    st.session_state.data = data

# show current data (will be empty to first time the page is opened, but will then show the
# incrementally built table of data with each user interactions
st.dataframe(st.session_state.data)

# this is the function the sends the information to that dataframe when called
# variance is calculated at this point
def add_dfForm():
    row = pd.DataFrame({'Expense':[st.session_state.input_expense],
            'Amount':[st.session_state.input_amount],
            'Budget':[st.session_state.input_budget],
            'Variance':[st.session_state.input_budget-st.session_state.input_amount]})
    st.session_state.data = pd.concat([st.session_state.data, row])

# here's the place for the user to specify information
dfForm = st.form(clear_on_submit=True, key='dfForm')
with dfForm:
    dfColumns = st.columns(4)
    with dfColumns[0]:
        st.text_input('Expense', key='input_expense')
    with dfColumns[1]:
        st.number_input('Amount', key='input_amount')
    with dfColumns[2]:
        st.number_input('Budget', key='input_budget')
    with dfColumns[3]:
        # this button calls the add_dfForm funciton to add data when clicked
        # after add_dfForm runs, the page reloads from the top, rerunning and overwriting everything
        st.form_submit_button(on_click=add_dfForm)

Second Case

You are on the right track looping through with a counter to create a list of inputs and include a way of making the keys unique. You don’t want those keys to be random though, since each interaction will reload the page, regenerate the random key, and thus create a new, naive widget. (There is a time and place for using a time stamp in a key if you ever need to forcibly destroy and recreate a specific widget. The disabled variance button didn’t seem to have any trouble updating without that extra step in this case though.)

# a selection for the user to specify the number of rows
num_rows = st.slider('Number of Rows', min_value=1,max_value=10,value=1)

# columns to lay out the inputs
grid = st.columns(4)

def add_row(row):
    with grid[0]:
        st.text_input('Expense', key=f'input_expense{row}')
    with grid[1]:
        st.number_input('Amount', key=f'input_amount{row}')
    with grid[2]:
        st.number_input('Budget', key=f'input_budget{row}')
    with grid[3]:
        st.number_input('Variance', key=f'input_variance{row}',
                        value = st.session_state[f'input_budget{row}'] \
                               -st.session_state[f'input_amount{row}'],
                        disabled=True)

for r in range(num_rows):
    add_row(r)

image