Shiny应用使用renderUI时附加JavaScript监听器报错解决
需要实现的功能:统计一系列直方图从页面渲染完成,到用户点击对应直方图所消耗的时长。
应用初始加载时抛出如下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")时无法找到id为img的DOM元素。
已知现象:如果不通过renderUI渲染直方图,直接在静态UI中放置对应元素,JavaScript事件绑定可正常运行;但业务需求要求从服务端动态更换直方图,必须兼容renderUI渲染的动态UI场景。
shinyApp( ui = fluidPage( tags$script(' // ------ javascript code ------ $(document).ready(function(){ // 定义函数:将当前时间戳写入指定Shiny输入值 const clockEvent = function(inputName){Shiny.setInputValue(inputName, new Date().getTime())} // 监听id为img的输出值更新事件 $(document).on("shiny:value", function(event){ if (event.target.id === "img") {clockEvent("displayed_at")} } ) // 直接给img元素绑定点击事件:初始加载时元素不存在,此处报错 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")) }) } )
原代码在$(document).ready(页面初始DOM加载完成)阶段就直接查找#img元素并绑定点击事件,但#img是通过renderUI动态渲染的,初始页面加载时根本不存在于DOM树中,document.getElementById("img")返回null,给null设置onclick属性自然抛出类型错误。
就算首次点击按钮渲染出元素后手动绑定成功,后续如果动态UI被重新渲染、旧的DOM元素被销毁替换,之前绑定的事件也会失效,无法适配动态渲染场景。
使用事件委托替代直接元素绑定:把点击事件绑定到页面始终存在的父级节点(比如document),当点击事件冒泡到父节点时,再判断点击目标是否匹配#img选择器,匹配才执行对应逻辑。这种方式不需要目标元素在绑定事件时就存在,完全适配动态渲染的DOM场景。
只需要修改原JS代码中点击事件绑定的部分,替换为委托写法即可,修改后的JS代码如下:
$(document).ready(function(){ const clockEvent = function(inputName){Shiny.setInputValue(inputName, new Date().getTime())} // 监听img输出渲染完成的逻辑保持不变 $(document).on("shiny:value", function(event){ if (event.target.id === "img") {clockEvent("displayed_at")} } ) // 改用事件委托绑定点击事件,适配动态渲染元素 $(document).on("click", "#img", function(){ clockEvent("reacted_at") }) })
替换后即可正常统计渲染到点击的耗时,无论renderUI触发多少次重渲染,事件绑定都不会失效,也不会再出现初始加载的报错。
内容的提问来源于stack exchange,提问作者dca

