One observer to handle any number of buttons in Shiny

actionbutton

#1

A little over two years ago I asked a question on StackOverflow about how to attach multiple buttons to one observer. There were no good answers then, but after reading a pretty good JavaScript & jQuery book, I finally figured it out.

Why do I need this? Here's an example:

This is a list of projects with buttons to view or join any project. When setting up the observer(s) for these buttons, I have no idea how many projects there will be, but there could be hundreds. With actionButton() you need one observer for each button, which could lead to madness.

I finally solved this problem with a JavaScript/jQuery function that sets an on click event on all the buttons in a document. When a button is clicked, the function uses Shiny.onInputChange() to send the id (<button id="xxx"...) of the button that was clicked to the observer.

You can see the details with code examples at One observer for all buttons in Shiny using JavaScript/jQuery.


#2

Here's an option that works. It's a bit of a brute force implementation, but has worked well for me in my applications. The magic happens in the section of code marked by # One observer to rule them all.

The key feature of this is that you pass the list of potential buttons into an lapply where the FUN argument contains an observeEvent call.

library(shiny)
library(dplyr)

shinyApp(
  ui = 
    fluidPage(
      # Display the row for the button clicked
      verbatimTextOutput("details"),
      fluidRow(
        column(width = 3,
               selectInput(inputId ="data_source",
                           label = "Select a data set",
                           choices = c("mtcars", "iris"))),
        column(width = 9,
               uiOutput("show_table"))
      )
    ),
  
  server = 
    shinyServer(function(input, output, session){
      
      # Store the details of the clicked row
      Data <- reactiveValues(
        Info = NULL
      )
      
      # One Observer to Rule Them All (evil cackle)
      # Update the Data$Info value.
      observe({
        # Identify all of the buttons in the table.
        # Note that I assumed the same prefix on all buttons, and 
        # they only differ on the number following the underscore
        # This must happen in an observed since the number of rows 
        # in the table is not fixed.
        input_btn <- paste0("btn_", seq_len(nrow(display_table())))
        lapply(input_btn,
               function(x){
                 observeEvent(
                   input[[x]],
                   {
                     i <- as.numeric(sub("btn_", "", x))
                     Data$Info <- display_table()[i, -length(display_table())]
                   }
                 )
               })
      })
      
      # Generate the table of data.  
      display_table <- 
        reactive({
          tbl <- 
            get(input$data_source) %>% 
            # Add the row names as a column (not always useful)
            cbind(row_id = rownames(.),
                  .) %>% 
            # Add the action buttons as the last column
            mutate(button = vapply(row_number(),
                                   function(i){
                                     actionButton(inputId = paste0("btn_", i),
                                                  label = "View Details") %>% 
                                       as.character()
                                   },
                                   character(1)))
        })
      
      # Render the table with the action buttons
      output$show_table <- 
        renderUI({
            display_table() %>% 
            select(row_id, button) %>% 
            knitr::kable(format = "html",
                         # very important to use escape = FALSE
                         escape = FALSE) %>% 
            HTML()
        })
      
      # Print the details to the screen.
      output$details <- 
        renderPrint({
          req(Data$Info)
          Data$Info
        })
    })

)

#3

You've gotten a lot further than I ever did on programatically creating the required number of observeEvents()!

Before figuring out the JavaScript solution, I was using copy/paste-paste-paste... to create 100 observeEvents() and limited the table to 100 rows using pagination. Pagination turns out to be a lot more complicated than it looks, however, and both our solutions are better than that.