Home > Software design >  Attach javascript listener when using `renderUI` in Shiny app
Attach javascript listener when using `renderUI` in Shiny app

Time:06-17

I am ultimately trying to capture the time it takes a user to click on a series of histograms after they are displayed. However, in the example app below, a javascript error appears at the loading of the app:

Uncaught TypeError: Cannot set properties of null (setting 'onclick') at HTMLDocument. ((index):31:30) at e (jquery.min.js:2:30038) at t (jquery.min.js:2:30340)

Presumably this is because document.getElementById("img") doesn't find img at the loading of the app, but I don't know how to resolve that.

I can get this to work when the histogram is displayed outside of the renderUI, but I need to change the histogram dynamically from the server, so I need this to work with a rendered UI.


shinyApp(
  
  ui = fluidPage(
    tags$script('
    // ------ javascript code ------
    $(document).ready(function(){
        // function to set Shiny input value to current time: 
        const clockEvent = function(inputName){Shiny.setInputValue(inputName, new Date().getTime())}
        // trigger when the value of output id "img" changes: 
        $(document).on("shiny:value",
               function(event){
                   if (event.target.id === "img") {clockEvent("displayed_at")}
               }
              )
        // trigger when the image, after being sent or refreshed, is clicked:
        document.getElementById("img")
                    .onclick = function(){clockEvent("reacted_at")}
    })
    // ------------------------------
    '),
    
    sidebarLayout(
      sidebarPanel(

        actionButton(inputId="render_dynamic", label= "Create Dynamic UI")
      ),
      mainPanel(
        uiOutput("dynamic")
      )
    )
  ),
  server = function(input, output) {
    
    output$img <- renderImage({
      outfile <- tempfile(fileext='.png')
      png(outfile, width=400, height=400)
      hist(rnorm(100))
      dev.off()
      
      list(src = outfile,
           contentType = "image/jpeg")
    },
    deleteFile = FALSE)

    output$reaction_time <- renderPrint(paste('reaction time (ms)', input$reacted_at - input$displayed_at))
    
    output$dynamic <- renderUI({
      
      req(input$render_dynamic > 0)
      
      div(id = 'image_container',
          imageOutput("img", click = "photo_click"),
          textOutput("reaction_time"))
      
    })
  }
)

CodePudding user response:

Here is an approach avoiding renderUI and using bindEvent:

library(shiny)

ui = fluidPage(
  tags$script('
    // ------ javascript code ------
    $(document).ready(function(){
        // function to set Shiny input value to current time: 
        const clockEvent = function(inputName){Shiny.setInputValue(inputName, new Date().getTime())}
        // trigger when the value of output id "img" changes: 
        $(document).on("shiny:value",
               function(event){
                   if (event.target.id === "img") {clockEvent("displayed_at")}
               }
              )
        // trigger when the image, after being sent or refreshed, is clicked:
        document.getElementById("img")
                    .onclick = function(){clockEvent("reacted_at")}
    })
    // ------------------------------
    '),
  sidebarLayout(
    sidebarPanel(
      actionButton(inputId="render_dynamic", label= "Create Dynamic UI")
    ),
    mainPanel(
      imageOutput("img"),
      textOutput("reaction_time")
    )
  )
)

server = function(input, output, session) {
  output$img <- renderImage({
    outfile <- tempfile(fileext='.png')
    png(outfile, width=400, height=400)
    hist(rnorm(100))
    dev.off()
    list(src = outfile,
         contentType = "image/jpeg")
  }, deleteFile = FALSE) |> bindEvent(input$render_dynamic)
  
  output$reaction_time <- renderPrint({
      paste('reaction time (ms)', input$reacted_at - input$displayed_at)
  }) |> bindEvent(input$reacted_at)
}

shinyApp(ui, server)

I don't know if there is a good reason for you to output the plot as an image and show it via renderImage instead of using renderPlot directly - but here is the renderPlot version:

library(shiny)

ui = fluidPage(
  tags$script('
    // ------ javascript code ------
    $(document).ready(function(){
        // function to set Shiny input value to current time: 
        const clockEvent = function(inputName){Shiny.setInputValue(inputName, new Date().getTime())}
        // trigger when the value of output id "img" changes: 
        $(document).on("shiny:value",
               function(event){
                   if (event.target.id === "img") {clockEvent("displayed_at")}
               }
              )
        // trigger when the image, after being sent or refreshed, is clicked:
        document.getElementById("img")
                    .onclick = function(){clockEvent("reacted_at")}
    })
    // ------------------------------
    '),
  sidebarLayout(
    sidebarPanel(
      actionButton(inputId="render_dynamic", label= "Create Dynamic UI")
    ),
    mainPanel(
      plotOutput("img"),
      textOutput("reaction_time")
    )
  )
)

server = function(input, output, session) {
  output$img <- renderPlot({
    hist(rnorm(100))
  }) |> bindEvent(input$render_dynamic)
  
  output$reaction_time <- renderPrint({
      paste('reaction time (ms)', input$reacted_at - input$displayed_at)
  }) |> bindEvent(input$reacted_at)
}

shinyApp(ui, server)

PS: If you are still interested in how to solve the renderUI problem please check the following post on GitHub.

CodePudding user response:

Seems like, from the client's perspective, the document is fully loaded before you renderUI another element. So the JQuery $(document).ready(...) gives its OK to proceed with trying to attach an event to an element which is not there (yet).

Options to avoid renderUI have already been given. If you don't want the "placeholder" blank space, you can set the image height to zero upon rendering:

ui <- fluidPage(
## ...
    imageOutput("img", click = "photo_click",
    height = 0
    )
## ...
  • Related