22

I have an R Shiny App with a Data Table. One column contains action buttons with a unique ID. I'd like to handle clicks on those buttons, but unfortunately, my event handling code (a simple print statement) is never executed. See this self-contained example (app.R):

library(shiny)
library(DT)

ui <- shinyUI(
    fluidPage(
        title = "DataTable with Buttons",
        fluidRow(
            column(
                width = 8,
                dataTableOutput("employees")
            )
        )
    )
)

server <- shinyServer(function(input, output) {
    df <- data.frame(
        name = c('Dilbert', 'Alice', 'Wally', 'Ashok', 'Dogbert'),
        motivation = c(62, 73, 3, 99, 52),
        stringsAsFactors = FALSE
    )
    fireButtons <- list()
    fireButtonIds <- list()
    for (r in rownames(df)) {
        id <- paste("fire_", r, sep = "")
        fireButtonIds[[r]] <- id
        button <- actionButton(id, label = "Fire")
        fireButtons[[r]] <- as.character(button)
    }
    df$actions <- fireButtons
    dt <- datatable(df, colnames = c("#", "Name", "Motivation", "Actions"))
    output$employees <- renderDataTable(dt)


    for (id in fireButtonIds) {
        # binding doesn't work
        # - is the path wrong?
        # - is it because the button is really a string, not an object?
        observeEvent(input$employees$x$data$actions[[id]], {
            print(paste("click on", i))
        })
    }
})

shinyApp(ui = ui, server = server)

I see two possible problems:

  1. The path I'm using (input$employees$x$data$actions[[id]]) is just wrong
  2. The path I'm using is correct, but it doesn't point to something that could actually be handled, i.e. it's just a HTML string and not a button object.

Or maybe there's a much better approch to put buttons inside a data table...?

Patrick Bucher
  • 1,302
  • 2
  • 14
  • 36

1 Answers1

44

Does this accomplish what you're trying to do?

library(shiny)
library(DT)

shinyApp(
  ui <- fluidPage(
    DT::dataTableOutput("data"),
    textOutput('myText')
  ),

  server <- function(input, output) {

    myValue <- reactiveValues(employee = '')

    shinyInput <- function(FUN, len, id, ...) {
      inputs <- character(len)
      for (i in seq_len(len)) {
        inputs[i] <- as.character(FUN(paste0(id, i), ...))
      }
      inputs
    }

    df <- reactiveValues(data = data.frame(

      Name = c('Dilbert', 'Alice', 'Wally', 'Ashok', 'Dogbert'),
      Motivation = c(62, 73, 3, 99, 52),
      Actions = shinyInput(actionButton, 5, 'button_', label = "Fire", onclick = 'Shiny.onInputChange(\"select_button\",  this.id)' ),
      stringsAsFactors = FALSE,
      row.names = 1:5
    ))


    output$data <- DT::renderDataTable(
      df$data, server = FALSE, escape = FALSE, selection = 'none'
    )

    observeEvent(input$select_button, {
      selectedRow <- as.numeric(strsplit(input$select_button, "_")[[1]][2])
      myValue$employee <<- paste('click on ',df$data[selectedRow,1])
    })


    output$myText <- renderText({

      myValue$employee

    })

  }
)
kostr
  • 836
  • 7
  • 13
  • Yes, it does! I still need to understand the code completely, but it works as intended. Thank you! – Patrick Bucher Aug 18 '17 at 06:14
  • 4
    Sorry for the lack of explanation. The first function you see being created in the server code is one that creates & names inputs. Then, we apply that function to create a column in a reactive dataframe. That dataframe is placed into a DT output, where it appears in the UI. The event handler function (observeEvent) reacts anytime any select_button (which we created 5 of) is hit. In this event, it changes a reactive value (myValue$employee) to match the row ID name of the employee. Then that value is passed to the myText output and rendered in the UI. Hope that makes sense! – kostr Aug 18 '17 at 15:10
  • 2
    This doesn't actually fire if you click twice on the same button. Any fix for that? – Jan Stanstrup Oct 06 '17 at 13:35
  • 4
    Old question, but for the problem of not firing you have 2 options. With `shiny` version >= 1.1 you can use `Shiny.setInputValue(\"select_button\", this.id, {priority: \"event\"})` (`setInputValue` is basically the same as `onInputChange`). With `shiny` versions <1.1 you can use `Shiny.onInputChange(\"select_button\", [this.id, Math.random()]) `. The problem is that `shiny` reacts only on changes, thus you have to set the value in a way that `shiny` knows it is a new value. In the newer version you can do that explicitly, in the older version you have to use a trick with a random variable. – thothal Aug 24 '18 at 14:04
  • For the older version you also have to adapt how to read back tha value in the observer: `strsplit(input$select_button[1], "_")`. This is because not `input$select_button` returns a vector, with the random variable in the second position and the `id` in the first. – thothal Aug 24 '18 at 14:06
  • @kostr this is a super helpful post. Apologies for the bother, but I am having some issues adding an icon to the button instead of a label. Is this possible with your solution? Thanks in advance! – ben Nov 09 '18 at 20:07
  • 1
    @ben try using this as a guide: https://stackoverflow.com/questions/44841346/adding-an-image-to-shiny-action-button You should be able to NULL out the label and add a style with a URL embedded into the actionbutton input. – kostr Nov 13 '18 at 17:44
  • Cheers, thanks @kostr, I actually just recently figured it out. Thanks again for this helpful post – ben Nov 13 '18 at 21:22
  • @thothal Thank you! I had been trying to debug this for too long! – Scientist_jake Jan 21 '21 at 20:03
  • for anyone using modules – remember to add module name to onclick selector! `Shiny.onInputChange(\"moduleName-select_button\", this.id)` or `onclick = sprintf('Shiny.setInputValue(\"%s\", this.id, {priority: \"event\"})', session$ns('select_button'))` – vladli May 01 '23 at 11:47