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)
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.
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)