You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

R Shiny嵌套表格子行:复选框与YN列双向同步实现求助

问题解决:DT嵌套表格子行复选框与YN列双向同步

问题说明

需要实现:

  • 嵌套表格子行加载时,复选框值与对应YN列数值一致
  • 用户点击/取消复选框时,YN列同步更新
  • 父行不显示复选框

现有代码可运行,但子行复选框点击后YN列无法更新,以下是修正方案:

问题根源

  1. 复选框ID冲突:所有子行复选框使用同一前缀check,无法区分所属子表格
  2. 表格对象错误:原回调操作的是父表格而非动态生成的子表格
  3. 事件绑定时机错误:页面加载时绑定事件,但子表格是点击父行后才生成,导致事件无法绑定到动态元素

修正后的完整代码

library(DT)
library(purrr)

### 构造主表和子表数据 ###
subdat <- data.frame(
  Gene_SUB=c("MUTYH","AR"),
  Location_SUB=c("chr1:45797228","chr2:45797228"),
  Exon_SUB=c(NA,23),          
  HGVS_p_SUB=c("NP_001121897.1:p.(Gly396Asp)","NP_001121897.1:p.(Gly396Asp)"),
  stopA=c(45797278,114925456),
  YN = c(FALSE, FALSE),
  stringsAsFactors = FALSE
)

maindat <- data.frame(
  Gene=c("MUTYH","AR"),
  Location=c("chr1:45797228","chr2:45797228"),
  Exon=c(NA,23),          
  HGVS_p=c("NP_001121897.1:p.(Gly396Asp)","NP_001121897.1:p.(Gly396Asp)"),
  stopA=c(45797278,114925456),
  stringsAsFactors = FALSE
)

# 生成带唯一ID的复选框:check_父行标识_子行索引
shinyCheckbox <- function(parentId, values) {
  inputs <- character(length(values))
  for(i in seq_along(inputs)) {
    inputs[i] <- 
      as.character(
        checkboxInput(paste0("check_", parentId, "_", i), label = NULL, value = values[i], width = "20px")
      )
  }
  inputs
}

# 按stopA拆分子表,为每个子表生成带唯一ID的复选框
fs <- split(subdat, factor(subdat$stopA, levels = unique(subdat$stopA)))
subdats <- lapply(names(fs), function(id) {
  sub <- fs[[id]]
  sub$check <- shinyCheckbox(id, sub$YN)
  transpose(sub)
})

oplus <- sapply(subdats, function(x) if(length(x)) "&oplus;" else "")
Dat <- cbind(" " = oplus, maindat, details = I(subdats))

### 修正后的回调函数 ###
callback = JS(
  "table.column(1).nodes().to$().css({cursor: 'pointer'});",
  "// 生成子表格唯一ID",
  "var childId = function(d){",
  "  var tail = d.slice(2, d.length - 1);",
  "  return 'child_' + tail.join('_').replace(/[\\s|\\.|'|,|\\(|\\)]/g, '_');",
  "};",
  "// 构造子表格HTML结构",
  "var format = function (d) {",
  "  if (d != null) {",
  "    var id = childId(d);",
  "    var html = '<table class=\"display compact\" id=\"' + id + '\"><thead><tr>';",
  "    for (var key in d[d.length-1][0]) {",
  "      html += '<th>' + key + '</th>';",
  "    }",
  "    html += '</tr></thead></table>';",
  "    return html;",
  "  } else {",
  "    return '';",
  "  }",
  "};",
  "// 子表格行样式回调",
  "var rowCallback = function(row, dat, displayNum, index){",
  "  if($(row).hasClass('odd')){",
  "    for(var j=0; j<dat.length; j++){",
  "      $('td:eq('+j+')', row).css('background-color', 'papayawhip');",
  "    }",
  "  } else {",
  "    for(var j=0; j<dat.length; j++){",
  "      $('td:eq('+j+')', row).css('background-color', 'lemonchiffon');",
  "    }",
  "  }",
  "};",
  "// 子表格表头样式回调",
  "var headerCallback = function(thead, data, start, end, display){",
  "  $('th', thead).css({",
  "    'border-top': '3px solid indigo',", 
  "    'color': 'indigo',",
  "    'background-color': '#fadadd'",
  "  });",
  "};",
  "// 初始化子表格",
  "var format_datatable = function (d) {",
  "  var dataset = [];",
  "  var n = d.length - 1;",
  "  for (var i = 0; i < d[n].length; i++) {",
  "    var datarow = $.map(d[n][i], function (value, index) {",
  "      return [value];",
  "    });",
  "    dataset.push(datarow);",
  "  }",
  "  var id = 'table#' + childId(d);",
  "  var subtable = $(id).DataTable({",
  "                     'data': dataset,",
  "                     'autoWidth': true,",
  "                     'deferRender': true,",
  "                     'info': false,",
  "                     'lengthChange': false,",
  "                     'ordering': d[n].length > 1,",
  "                     'order': [],",
  "                     'paging': false,",
  "                     'scrollX': false,",
  "                     'scrollY': false,",
  "                     'searching': false,",
  "                     'sortClasses': false,",
  "                     'rowCallback': rowCallback,",
  "                     'headerCallback': headerCallback,",
  "                     'columnDefs': [{targets: '_all', className: 'dt-center'}]",
  "                   });",
  "};",
  "// 父行点击展开/收起子表格",
  "table.on('click', 'td.details-control', function () {",
  "  var td = $(this),",
  "      row = table.row(td.closest('tr'));",
  "  if (row.child.isShown()) {",
  "    row.child.hide();",
  "    td.html('&oplus;');",
  "  } else {",
  "    row.child(format(row.data())).show();",
  "    td.html('&CircleMinus;');",
  "    format_datatable(row.data());",
  "  }",
  "});",
  "// 事件委托监听动态生成的复选框点击",
  "$(document).on('click', '[id^=check_]', function(){",
  "  var id = this.getAttribute('id');",
  "  // 解析父行标识和子行索引",
  "  var match = id.match(/check_(.*)_(\\d+)/);",
  "  var parentId = match[1];",
  "  var childRowIdx = parseInt(match[2]) - 1;",
  "  // 获取当前复选框状态",
  "  var isChecked = $(this).prop('checked');",
  "  // 找到对应的子表格",
  "  var childTableId = 'child_' + parentId;",
  "  var subtable = $('#' + childTableId).DataTable();",
  "  // 更新YN列(假设YN列是第5列,从0开始计数)",
  "  subtable.cell(childRowIdx, 5).data(isChecked).draw();",
  "});"
)

### 渲染表格 ###
datatable(Dat, callback = callback, escape = FALSE,
          options = list(
            columnDefs = list(
              list(visible = FALSE, targets = ncol(Dat)),
              list(orderable = FALSE, className = 'details-control', targets = 1),
              list(className = "dt-center", targets = "_all")
            )
          )
)

关键修改点

  • 唯一复选框ID:修改shinyCheckbox函数,生成包含父行标识和子行索引的唯一ID,避免冲突
  • 事件委托绑定:使用$(document).on('click', '[id^=check_]', ...)监听动态生成的复选框,确保事件能触发
  • 定位子表格:通过ID解析找到对应的子表格实例,更新子表格的YN列,而非父表格
  • 列索引确认:确保YN列的索引正确(代码中假设是第5列,从0开始计数,可根据实际列位置调整)

内容的提问来源于stack exchange,提问作者user3781528

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 14:02:33