pythonpy-shiny

How to dynamically insert new tabs with the press of an input task button?


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.


Solution

  • 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:

    It looks like shown below. The remove functionality can be implemented similarly.

    enter image description here

    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)