Dropdown lists are disabled when DT table is re-rendered [Shiny]

50 views Asked by At

I have the dt table with nested dropdown lists. Lists work fine until the table is being re-rendered (for example, i need to add extra record to DT table).

Lists work but their values are not updated (print(input$<list name> generates values prior to re-rendering of the table).

i have used the example in the answer section of here.

I expected to have dropdown lists with values updated in input$, like the list "sel1" is updating vlue in input$sel1. Upon the values updated output$sel should be updated as well.

The option i have found is to re-render the table with dropdown lists having different ids each time the table is being rendered. but it looks not efficient and clumsy.

I have extended the script from the example with action button to re-render the table. The app is as follows:

library(shiny)
library(DT)

# this selectInput will not be included in the app;
# we just use it to extract the required HTML dependencies
select_input <- selectInput("x", label = NULL, choices = c("A", "B"))
deps <- htmltools::findDependencies(select_input)
# now you just have to include tagList(deps) somewhere in the Shiny UI

ui <- fluidPage(
  actionButton('my_btn', 'Re-render DT table'),
  title = 'Selectinput column in a table',
  DTOutput('foo'),
  verbatimTextOutput('sel'),
  tagList(deps)
)

server <- function(input, output, session) {
  
  data <- head(iris, 5)
  
  for (i in 1:nrow(data)) {
    data$species_selector[i] <- 
      as.character(
        selectInput(
          paste0("sel", i), "", 
          choices = unique(iris$Species), 
          width = "100px"
        )
      )
  }
  
  output$foo = renderDT({
    datatable(
      data, escape = FALSE, selection = 'none', 
      options = list(
        dom = 't', 
        paging = FALSE, 
        ordering = FALSE,
        initComplete = JS(c(
          "function(settings, json) {",
          "  var $table = this.api().table().node().to$();",
          "  $table.find('[id^=sel]').selectize();", # apply selectize() to all elements whose id starts with 'sel' (here sel1, sel2, ...)
          "}"
        )),
        preDrawCallback = 
          JS("function() {Shiny.unbindAll(this.api().table().node());}"),
        drawCallback 
        = JS("function() {Shiny.bindAll(this.api().table().node());}")
      )  
    )
  })
  
  output$sel = renderPrint({
    str(sapply(1:nrow(data), function(i) input[[paste0("sel", i)]]))
  })
  
  observeEvent(input$my_btn,{
    
    output$foo = renderDT({
      datatable(
        data, escape = FALSE, selection = 'none', 
        options = list(
          dom = 't', 
          paging = FALSE, 
          ordering = FALSE,
          initComplete = JS(c(
            "function(settings, json) {",
            "  var $table = this.api().table().node().to$();",
            "  $table.find('[id^=sel]').selectize();", # apply selectize() to all elements whose id starts with 'sel' (here sel1, sel2, ...)
            "}"
          )),
          preDrawCallback = 
            JS("function() {Shiny.unbindAll(this.api().table().node());}"),
          drawCallback 
          = JS("function() {Shiny.bindAll(this.api().table().node());}")
        )  
      )
    })
    print(input)
    print(input$sel1)
    
  })
  
}

1

There are 1 answers

1
Stéphane Laurent On BEST ANSWER

I never clearly understood why, but you have to unbind when re-rendering the table. You can do that with the callback:

      callback = JS(c(
        "$('#my_btn').on('click', function() {",
        "  Shiny.unbindAll(table.table().node());",
        "});"
      )),

As a side note, don't put an output slot inside an observer (unless there's really no other way). Here you can simply insert input$my_btn inside the renderDT.

As another side note, you should avoid to re-render the table. If you have to add new records in your real app, use a proxy and the DT function replaceData instead.

Full code:

library(shiny)
library(DT)

# this selectInput will not be included in the app;
# we just use it to extract the required HTML dependencies
select_input <- selectInput("x", label = NULL, choices = c("A", "B"))
deps <- htmltools::findDependencies(select_input)
# now you just have to include tagList(deps) somewhere in the Shiny UI

ui <- fluidPage(
  actionButton('my_btn', 'Re-render DT table'),
  title = 'Selectinput column in a table',
  DTOutput('foo'),
  verbatimTextOutput('sel'),
  tagList(deps)
)

server <- function(input, output, session) {
  
  data <- head(iris, 5)
  
  for (i in 1:nrow(data)) {
    data$species_selector[i] <- 
      as.character(
        selectInput(
          paste0("sel", i), "", 
          choices = unique(iris$Species), 
          width = "100px"
        )
      )
  }
  
  output$foo = renderDT({
    input$my_btn # table will be refreshed on clicking this button
    datatable(
      data, escape = FALSE, selection = 'none', 
      callback = JS(c(
        "$('#my_btn').on('click', function() {",
        "  Shiny.unbindAll(table.table().node());",
        "});"
      )),
      options = list(
        dom = 't', 
        paging = FALSE, 
        ordering = FALSE,
        initComplete = JS(c(
          "function(settings, json) {",
          "  var $table = this.api().table().node().to$();",
          "  $table.find('[id^=sel]').selectize();", # apply selectize() to all elements whose id starts with 'sel' (here sel1, sel2, ...)
          "}"
        )),
        preDrawCallback = 
          JS("function() {Shiny.unbindAll(this.api().table().node());}"),
        drawCallback 
        = JS("function() {Shiny.bindAll(this.api().table().node());}")
      )  
    )
  })
  
  output$sel = renderPrint({
    str(sapply(1:nrow(data), function(i) input[[paste0("sel", i)]]))
  })
  
  observe({
    print(input$sel1)
  })
  
}

shinyApp(ui, server)