1

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"))
      
    })
  }
)
1
  • There is no need to use renderUI. renderImage is also reactive, so you can change the histogram from the server without renderUI. Is there any reason why you don't use renderPlot? Commented Jun 17, 2022 at 6:49

2 Answers 2

2

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you! I was hoping to use RenderUI because I render various various pages that a user progresses through. But perhaps I can use this and re-write the code to use conditionalPanel or perhaps just rendering the various UI components conditionally.
2

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
    )
## ...

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.