Search code examples
pythonpy-shiny

How to get an "Edit" button to be visible only when the user has clicked on a row in the table?


I'd like to make it so that when the user loads that app and looks at the "Session" tab of the app, that there is a "New" button visible that will eventually take them to a new record form. If the user selects a row in the existing table though, I want an "Edit" button to be visible so they can edit that single selected row.

For the most part I have this functionality working, however I'm having some trouble with the Edit button flickering on and off when a row is selected before finally goes invisible. I believe the problem has to do with my selected_active_practive_session() reactive calculation. When I step through the execution, it returns the correct row id the first time, but then the Shiny App forces it to run again, and again, ultimately changing the output value to None. This causes the "Edit" button to flicker on and off before staying off.

#core
import pandas as pd

# Web/Visual frameworks
from shiny import App, ui, render, reactive, types

df_sessions = pd.DataFrame(
    {'Date':['9-1-2024','9-2-2024','9-3-2024','9-4-2024'],
     'Song_id':[1,1,2,2],
     'Notes':['Focused on tempo (65bpm)','focused on tempo (70bpm)','Started learning first page','Worked on first page']})

app_ui = ui.page_fluid(
    ui.page_navbar(
        ui.nav_panel("New Practice Session", "New Practice Session - Input Form"),
        ui.nav_panel("New Song", "New Song - Input Form"),
        ui.nav_panel("New Artist", "New Artist - Input Form"),
        title="Guitar Study Tracker",
        id="page",
    ),
    ui.output_ui('page_manager'),
)



def server(input, output, session):

    def nav_song(): # Placeholder
        return "Song"

    def nav_artist(): # Placeholder
        return "Artist"

    @render.data_frame
    def session_summary():
        return render.DataGrid(
            df_sessions,
            width="100%",
            height="100%",
            selection_mode="row"
        )


    @reactive.calc()
    def selected_active_practice_session():
        """Returns the row id of a selected row, otherwise returns None"""
        try:
            row_id = session_summary.cell_selection()['rows']
            row_id = row_id[0] if len(row_id)>0 else None # sets row_id to the selected row or None otherwise
        except types.SilentException as e: # On first run this control will not exist by default - catch it's SilentException
            row_id=None
        return row_id

    @reactive.calc             
    def ui_practice_session_button():
        if selected_active_practice_session() is not None:
            return ui.input_action_button("btn_update_session", "Update", width='100%')
        else:
            return None

    @reactive.calc
    def nav_practice_session():
        ret_ui = ui.row(
            ui.row(ui.column(2, ui_practice_session_button()),
                ui.column(8),
                ui.column(2,ui.input_action_button("btn_new_session", "New", width="100%")),
            ),
            
            ui.output_data_frame("session_summary")
        )
        return ret_ui

    @render.ui
    def page_manager():
        if input.page()=="New Practice Session":
            return nav_practice_session() 
        if input.page()=="New Song":
            return nav_song()
        if input.page()=="New Artist":
            return nav_artist()
        return None
        
app = App(app_ui, server)    

Solution

  • The issue with the flickering and finally invisible button in the example is that the relevant ui gets re-rendered multiple times due to the interdependent @reactive.calc and that within the re-rendered ui the cell_selection() of the summary table is empty (because the selection appeared in the "former" table), causing no button to be visible.

    Instead, you could define the ui without the button outside the server and define a placeholder ui.div where the button should be visible later. And then one can use an ui.insert_ui within the server if the selected data is non-empty which inserts the button before the placeholder div. Below also is a reactive value which keeps track of whether the button is visible, such that no second button can be inserted on this way.

    Notice that there is also ui.remove_ui if you need to remove the button and that the approach below similarly also works if you need to render more ui within the server.

    enter image description here

    import pandas as pd
    from shiny import App, ui, render, reactive, req
    
    df_sessions = pd.DataFrame(
        {'Date': ['9-1-2024', '9-2-2024', '9-3-2024', '9-4-2024'],
         'Song_id': [1, 1, 2, 2],
         'Notes': ['Focused on tempo (65bpm)', 'focused on tempo (70bpm)', 'Started learning first page', 'Worked on first page']})
    
    app_ui = ui.page_fluid(
        ui.page_navbar(
            ui.nav_panel("New Practice Session", "New Practice Session - Input Form",
                         ui.row(
                             ui.row(ui.column(2, ui.div(id="placeholder")),
                                    ui.column(8),
                                    ui.column(2, ui.input_action_button(
                                        "btn_new_session", "New", width="100%")),
                                    ),
                             ui.output_data_frame("session_summary")
                         )),
            ui.nav_panel("New Song", "New Song - Input Form",
                         "Song"),
            ui.nav_panel("New Artist", "New Artist - Input Form",
                         "Artist"),
            title="Guitar Study Tracker",
            id="page",
        ),
        ui.output_ui('page_manager'),
    )
    
    def server(input, output, session):
        
        updateButtonVisible = reactive.value(False)
    
        def nav_song():  # Placeholder
            return "Song"
    
        def nav_artist():  # Placeholder
            return "Artist"
    
        @render.data_frame
        def session_summary():
            return render.DataGrid(
                df_sessions,
                width="100%",
                height="100%",
                selection_mode="row"
            )
    
        @reactive.effect
        def insert_update_button():
            data_selected = session_summary.data_view(selected=True)
            req(not (data_selected.empty or updateButtonVisible.get()))
            ui.insert_ui(
                ui.input_action_button("btn_update_session",
                                       "Update", width='100%'),
                selector="#placeholder",
                where="beforeBegin"
            )
            updateButtonVisible.set(True)
    
        @render.ui
        def page_manager():
            if input.page() == "New Song":
                return nav_song()
            if input.page() == "New Artist":
                return nav_artist()
            return None
    
    
    app = App(app_ui, server)