开发者问题收集

在 Shiny 应用程序中使用 `renderUI` 时附加 javascript 侦听器

2022-06-17
289

我最终试图捕获用户在显示一系列直方图后单击它们所花费的时间。但是,在下面的示例应用中,在加载应用时会出现 javascript 错误:

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)

这可能是因为 document.getElementById("img") 在加载应用时找不到 img ,但我不知道如何解决。

当直方图显示在 renderUI 之外时,我可以让它工作,但我需要从服务器动态更改直方图,因此我需要它与渲染的 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"))
      
    })
  }
)
2个回答

这里有一种避免使用 renderUI 并使用 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)

我不知道是否有充分的理由让您将图输出为图像并通过 renderImage 显示它,而不是直接使用 renderPlot - 但这里是 renderPlot 版本:

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:如果您仍然对如何解决 renderUI 问题感兴趣,请查看以下 GitHub 上的帖子

ismirsehregal
2022-06-17

从客户端的角度来看,在 renderUI 另一个元素之前,文档似乎已完全加载。因此 JQuery $(document).ready(...) 表示可以继续尝试将事件附加到尚不存在的元素。

避免 renderUI 的选项已给出。如果您不想要“占位符”空白,您可以在渲染时将图像高度设置为零:

ui <- fluidPage(
## ...
    imageOutput("img", click = "photo_click",
    height = 0
    )
## ...
2022-06-17