Closing current expander and opening next by button-press

Summary

I want to create consecutive expanders, where only one is open at the time. A button click on ‘next’ inside the expander should open the next expander. I want to avoid clicking twice for collapsing the expander. Is there a way to define ‘open/collapsed’ before defining the expander itself?

Dummy code

import streamlit as st 

samples = 3

#intialise session states
for key in [f'next{i}' for i in range(0, samples)]:
   if key not in st.session_state:
      st.session_state[key] = False

for key in [f'is_expanded{i}' for i in range(0, samples)]:
   if key not in st.session_state:
      st.session_state[key] = False

for i in range(samples): 

    #define layout for 
    expander_container = st.container()
    with expander_container:
        in_expander = st.container()

    #define next button
    if st.session_state[f'next{i}']:
        st.session_state[f'is_expanded{i}'] = False
        st.session_state[f'is_expanded{i+1}'] = True

    #content of expander 
    in_expander.write('Inside the container')
    st.session_state[f'next{i}'] = in_expander.button('Next', key = f'next_button{i}')

    #define expander
    expander_container.expander(f'SĂŠtning {i+1}', expanded=st.session_state[f'is_expanded{i}'])



Actual behavior:
This does not place the content of the expander inside the expander.

Hi @KiriKoppengaard, :wave:

I can’t think of any hack, but I’ve enquired internally to check if there is a programming way to achieve this.

I’ll come back to you with a definitive answer shortly :slight_smile:

Best wishes,
Charly

1 Like

I made an example using script injection to accomplish this. There is occassionally a little lag in response since the javascript is loaded dynamically on demand. I’m sure this could be improved upon, but at least this illustrates a viable mechanism.

App is here: https://mathcatsand-examples.streamlit.app/formatted_container

Code is here: Streamlit-Mechanics-Examples/formatted_container.py at main · MathCatsAnd/Streamlit-Mechanics-Examples · GitHub

2 Likes

Thanks for your feedback, Debbie! :pray:

It’s worth adding that Expanders have an attribute expanded which could be assigned to a variable.

Here’s a great suggestion from @brianmhess

You could have a button in the expander content that says Next that just sets this expander’s expanded to False and sets the next to True. You’d want to do that in a callback, which means those expanded variables should probably be in session state. When you do that, the server will rerun the whole app on each Next, though. Which is worth keeping in mind.

Let me know how it goes and if you have any more questions :slight_smile:

Happy Streamlit-ing! :balloon:

Charly

Very true! This is indeed how I ended up accomplishing it. Thank you :slight_smile:

This is just what I wanted! Thank you very much :slight_smile:

In my enthusiasm, I totally forgot about the expanded parameter. I would definitely use that instead. (It’s tabs that don’t have a Python attribute to mess with and requires the JavaScript.) :woman_facepalming:

I’ll add another note here: I implemented it using the expanded keyword, but it doesn’t mix-and-match well with users manually expanding and contracting the elements. I tried passing (alternatingly) 0 and 1 instead of False and True to try and force Streamlit to recognize it as a new command and re-render but it seems the widget is caching the boolean value rather than the actual value passed to expanded, which means Streamlit doesn’t know to override its state it “got from the user.” This means I’d have to rely on something like st.empty to forcefully destroy and recreate the expanders


(e.g. If expanded is False and I manually expand it, then if I try to programatically set it to False, nothing happens since Streamlit thinks it’s the same and is happily maintaining the state for the user. I had hoped by passing it 0, I could trick Streamlit into processing it as “new information” but it doesn’t seem to do this so I can’t get around it like that.)

Hi @KiriKoppelgaard and @mathcatsand,

This code seems to do the work:

import streamlit as st

if "etgl" not in st.session_state:
    st.session_state.etgl = [True, False, False]

with st.expander('Expander1', st.session_state.etgl[0]):
    st.write("In expander 1")
    if st.button("Next", key="b1"):
        st.session_state.etgl = [False, True, False]
        st.experimental_rerun()

with st.expander('Expander2', st.session_state.etgl[1]):
    st.write("In expander 2")
    if st.button("Next", key="b2"):
        st.session_state.etgl = [False, False, True]
        st.experimental_rerun()

with st.expander('Expander3', st.session_state.etgl[2]):
    st.write("In expander 3")
    if st.button("Next", key="b3"):
        st.session_state.etgl = [True, False, False]
        st.experimental_rerun()

Cheers

Thanks @Shawn_Pereira . For me, the code is susceptible the issue I mentioned: it won’t mix and match well if a user manually expands/collapses things in addition to using the next buttons.

Suppose session state thinks an expander is closed, but a user clicks on it to expand it. This is done in browser and does not report back to session state. If you attempt to close it with session state, nothing will happen since you won’t actually be changing the parameters of the element.

With your code:
streamlit-home-2023-01-18-20-01-98

I see three ways around this:

  1. with javascript
  2. with manual destruction and recreation of the expander elements so they start as if new with each forced state
  3. using experimental rerun to “cycle” the expander through first the opposite of what you want, then what you want so it registers a definite “change of state”

If you just want to ensure you are opening the next one, you can add that cycling behavior (note this will not force the state of all the expanders if the user has pulled them out of sync with what session state thinks, though it’d be an easy addition if you really did want to force the state of all of them).

import streamlit as st

if 'cycle' not in st.session_state:
    st.session_state.cycle = -1

if "etgl" not in st.session_state:
    st.session_state.etgl = [True, False, False]

with st.expander('Expander1', st.session_state.etgl[0]):
    st.write("In expander 1")
    if st.button("Next", key="b1"):
        st.session_state.etgl = [False, False, False]
        st.session_state.cycle = 1
        st.experimental_rerun()

with st.expander('Expander2', st.session_state.etgl[1]):
    st.write("In expander 2")
    if st.button("Next", key="b2"):
        st.session_state.etgl = [False, False, False]
        st.session_state.cycle = 2
        st.experimental_rerun()

with st.expander('Expander3', st.session_state.etgl[2]):
    st.write("In expander 3")
    if st.button("Next", key="b3"):
        st.session_state.etgl = [False, False, False]
        st.session_state.cycle = 0
        st.experimental_rerun()

if st.session_state.cycle >= 0:
    st.session_state.etgl[st.session_state.cycle] = True
    st.session_state.cycle = -1
    st.experimental_rerun()

Hi @mathcatsand

  1. Agree with you on the user not manually opening & closing the expanders, but I provided a response based on the discussion subject ‘Closing current expander and opening next by button-press’.
  2. Yes, the solution does not make for perfect code, but works within the limitations of the expander widget. Hopefully, Streamlit will give us more parameters to manipulate when using the expander widget in future.
  3. Not sure the reason for st.session_state.cycle, as the variable is being assigned a value but not being used. The code I provided cycles through the expanders as intended. Maybe, I am missing something
?

Cheers

oh btw, I tried to upload a recorded screencast, but it wouldnt accept the file (webm format). How did you upload yours?

1 Like

Sorry for any confusion, I was just trying to illustrate what I mentioned in the post where I said I had implemented a solution using the expanded keyword but found it to have this limitation.

The purpose of cycling is to make it robust to the mentioned limitation (though I only made a guarantee that the next one would open since that is the only one that is cycled).

Suppose a user is on expander 1 and clicks next (session state has the 1 is closed and 2 is open now). The user wants to go back so they manually collapse expander 2 and open expander 1. In that situation, that next button in expander 1 won’t work if they try to click next without the cycling in place. The button click set everything to False but the cycling flag lets you get to the end, set the correct one to true and rerun, guaranteeing that the next container will be opened because the backend saw a change of state.

How much a limitation is in fact a limitation is of course a personal preference according to use case. I just meant to explain since I hadn’t posted my other solution to illustrate. :slight_smile:

PS I use cloud convert to make gifs from screen records.

2 Likes

Changed the True / False assignment so button 2 cycles back to expander 1.

@mathcatsand - getting around the user manually collapsing the controls by injecting a CSS Style to remove the expander controls.

import streamlit as st

st.markdown("""<style>
[class^=streamlit-expanderHeader] {display: none;}
</style>""", unsafe_allow_html=True)

if "etgl" not in st.session_state:
    st.session_state.etgl = [True, False]

with st.expander('Expander1', st.session_state.etgl[0]):
    st.write("In expander 1")
    if st.button("Next", key="b1"):
        st.session_state.etgl = [False, True]
        st.experimental_rerun()

with st.expander('Expander2', st.session_state.etgl[1]):
    st.write("In expander 2")
    if st.button("Next", key="b2"):
        st.session_state.etgl = [True, False]
        st.experimental_rerun()

streamlit-20231108-streamlit-expander-2023-11-08-19-11-30-3

1 Like

Can’t edit my last post. After updating to Streamlit Version 1.28.2 need to change the markdown style class to:

st.markdown("""<style>
            [class^=st-emotion-cache-p5msec] {display: none;}
            </style>""", unsafe_allow_html=True)