Shiny dynamic tabs

This is a simple shiny app that demonstrates dynamic shiny tabs. Each tab contains a shiny module which returns a series of data parameters and tabs can be added and removed. In one use-case this construct was used in three higher level tabs, returning a complex, nested structure.

Code is available at: https://github.com/harveyl888/shiny_dynamic_tabs

The concept is fairly simple - upon start up a tabsetPanel is built with a button prepended before the first tab. When clicked, this button adds a new tab containing a shiny module (the tab also contains a close button). The shiny module returns a series of data and these data, from all tabs, are stored in a reaciveValues list (rv$return_data). As tabs are added, deleted or their content changed, the contents of rv$return_data updates.

tabsetPanel code

The code below builds the tabsetPanel. Upon execution, rv$dataset_count is incremented from 0 to 1, dataset_name is set to dataset_1 and added to the rv$dataset_names list, and rv$trigger_add_data_button is changed from FALSE to TRUE. In addition, the first tab is created, containing a shiny module which returns data to rv$return_data. The line:

1rv$return_data[[dataset_name]] <<- mod_data(id = dataset_name, datasetname = dataset_name)

ensures that the shiny module returns data to a named list using the tab name itself as the component name.

 1  ## tabs
 2  output$ui_tabs <- renderUI({
 3    isolate({
 4      rv$dataset_count <- rv$dataset_count + 1
 5      dataset_name <- paste0("dataset_", rv$dataset_count)
 6      rv$dataset_names[[length(rv$dataset_names) + 1]] <- dataset_name
 7      rv$return_data[[dataset_name]] <<- mod_data(id = dataset_name, datasetname = dataset_name)
 8      rv$trigger_add_data_button <- TRUE
 9    })
10    tabsetPanel(id = "tab_data",
11                tabPanel(title = tab_title(dataset_name), value = dataset_name, mod_data_UI(dataset_name)))
12  })

Add Button to tabsetPanel

This observeEvent code is run when rv$trigger_add_data_button is set to TRUE. It runs a short javascript function (addbutton) to add a button to the tabsetPanel. The button is given the id add_data and an observeEvent is added which runs the function add_dataset() when the button is clicked.

Here is the R code:

 1  ## add a button to the tabPanel
 2  observeEvent(rv$trigger_add_data_button, {
 3    if (rv$trigger_add_data_button) {
 4      rv$trigger_add_data_button <- FALSE
 5      shinyjs::delay(100, session$sendCustomMessage(type = "addbutton", list(id = "tab_data", trigger = "add_data")))
 6      tryCatch(o_data$destroy(),
 7               error = function(e) NULL)
 8      o_data <<- observeEvent(input$add_data, {
 9        add_dataset()
10      }, ignoreInit = TRUE)
11    }
12  }, once = FALSE)

and here is the javascript function.

1Shiny.addCustomMessageHandler('addbutton', function(message) {
2  var button = "<li class='list_button'><button type='button' class='btn btn-success' onclick='trigger_shiny(\"" + message.trigger + "\")'><i class='fa fa-plus'></i></button></li>";
3  $("#" + message.id).first().prepend(button);
4});
5
6function trigger_shiny(trigger, value = 1) {
7  Shiny.setInputValue(trigger, value, {priority: "event"});
8}

Add a new tab with data

The add_dataset function is run whenever the button in the tabsetPanel is clicked. This function adds a new tab containing a shiny module. It is very similar to the tabsetPanel code

1  ## function to add a new dataset
2  add_dataset <- function() {
3    rv$dataset_count <- rv$dataset_count + 1
4    dataset_name <- paste0("dataset_", rv$dataset_count)
5    rv$dataset_names[[length(rv$dataset_names) + 1]] <- dataset_name
6    rv$return_data[[dataset_name]] <<- mod_data(id = dataset_name, datasetname = dataset_name)
7    appendTab(inputId = "tab_data", tabPanel(title = tab_title(dataset_name), value = dataset_name, mod_data_UI(dataset_name)))
8  }

Creating a new tab

Finally, the function below, builds a title for a tab including a close button.

1  ## tab title with close button
2  tab_title <- function(name, type = "data") {
3    tags$span(
4      name,
5      tags$span(icon("times"),
6                style = "margin-left: 5px;",
7                onclick = paste0("Shiny.setInputValue(\"", paste0("remove_", type, "_tab"), "\", \"", name, "\", {priority: \"event\"})"))
8    )
9  }

When the close icon is clicked the following observeEvent is triggered, removing the tab and updating rv$dataset_names, a list of the names of the current tabs.

1  ## remove a dataset
2  observeEvent(input$remove_data_tab, {
3    removeTab(inputId = "tab_data", target = input$remove_data_tab)
4    isolate({rv$dataset_names <- rv$dataset_names[!rv$dataset_names == input$remove_data_tab]})
5  })

Screen Captures

Upon start up, the tabPanel is built with a button and a single tab. Data returned is printed to the right of the tabPanel.

Adding additional tabs and changing their content updates the returned data.

Removing tab #2 updates the returned data.