"Show more items" functionality

I’d like to add a feature to my streamlit app where a list ends with a “Show more” button, and pressing the button queries for more data, and puts the “Show more” button at the end of the extended list. Exactly like how google scholar implements it: https://scholar.google.com/citations?user=JicYPdAAAAAJ&hl=en&oi=ao

Using the https://gist.github.com/tvst/036da038ab3e999a64497f42de966a92 SessionState.py class, I came up with something nice that’s quite similar:

import random
import time
import streamlit as st
import SessionState


@st.cache
def long_calculation(i):
    time.sleep(1)
    return 1000 * i + random.randrange(100)


def main():
    session_state = SessionState.get(length=5)
    if st.button("Show more"):
        session_state.length += 5
    for i in range(1, session_state.length + 1):
        st.write("item %d: %d" % (i, long_calculation(i)))


main()

The only difference between this and my imagined ideal solution is that here the “Show more” button is at the beginning, not at the end where it should be. Unfortunately this is extremely bad UX for long lists, and I’m stuck here. Is there a way to fix this issue, either along these lines, or starting from scratch?

Hey @danielvarga -

If I’m correctly understanding what you’re after, you can accomplish this with some minor abuse of RerunException (which is currently undocumented and subject to change):


from streamlit.ScriptRequestQueue import RerunData
from streamlit.ScriptRunner import RerunException

def main():
    session_state = SessionState.get(length=5)
    for i in range(1, session_state.length + 1):
        st.write("item %d: %d" % (i, long_calculation(i)))
    if st.button("Show more"):
        session_state.length += 5
        # Force an immediate rerun of the report. This is a bit
        # ugly, but it ensures that the report will be rerun
        # *after* session state gets updated.
        raise RerunException(RerunData(widget_state=None))

Hopefully, when SessionState becomes a proper Streamlit citizen, simply modifying it will be enough to trigger a rerun, and these gymnastics won’t be necessary!

1 Like

I like this expand content idea, what would be nice if a component like st.markdown which has content which is easily truncated had an option like st.markdown(txt, show_lines=5), so it only displays the first few lines and puts in a expand icon to show all the text.

Useful for things like putting in a block of explainer text for different objects. And other uses, like showing the head/tail of a log with an expand option to show all the log.

1 Like

That’s awesome, @tim, thank you! What’s streamlit’s current roadmap for solidifying this part of the API?

It’s currently being tracked in this Github issue . I don’t know there’s a fixed timeline decided for this feature yet, but it’s being discussed and I think we’ll be working on it in earnest soon!

1 Like

Dear @tim,

Following https://github.com/streamlit/streamlit/issues/1294#issuecomment-623440171 I upgraded to the nightly from a pretty old streamlit (0.52.2), with https://gist.github.com/tvst/0899a5cdc9f0467f7622750896e6bd7f#file-st_state_patch-py and it seems like your nice hack does not work now. Is there a way to fix it?

Hi @danielvarga, are you sure you’re using the current/latest version of that gist?

It’s been updated to handle versions before 0.54.0 and after.

If you are, could you share your error message and traceback?

I am using the current version. There’s no error or traceback, but the script behaves in a weird way. For each click it does the long calculations, appends the results at the end as it is supposed to, but then removes everything, restarts the calculations, and only does it for the first five items. So it re-runs twice for each button-click, and the second rerun destroys the results of the first rerun. I am not sure if I’m using st.SessionState() right:

import streamlit as st
import st_state_patch
from streamlit.ScriptRequestQueue import RerunData
from streamlit.ScriptRunner import RerunException
import time

def long_calculation(x):
    time.sleep(1)
    return 10 * x

def main():
    session_state = st.SessionState()
    if not session_state:
        session_state.length = 5
    for i in range(1, session_state.length + 1):
        st.write("item %d: %d" % (i, long_calculation(i)))
    if st.button("Show more"):
        session_state.length += 5
        # Force an immediate rerun of the report. This is a bit
        # ugly, but it ensures that the report will be rerun
        # *after* session state gets updated.
        raise RerunException(RerunData(widget_state=None))

main()

See a working example below @danielvarga .

import streamlit as st
from state import get
from streamlit.ScriptRequestQueue import RerunData
from streamlit.ScriptRunner import RerunException
import time

def long_calculation(x):
    time.sleep(0.1)
    return 10 * x

session_state = get(length=5)

for i in range(1, session_state.length + 1):
    st.write("item %d: %d" % (i, long_calculation(i)))

if st.button("Show more"):
    session_state.length += 5
    raise RerunException(RerunData(widget_state=None))

And state.py below

"""Hack to add per-session state to Streamlit.

Usage
-----

>>> import SessionState
>>>
>>> session_state = SessionState.get(user_name='', favorite_color='black')
>>> session_state.user_name
''
>>> session_state.user_name = 'Mary'
>>> session_state.favorite_color
'black'

Since you set user_name above, next time your script runs this will be the
result:
>>> session_state = get(user_name='', favorite_color='black')
>>> session_state.user_name
'Mary'

"""
import streamlit.ReportThread as ReportThread
from streamlit.server.Server import Server


class SessionState(object):
    def __init__(self, **kwargs):
        """A new SessionState object.

        Parameters
        ----------
        **kwargs : any
            Default values for the session state.

        Example
        -------
        >>> session_state = SessionState(user_name='', favorite_color='black')
        >>> session_state.user_name = 'Mary'
        ''
        >>> session_state.favorite_color
        'black'

        """
        for key, val in kwargs.items():
            setattr(self, key, val)


def get(**kwargs):
    """Gets a SessionState object for the current session.

    Creates a new object if necessary.

    Parameters
    ----------
    **kwargs : any
        Default values you want to add to the session state, if we're creating a
        new one.

    Example
    -------
    >>> session_state = get(user_name='', favorite_color='black')
    >>> session_state.user_name
    ''
    >>> session_state.user_name = 'Mary'
    >>> session_state.favorite_color
    'black'

    Since you set user_name above, next time your script runs this will be the
    result:
    >>> session_state = get(user_name='', favorite_color='black')
    >>> session_state.user_name
    'Mary'

    """
    # Hack to get the session object from Streamlit.

    ctx = ReportThread.get_report_ctx()

    this_session = None

    current_server = Server.get_current()
    if hasattr(current_server, '_session_infos'):
        # Streamlit < 0.56
        session_infos = Server.get_current()._session_infos.values()
    else:
        session_infos = Server.get_current()._session_info_by_id.values()

    for session_info in session_infos:
        s = session_info.session
        if (
            # Streamlit < 0.54.0
            (hasattr(s, '_main_dg') and s._main_dg == ctx.main_dg)
            or
            # Streamlit >= 0.54.0
            (not hasattr(s, '_main_dg') and s.enqueue == ctx.enqueue)
        ):
            this_session = s

    if this_session is None:
        raise RuntimeError(
            "Oh noes. Couldn't get your Streamlit Session object"
            'Are you doing something fancy with threads?')

    # Got the session object! Now let's attach some state into it.

    if not hasattr(this_session, '_custom_session_state'):
        this_session._custom_session_state = SessionState(**kwargs)

    return this_session._custom_session_state
2 Likes

Works perfectly, thank you @Jonathan_Rhone!