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