I am developing a Shiny app in Python and I am currently struggling to find a way to dynamically generate new tabs with the press of either an input task button or with a tab called "New". I've researched into using ui.insert_ui
and achieved some level of the intended effect with dynamically adding panels to my ui.navset_tab
but when clicking on the new tab, the tab freezes and I can't switch to the new tab.
Is there a more optimal way to create dynamic tabs and also remove them?
My code is outlined below:
from shiny import App, Inputs, Outputs, Session, reactive, render, ui
from shinywidgets import output_widget, render_widget
app_ui = ui.page_fluid(
ui.navset_tab(
ui.nav_panel("Home",
ui.card(
ui.card_header("Overview"),
ui.p("This is the landing page"),
ui.input_task_button(id = "create_tab",
label = "Create New Tab",
width = "400px",
type = "success"),
)
),
id = "shiny_tabs"
)
)
def server(input, output, session):
# Set reactive values
tabs_created = reactive.value(1)
# Generate tabs
@reactive.Effect
@reactive.event(input.create_tab)
def _():
tab_title = f"View {tabs_created.get()}"
ui.insert_ui(
ui.navset_tab(
ui.nav_panel(tab_title,
ui.modal("This will include the accordion content")
)
),
selector = "#shiny_tabs",
where = "beforeEnd"
)
tabs_created.set(tabs_created.get() + 1)
app = App(app_ui, server)
I also posted this question in posit-dev/py-shiny#1510.
I wouldn't use an ui.insert_ui
approach here because it seems to make things too complicated. Instead you can use Shiny Modules. The below example basically consists out of these three parts:
A reactive.value
navs
which has an entry for each appended ui.nav_panel
, e.g. nav.get() == [1, 2]
if the button was clicked twice. It is similar to your tabs_created
, but I use a list here.
A module.ui
which is used for generating the new ui.nav_panel
, it depends on the panel number:
@module.ui
def nav_panel_ui(panelNumber):
tab_title = f"New tab {panelNumber}"
return ui.nav_panel(
tab_title,
ui.p(f"Content of tab {panelNumber}"),
value="panel" + str(panelNumber)
)
A rendered ui.navset_tab
(using on the server side render.ui
and on the ui
side ui.output_ui
. The ui.navset_tab
has this structure:
ui.navset_tab(
ui.nav_panel("Home", ...), # your 'Home' tab, visible all the time
# all additional nav_panels defined by navs.get()
# the first parameter of nav_panel_ui is the id
# '*' in order to pass the list values (nav_panels) to navset_tab()
*[nav_panel_ui(str(x), panelNumber=str(x)) for x in navs.get()],
.... # additional arguments
)
It looks like shown below. The remove functionality can be implemented similarly.
from shiny import App, reactive, module, render, ui
@module.ui
def nav_panel_ui(panelNumber):
tab_title = f"New tab {panelNumber}"
return ui.nav_panel(
tab_title,
ui.p(f"Content of tab {panelNumber}"),
value="panel" + str(panelNumber)
)
app_ui = ui.page_fluid(
ui.output_ui("myUI")
)
def server(input, output, session):
# Initialize reactive value which stores a list containing nav numbers
navs = reactive.value([])
# Set the list of nav values on button click
# E.g. clicking the button twice sets navs = [1, 2]
# Below this is used for rendering a list of tabs of the same length as navs
@reactive.Effect
@reactive.event(input.create_tab)
def _():
if len(navs.get()) == 0:
navs.set(navs.get() + [1])
else:
navs.set(navs.get() + [max(navs.get()) + 1])
# render the navset_tab, home tab + [nav_panel1, nav_panel2, ...]
@render.ui
def myUI():
return ui.navset_tab(
# the home tab
ui.nav_panel("Home",
ui.card(
ui.card_header("Overview"),
ui.p("This is the landing page"),
ui.input_task_button(id="create_tab",
label="Create New Tab",
width="400px",
type="success"),
),
value="panel0"
),
# all additional nav_panels defined by navs.get()
# the first parameter of nav_panel_ui is the id
# '*' in order to pass the list values (nav_panels) to navset_tab()
*[nav_panel_ui(str(x), panelNumber=str(x)) for x in navs.get()],
id="shiny_tabs"
)
app = App(app_ui, server)