Search code examples
ruser-interfaceshinyshiny-reactivity

How to correctly use the selector in the R Shiny removeUI function?


In running the code posted below, the user adds tables via clicks of the "Add table" action button. This part works fine. However, I'm also trying to allow the user to remove one table at a time via the selectizeInput() function, with table deletion executed via Shiny's removeUI() function in the server section. I'm having a hard time coding the correct "selector" within the selectizeInput(). Please see my last observeEvent() in the server section which shows my placeholder for removeUI(). Could someone please help with the correct selector for deleting a selected table?

The user selects the table name to delete, but as currently drafted ALL tables are deleted and not just the selected table, because of my NULL placeholder. Also, remaining tables after deletion, and all tables added after deletion, should left align so that there is a continuous block of rendered tables.

Code:

library(rhandsontable)
library(shiny)

data1 <- data.frame(row.names = c("A","B","C","Sum"),"Col 1"=c(1,1,0,2),check.names=FALSE)

ui <- fluidPage(br(),
        actionButton("addTbl","Add table"), br(), br(),
        tags$div(id = "placeholder",        
                 tags$div(
                   style = "display: inline-block", 
                   rHandsontableOutput("hottable1")
                  )
                ),br(),
        selectizeInput(inputId = "select_deletion",
                       label = "Select deletion",
                       choices = NULL,
                       selected = NULL,
                       multiple = TRUE
                       )
)

server <- function(input, output, session) {
  uiTbl <- reactiveValues(div_01_tbl = data1)
  rv <- reactiveValues()
  
  observeEvent(input$hottable1, {uiTbl$div_01_tbl <- hot_to_r(input$hottable1)})
  
  observe({
    divID <- paste0("div_", sprintf("%02d", input$addTbl+1))
    dtID <- paste0(divID, "_DT")
    uiTbl[[paste0(divID,"_tbl")]] <- data1 # captures initial dataframe values

    insertUI(
      selector = "#placeholder",
      ui = tags$div(
        id = divID,
        style = "display:inline-block;",
        rHandsontableOutput(dtID)
      )
    )
    
    output[[dtID]] <- renderRHandsontable({
      req(uiTbl[[paste0(divID,"_tbl")]])
      rhandsontable(uiTbl[[paste0(divID,"_tbl")]], useTypes = TRUE)
    })

    observeEvent(input[[dtID]], {uiTbl[[paste0(divID,"_tbl")]] <- hot_to_r(input[[dtID]])})
   
    observe({
      tables_list <- reactiveValuesToList(uiTbl)
      tables_list <- tables_list[order(names(tables_list))]
      table_lengths <- lengths(tables_list)
      cumsum_table_lengths <- cumsum(table_lengths)[table_lengths != 0L]
      table_names <- paste("Col", cumsum_table_lengths)
      for(i in seq_along(cumsum_table_lengths)){
        names(uiTbl[[names(cumsum_table_lengths[i])]]) <- table_names[i]
      }
      
      freezeReactiveValue(input, "select_deletion")
      updateSelectizeInput(session, inputId = "select_deletion", choices = table_names, selected = NULL)
      
      observeEvent(input$select_deletion,{ # << attempts to delete selected table via selectizeInput
        removeUI(selector = NULL)
        uiTbl[[paste0(divID,"_tbl")]] <- NULL
      })
    })
  })
}

shinyApp(ui, server)

Solution

  • You need to be extremely careful when nesting observers. In general I don't recommend doing it at all. In this case you should only use it to create an observer for each new table to keep uiTbl updated on user input.

    Please check the following - I'm passing a named list to selectizeInput so we can access the divIDs for the deletion of a table:

    library(shiny)
    library(rhandsontable)
    
    data1 <- data.frame(row.names = c("A","B","C","Sum"),"Col 1"=c(1,1,0,2),check.names=FALSE)
    
    ui <- fluidPage(
      br(),
      actionButton("addTbl","Add table"),
      br(), br(),
      tags$div(id = "placeholder",        
               tags$div(
                 style = "display: inline-block", 
                 rHandsontableOutput("hottable1")
               )
      ),
      br(),
      selectizeInput(inputId = "select_deletion",
                     label = "Select deletion",
                     choices = NULL,
                     selected = NULL,
                     multiple = FALSE),
      actionButton("delete", "Delete", class = "pull-left btn btn-danger")
    )
    
    server <- function(input, output, session) {
      uiTbl <- reactiveValues(div_01_tbl = data1)
      rv <- reactiveValues()                
      
      observeEvent(input$hottable1, {uiTbl$div_01_tbl <- hot_to_r(input$hottable1)})
      
      observe({
        divID <- paste0("div_", sprintf("%02d", input$addTbl+1))
        dtID <- paste0(divID, "_DT")
        uiTbl[[paste0(divID,"_tbl")]] <- data1 # captures initial dataframe values
        
        insertUI(
          selector = "#placeholder",
          ui = tags$div(
            id = divID,
            style = "display:inline-block;",
            rHandsontableOutput(dtID)
          )
        )
        
        output[[dtID]] <- renderRHandsontable({
          req(uiTbl[[paste0(divID,"_tbl")]])
          rhandsontable(uiTbl[[paste0(divID,"_tbl")]], useTypes = TRUE)
        })
        
        observeEvent(input[[dtID]], {uiTbl[[paste0(divID,"_tbl")]] <- hot_to_r(input[[dtID]])})
      })
      
      observe({
        tables_list <- reactiveValuesToList(uiTbl)
        tables_list <- tables_list[order(names(tables_list))]
        table_lengths <- lengths(tables_list)
        cumsum_table_lengths <- cumsum(table_lengths)[table_lengths != 0L]
        table_names <- paste("Col", cumsum_table_lengths)
        for(i in seq_along(cumsum_table_lengths)){
          names(uiTbl[[names(cumsum_table_lengths[i])]]) <- table_names[i]
        }
        # print(tables_list) ### PRINT ###
        # browser() ### use browser() to analyse your observer
        divIDs <- gsub("_tbl", "", names(tables_list[table_lengths != 0L]))
        names(divIDs) <- table_names
        freezeReactiveValue(input, "select_deletion")
        updateSelectizeInput(session, inputId = "select_deletion", choices = divIDs, selected = NULL)
      })
      
      observeEvent(input$delete, {
        tables_list <- reactiveValuesToList(uiTbl)
        table_lengths <- lengths(tables_list)
        if(length(table_lengths[table_lengths > 0L]) > 1L){
          req(input$select_deletion)
          removeUI(selector = paste0("#", input$select_deletion))
          rv[[input$select_deletion]] <- NULL
          uiTbl[[paste0(input$select_deletion,"_tbl")]] <- NULL 
        }
      })
    }
    
    shinyApp(ui, server)