Button to delete streamlit containers

Summary

Hello every one, I am having a page in which user can add QnA with multiple questions and single answer. Currently how it works is I am having add and delete QnA button in the side bar from where we can add or delete it but only in last.


but with this we cannot delete a QnA in the mid somewhere which is a big problem as if there are 100 of these QnA and I want to delete 60th one I have to delete last 40 first. For this I want to add delete button after every container with which I can delete any QnA on any position. And the same problem goes with add or delete questions in each QnA. How can I achieve this?

Steps to reproduce

Code snippet:

import streamlit as st
from streamlit_javascript import st_javascript
import csv
import time
import ftplib
import urllib.request
import json

st.set_page_config(layout="wide")

st.title('Knowledge Base')

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

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

for k, v in st.session_state.items():
    st.session_state[k] = v

st.session_state["changedcard"] = st_javascript(f"JSON.parse(sessionStorage.getItem('changedcard'));")       
if st.session_state["changedcard"] == 0:
    pass
else:
    st_javascript(f"(sessionStorage.removeItem('changedcard'));")
    st_javascript(f"(sessionStorage.removeItem('editcard'));")
    testv = json.dumps(st.session_state["changedcard"])
    st.session_state[st.session_state["editcard"]] = testv


with st.sidebar:
    if st.button("Add QnA"):
        st.session_state["num_questions"] += 1
    else:
        pass
    if st.button("Delete QnA"):
        st.session_state["num_questions"] -= 1
    else:
        pass
    

for i in range(st.session_state["num_questions"]):
    if f"qa_num_{i}" not in st.session_state:
        st.session_state[f"qa_num_{i}"] = 0

    if f"ans_{i}" not in st.session_state:
        st.session_state[f"ans_{i}"] = ""

    con = st.container()
    if con.button(f"{i}.Add Questions"):
        st.session_state[f"qa_num_{i}"] += 1
    else:
        pass
    if con.button(f"{i}.Delete Questions"):
        st.session_state[f"qa_num_{i}"] -= 1
    else:
        pass
    # qa_num = con.number_input(
    #     str(i + 1) + ". Add or remove Questions",
    #     min_value=0,
    #     step=1,
    #     key=f"qa_num_{i}",
    # )
    col1, col2 = con.columns([5, 5])
    for j in range(st.session_state[f"qa_num_{i}"]):
        if f"question_{i}_{j}" not in st.session_state:
            st.session_state[f"question_{i}_{j}"] = ""

        col1.text_input(
            "Questions",
            label_visibility="visible" if j == 0 else "collapsed",
            key=f"question_{i}_{j}",
        )

    with col2:
        if f"ans_{i}" not in st.session_state:
            st.session_state[f"ans_{i}"] = ''
        if st.session_state[f"ans_{i}"] == '':
            if st.button(f"{i+1}.Create card"):
                st.session_state.refresh=1
                st.session_state[f"ans_{i}"] = '''{
                    "type": "AdaptiveCard",
                    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
                    "version": "1.6",
                    "body": []
                }'''
            else:
                pass
        else:
            pass
        
        try:
            ans=json.loads(st.session_state[f"ans_{i}"])
            k = "editcard"
            adata = json.dumps(ans)
            if st.button(f"{i+1}.Edit"):
                st_javascript(f"sessionStorage.setItem('{k}', JSON.stringify({adata}));")
                st.session_state["editcard"] = f"ans_{i}"
            else:
                pass
            if st.button(f"{i+1}.Delete card"):
                st.session_state.refresh=1
                st.session_state[f"ans_{i}"] = ''
            else:
                pass
        except:
            st.text_area(
                "Answer",
                key=f"ans_{i}",
                )  
if st.session_state.refresh==1:
    st.session_state.refresh = 0
    time.sleep(3)
    st.experimental_rerun()

Debug info

  • Streamlit version: 1.14.0
  • Python version: 3.8.0
  • Using Conda

@blackary or anyone else can you please help?

Since you are saving your questions and answers to session state by number, that will create difficulty if you want to delete from the middle since your loop is just blindly working through 1, 2, 3, …

Alternative 1: You can keep a list of question/answer indexes, and loop through that list rather than a complete range. The delete button would remove the index from the list of active questions (possibly deleting the associated data in session state, but not decrement num_questions as you’d want to keep creating new indexes for new questions if you wanted to avoid shifting around all your other data.

Alternative 2: You could use something like pandas to keep your list of data and iterate directly through your items. It could handle all the indexing for you and give you a simpler method or removing or sorting your questions.

1 Like

@mathcatsand Thank you. Can you please give a coding example for my reference to better understand the approach you are suggesting.

I’ve made an example and hosted it.

Code: Streamlit-Mechanics-Examples/qna_maker.py at main · MathCatsAnd/Streamlit-Mechanics-Examples · GitHub

App: https://mathcatsand-examples.streamlit.app/qna_maker

Thank you so much @mathcatsand I am trying your code and approach.

@mathcatsand I tried this approach as well can this work? because it is not allowing me to delete containers.

import streamlit as st
from streamlit_javascript import st_javascript
import csv
import time
import ftplib
import urllib.request
import json

st.set_page_config(layout="wide")
# with open("designing.css") as source_des:
#     st.markdown(f"<style>{source_des.read()}</style>",unsafe_allow_html=True)
st.title('Knowledge Base')

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

if "question" not in st.session_state:
    st.session_state.question = []

if "answer" not in st.session_state:
    st.session_state.answer = []

if "qindex" not in st.session_state:
    st.session_state.qindex = []

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

for k, v in st.session_state.items():
    st.session_state[k] = v

head1, head2 = st.columns([3,3])
if head1.button('Load'):
    head1.write('File Loaded')
else:
    pass

if head2.button('Save'):
    head2.write('File Saved')
else:
    pass

with st.sidebar:
    if st.button("Add QnA"):
        st.session_state["num_questions"] += 1
    else:
        pass
    
for i in range(st.session_state["num_questions"]):
    
    if i >= len(st.session_state.question):
        st.session_state.question.append([])
    
    if i >= len(st.session_state.answer):
        st.session_state.answer.append('')
    
    if i >= len(st.session_state.qindex):
        st.session_state.qindex.append(0)

    con = st.container()
    if con.button(f"{i}.Add Questions"):
        st.session_state.qindex[i] += 1
    else:
        pass
    if con.button(f"{i}.Delete Questions"):
        st.session_state.qindex[i] -= 1
    else:
        pass
    
    col1, col2 = con.columns([5, 5])
    for j in range(st.session_state.qindex[i]):
        if j >= len(st.session_state.question[i]):
            st.session_state.question[i].append('')

        st.session_state.question[i][j] = col1.text_input(
            f"question_{i}_{j}",
            label_visibility="visible" if j == 0 else "collapsed",
        )

    with col2:
        st.session_state.answer[i] = st.text_area(f"ans_{i}")
    if st.button(f"{i}.Delete QnA"):
        st.session_state['qindex'].pop(i)
        st.session_state['answer'].pop(i)
        st.session_state['question'].pop(i)
        st.session_state["num_questions"] -= 1
        st.session_state.refresh==1
    else:
        pass
if st.session_state.refresh==1:
    st.session_state.refresh = 0
    time.sleep(3)
    st.experimental_rerun()
print(st.session_state)

Yes, using lists instead of dataframes is a valid approach. What do you mean by “not allowing me to delete containers?” Can you describe the problem?

@mathcatsand Ok let me explain. Now Here I have added two QnA and added questions and asnwers.


Now when I click on Delete QnA button for the first QnA this happens.

And after I do a rerun I get this.

Now This is what’s happening with the session states which I have no clue why is it happening.

Can you help me in what can I do to resolve this?

You have your delete button inside of a loop and the action it takes modifies the length of that loop. Additionally, your widgets are going to remember their previous (deleted) value if you don’t have some way to tell Streamlit that it’s a new widget with a new value. If Streamlit constructs a widgets with the same arguments, it will think it’s the same widget even if you intended it to create a new one.

You can force the value of your widgets:

st.session_state.question[i][j] = col1.text_input(
            f"question_{i}_{j}",
            label_visibility="visible" if j == 0 else "collapsed",
            value = st.session_state.question[i][j]
        )
t.session_state.answer[i] = st.text_area(f"ans_{i}", value=st.session_state.answer[i])

And you can make sure you aren’t modifying something within a loop that is simultaneously dictating what that loop is.

def delete_qna(i):
    print(f'popping index {i}')
    st.session_state['qindex'].pop(i)
    st.session_state['answer'].pop(i)
    st.session_state['question'].pop(i)
    st.session_state["num_questions"] -= 1
    print(st.session_state)
# No conditional, just a direct callback function
st.button(f"{i}.Delete QnA", on_click=delete_qna, args=[i])

Thank you @mathcatsand It worked. I have another problem if you can help with that the button names I want to be same but whenever I am adding a key in the button like

st.button("Delete QnA",key = f"{i}.Delete QnA", on_click=delete_qna, args=[i])

I am getting this


What can I do to resolve this

Since buttons aren’t stateful, you can’t dictate a button state. So if you assign a key to a button where the data associated to the key is pre-existing, it will error. You can only assign a key my_key to a button if you never assign a value to st.session_state.my_key manually.

At a minimum, you have to exclude all button keys from your session_state dictionary rewrite at the top of your script (or not use keys on buttons):

for k, v in st.session_state.items():
    st.session_state[k] = v

@mathcatsand ok But if I don’t use keys on buttons how can I make them with same name?

You can’t have both. It’s either unique labels or unique keys.

Then use keys on buttons but do not assign values to them.

@mathcatsand @Goyo I have removed this line from the code and it seems to be working fine now.

for k, v in st.session_state.items():
    st.session_state[k] = v

Will removing it cause any problems later?
And @mathcatsand I am trying to follow the code you provided which is really great.
I also want to add the paging functionality just the way you added in your code but for containers and not the columns. Can you help me direct how can I achieve it in my current code?

If you wrote that code in order to solve some problem, then the problem might reappear and you will have to think of another way to solve it.

@Goyo I wrote that code because when changing pages all the content of the page was getting lost.

But even after removing that code I don’t see that problem now.

That seems to an intermittent bug, so it might come back. In that case you would have to exclude the buttons keys from the loop, as mentioned in a former comment.

That workaround is needed to preserve data associated to a widget key, specifically. When the widget is no longer rendered, Streamlit’s cleanup process deletes the associated key from session state (and it’s currently a bug/feature request for Streamlit to connect to a widget of the same key on a different page as it currently doesn’t).

If the data is not stored in a key associated to a widget, you won’t lose it by going to another page.

(I’m on mobile now, so I’ll provide the columns to containers modification later today.)