在R Shiny动态renderUI环境中使用Sortable List的问题及解决
动态Shiny UI中Sortable列表的问题修复方案
我帮你梳理下这个问题的核心原因,以及对应的解决办法——你提到的shinyjqui开发版中的orderInput()确实是最适配你场景的方案,我再给你补充细节,同时也提供手动修复原生Sortable绑定的思路。
问题根源
你遇到的Uncaught TypeError: Cannot read property 'toArray' of null错误,本质是动态生成的UI组件没有被Shiny的Input Binding正确初始化:
- 你的自定义
sortableListBinding只会在页面首次加载时执行initialize方法,创建Sortable实例; - 当用
renderUI替换DOM,或用insertUI新增元素时,新的.sortableList元素不会自动触发初始化逻辑,导致Sortable实例不存在,调用toArray()时就会报错; - 同时Shiny也无法识别这些未初始化的组件,自然捕获不到输入值。
推荐方案:使用shinyjqui开发版的orderInput()
这个组件已经封装了动态UI下的Sortable绑定逻辑,不需要你手动写JS,开箱即用:
步骤1:安装开发版包
remotes::install_github("yang-tang/shinyjqui")
步骤2:替换自定义组件,重构代码
用renderUI的版本:
library(shiny) library(shinydashboard) library(stringi) library(shinyjqui) ui <- dashboardPage( dashboardHeader(), dashboardSidebar(), dashboardBody( actionButton('actbtn', 'Add Sortable List'), uiOutput("moreControls"), verbatimTextOutput('sorted_output') )) server <- function(input, output, session) { # 实时输出所有排序后的列表内容 output$sorted_output <- renderPrint({ lapply(0:input$actbtn, function(numb){ input[[paste0("sortable_list", numb)]] }) }) output$moreControls <- renderUI({ current_count <- input$actbtn tagList(lapply(0:current_count, function(numb){ fluidRow( column(6, box( orderInput( inputId = paste0("sortable_list", numb), label = NULL, items = replicate(6, paste(unlist(strsplit(stri_rand_lipsum(1, start_lipsum = F), " "))[1:10], collapse = " ")), connect = NULL # 不需要跨列表拖拽的话设为NULL ), width = 12, height = '300px' )) ) })) }) } shinyApp(ui=ui, server=server)
用insertUI的版本:
# 在server函数里替换成这段observeEvent observeEvent(input$actbtn,{ insertUI( selector = "#actbtn", where = "afterEnd", ui = tags$div(id = paste0('divID_', input$actbtn), box( column( 6, orderInput( inputId = paste0("sortable_list", input$actbtn), label = NULL, items = replicate(6, paste(unlist(strsplit(stri_rand_lipsum(1, start_lipsum = F), " "))[1:10], collapse = " ")) ) ) ) ) ) })
手动修复原生Sortable绑定的方案(不依赖第三方包)
如果你不想用shinyjqui,可以修改你的JS绑定逻辑,确保动态新增的元素能被初始化:
步骤1:修改sortableList.js
var sortableListBinding = new Shiny.InputBinding(); $.extend(sortableListBinding, { find: function(scope) { return $(scope).find(".sortableList"); }, getValue: function(el) { // 先检查Sortable实例是否存在 const sortableInstance = Sortable.get(el); return sortableInstance ? sortableInstance.toArray() : []; }, subscribe: function(el, callback) { $(el).on("change.sortableListBinding", function(e) { callback(); }); }, unsubscribe: function(el) { $(el).off(".sortableListBinding"); }, initialize: function(el) { // 避免重复初始化 if (!Sortable.get(el)) { Sortable.create(el,{ onUpdate: function (evt) { $(el).trigger("change"); } }); } } }); Shiny.inputBindings.register(sortableListBinding); // 新增手动初始化函数,供Shiny动态添加后调用 window.initSortableLists = function() { $(".sortableList").each(function() { sortableListBinding.initialize(this); }); };
步骤2:在Shiny中触发初始化
在UI里添加监听自定义消息的脚本:
ui <- dashboardPage( dashboardHeader(), dashboardSidebar(), dashboardBody( tags$script(HTML(" Shiny.addCustomMessageHandler('initSortable', function() { window.initSortableLists(); }); ")), # 原来的UI元素... ))
在动态生成UI后,调用初始化函数:
output$moreControls <- renderUI({ current_count <- input$actbtn ui <- tagList(lapply(0:current_count, function(numb){ # 原来的sortableList组件代码... })) # 通知前端初始化新的Sortable列表 session$sendCustomMessage("initSortable", NULL) ui })
这种方法需要自己维护绑定逻辑,适合需要高度定制的场景,但不如shinyjqui的方案简洁。
内容的提问来源于stack exchange,提问作者Fideldue
相关产品推荐
相关产品推荐

