我正在尝试将两个常见的共享JS回调组合到一个R数据表闪亮应用程序中(具有单选按钮(请参阅https://yihui.shinyapps.io/DT-radio/和从Shiny中的单选按钮提取用户输入值) DT 到数据框或列表)并在子/父表中嵌套行(请参阅https://stackoverflow.com/a/56599838/10624798 以及许多其他地方)它们都可以单独工作,但不能一起使用。我不确定我是否错误地绑定了 JS,或者 JS 在某种程度上相互矛盾?无论哪种方式,我都无法再访问用户输入(如果我查看输入 $xxx 的结构,则仅返回 null)。我已经包含了一个小例子(尽管仍然很长)来表达我的意思。
library(shiny)
library(DT)
library(shinyWidgets)
library(tidyverse)
shinyApp(
ui = fluidPage(
title = 'Radio button and a dropdown manue ',
sliderInput("n_rows_table", "Number of rows:",
min = 0, max = 10,
value = 5),
actionBttn(
inputId = "btnCancel",
label = "Make tables",
size = "sm",
color = "warning"
),
p("THIS EXAMPLE DOES NOT WORK!"),
DT::dataTableOutput("datatable"),
verbatimTextOutput('sel'),
p("THIS SIMPLER EXAMPLE DOES WORK!"),
DT::dataTableOutput("datatable2"),
verbatimTextOutput('sel2'),
p("These of the R6 input class objects, the the ones from the first tabel do not show up"),
verbatimTextOutput('sel_all'),
),
server = function(input, output, session) {
# Ideally instead of working with a counter,
# this would just override the old value so instead of a_1, a_2,
# everything you click the button it just sets input$a back to null
# until the users clicks again.
# But in the meantime this is a work around
counter <- reactiveValues(countervalue = 0) # Defining & initializing the reactiveValues object
observeEvent(input$btnCancel, {
counter$countervalue <- counter$countervalue + 1 # if the add button is clicked, increment the value by 1 and update it
})
# ----- Create a table based on the number of rows from the slider
# ----- and create it when the user clicks the button
data_for_table <- eventReactive(
input$btnCancel,
{
tibble(
let_rowid = paste0(letters[1:input$n_rows_table], "_", counter$countervalue ),
val_1 = round(runif(input$n_rows_table, 0, 10), 1),
val_2 = round(rnorm(input$n_rows_table), 2),
val_3 = round(rnorm(input$n_rows_table), 2),
val_4 = letters[1:input$n_rows_table],
Yes = "Yes",
No = "No",
Maybe = "Maybe",
result = NA # ideally the what ever selection in yes/no/maybe shows up in this column (future improvement)
) %>%
mutate(oplus = "⊕") %>%
relocate(oplus) %>%
mutate(
Yes = sprintf('<input type="radio" name="%s" value="%s"/>', let_rowid , Yes),
No = sprintf('<input type="radio" name="%s" value="%s"/>', let_rowid , No),
Maybe = sprintf('<input type="radio" name="%s" value="%s"/>', let_rowid , Maybe)
) %>%
nest(datalist = c(val_3, val_4)) %>%
mutate(datalist = map(datalist, as.list)) %>%
mutate(datalist = map(datalist, list))
})
# ----- Render the table
# ----- The table renders ok.
output$datatable <- DT::renderDT({
parentRows <- which(data_for_table()[,1] != "")
# ------ This JS is neede to make the child/parent dropdown
callback <- JS(
sprintf("var parentRows = [%s];", toString(parentRows-1)),
sprintf("var j0 = %d;", 0),
"var nrows = table.rows().count();",
"for(let i = 0; i < nrows; ++i){",
" var $cell = table.cell(i,j0).nodes().to$();",
" if(parentRows.indexOf(i) > -1){",
" $cell.css({cursor: 'pointer'});",
" }else{",
" $cell.removeClass('details-control');",
" }",
"}",
"",
"// --- make the table header of the nested table --- //",
"var formatHeader = function(d, childId){",
" if(d !== null){",
" var html = ",
" '<table class=\"display compact hover\" ' + ",
" 'style=\"padding-left: 30px;\" id=\"' + childId + ",
" '\"><thead><tr>';",
" var data = d[d.length-1] || d.datalist;",
" for(let key in data[0]){",
" html += '<th>' + key + '</th>';",
" }",
" html += '</tr></thead></table>'",
" return html;",
" } else {",
" return '';",
" }",
"};",
"",
"// --- row callback to style rows of child tables --- //",
"var rowCallback = function(row, dat, displayNum, index){",
" if($(row).hasClass('odd')){",
" $(row).css('background-color', 'papayawhip');",
" $(row).hover(function(){",
" $(this).css('background-color', '#E6FF99');",
" }, function(){",
" $(this).css('background-color', 'papayawhip');",
" });",
" } else {",
" $(row).css('background-color', 'lemonchiffon');",
" $(row).hover(function(){",
" $(this).css('background-color', '#DDFF75');",
" }, function(){",
" $(this).css('background-color', 'lemonchiffon');",
" });",
" }",
"};",
"",
"// --- header callback to style header of child tables --- //",
"var headerCallback = function(thead, data, start, end, display){",
" $('th', thead).css({",
" 'border-top': '3px solid indigo',",
" 'color': 'indigo',",
" 'background-color': '#fadadd'",
" });",
"};",
"",
"// --- make the datatable --- //",
"var formatDatatable = function(d, childId){",
" var data = d[d.length-1] || d.datalist;",
" var colNames = Object.keys(data[0]);",
" var columns = colNames.map(function(x){",
" return {data: x.replace(/\\./g, '\\\\\\.'), title: x};",
" });",
" var id = 'table#' + childId;",
" if(colNames.indexOf('datalist') === -1){",
" var subtable = $(id).DataTable({",
" 'data': data,",
" 'columns': columns,",
" 'autoWidth': true,",
" 'deferRender': true,",
" 'info': false,",
" 'lengthChange': false,",
" 'ordering': data.length > 1,",
" 'order': [],",
" 'paging': false,",
" 'scrollX': false,",
" 'scrollY': false,",
" 'searching': false,",
" 'sortClasses': false,",
" 'rowCallback': rowCallback,",
" 'headerCallback': headerCallback,",
" 'columnDefs': [{targets: '_all', className: 'dt-center'}]",
" });",
" } else {",
" var subtable = $(id).DataTable({",
" 'data': data,",
" 'columns': columns,",
" 'autoWidth': true,",
" 'deferRender': true,",
" 'info': false,",
" 'lengthChange': false,",
" 'ordering': data.length > 1,",
" 'order': [],",
" 'paging': false,",
" 'scrollX': false,",
" 'scrollY': false,",
" 'searching': false,",
" 'sortClasses': false,",
" 'rowCallback': rowCallback,",
" 'headerCallback': headerCallback,",
" 'columnDefs': [",
" {targets: -1, visible: false},",
" {targets: 0, orderable: false, className: 'details-control'},",
" {targets: '_all', className: 'dt-center'}",
" ]",
" }).column(0).nodes().to$().css({cursor: 'pointer'});",
" }",
"};",
"",
"// --- display the child table on click --- //",
"// array to store id's of already created child tables",
"var children = [];",
"table.on('click', 'td.details-control', function(){",
" var tbl = $(this).closest('table'),",
" tblId = tbl.attr('id'),",
" td = $(this),",
" row = $(tbl).DataTable().row(td.closest('tr')),",
" rowIdx = row.index();",
" if(row.child.isShown()){",
" row.child.hide();",
" td.html('⊕');",
" } else {",
" var childId = tblId + '-child-' + rowIdx;",
" if(children.indexOf(childId) === -1){",
" // this child has not been created yet",
" children.push(childId);",
" row.child(formatHeader(row.data(), childId)).show();",
" td.html('⊖');",
" formatDatatable(row.data(), childId, rowIdx);",
" }else{",
" // this child has already been created",
" row.child(true);",
" td.html('⊖');",
" }",
" }",
"}); ",
"// --- add radio button functionality --- //",
"table.rows().every(function(i, tab, row) {",
" var $this = $(this.node());",
" $this.attr('id', this.data()[0]);",
" $this.addClass('shiny-input-radiogroup');",
" });",
" Shiny.unbindAll(table.table().node());",
" Shiny.bindAll(table.table().node());")
datatable(
data_for_table(),
escape = F,
rownames = F,
callback = callback,
options = list(
dom = 't',
paging = FALSE,
ordering = FALSE,
paging = FALSE,
searching = FALSE,
columnDefs = list(
list(
visible = FALSE,
targets = c(c(1, ncol(data_for_table())-1)) # do not show certain ID variables, we do not need
),
list(
orderable = FALSE,
className = "details-control",
targets = 0
),
list(
className = "dt-left",
targets = "_all"
)
)
)
)
},
server = F)
list_results <- reactive({
list_values <- list()
for (i in unique(data_for_table()$let_rowid)) {
list_values[[i]] <- paste0(i, ": ", input[[i]])
}
list_values
})
output$sel = renderPrint({
list_results()
})
####################################
## this simpler version does work ##
####################################
data_for_table2 <- eventReactive(
input$btnCancel,
{
tibble(
let_rowid = paste0(letters[11:(10+input$n_rows_table)], "_", counter$countervalue ),
val_1 = round(runif(input$n_rows_table, 0, 10), 1),
val_2 = round(rnorm(input$n_rows_table), 2),
val_3 = round(rnorm(input$n_rows_table), 2),
val_4 = letters[1:input$n_rows_table],
Yes = "Yes",
No = "No",
Maybe = "Maybe",
result = NA # ideally the what ever selection in yes/no/maybe shows up in this column (future improvement)
) %>%
mutate(
Yes = sprintf('<input type="radio" name="%s" value="%s"/>', let_rowid , Yes),
No = sprintf('<input type="radio" name="%s" value="%s"/>', let_rowid , No),
Maybe = sprintf('<input type="radio" name="%s" value="%s"/>', let_rowid , Maybe)
) })
output$datatable2 <- DT::renderDT({
# ---- only difference here is he lack of a drop down.
callback <- JS("table.rows().every(function(i, tab, row) {
var $this = $(this.node());
$this.attr('id', this.data()[0]);
$this.addClass('shiny-input-radiogroup');
});
Shiny.unbindAll(table.table().node());
Shiny.bindAll(table.table().node());")
datatable(
data_for_table2(),
escape = F,
rownames = F,
callback = callback,
options = list(
dom = 't',
paging = FALSE,
ordering = FALSE,
paging = FALSE,
searching = FALSE,
columnDefs = list(
# list(
# visible = FALSE,
# targets = c(ncol(data_for_table2())-1+0) # do not show certain ID variables, we do not need
# ),
list(
orderable = FALSE,
className = "details-control",
targets = 0
),
list(
className = "dt-left",
targets = "_all"
)
)
)
)
},
server = F)
list_results2 <- reactive({
list_values <- list()
for (i in unique(data_for_table2()$let_rowid)) {
list_values[[i]] <- paste0(i, ": ", input[[i]])
}
list_values
})
output$sel2 = renderPrint({
list_results2()
})
# make this regex working
list_results_all <- reactive({
list_values_all <- list()
for(i in names(input)[grepl("([a-z]{1}_)([0-9]{1,3})",names(input))]){
list_values_all[[i]] <- tibble(id = i, value = paste0(input[[i]]))
}
do.call(rbind, list_values_all)
})
output$sel_all = renderPrint({
list_results_all()
})
}
)
给出的答案解决了我最初向 MWE 提出的问题。然而,深入研究一下,通过向表中添加额外类型的按钮,单选按钮仍然会损坏(例如,通过添加删除按钮,如果我首先单击单选按钮,然后单击删除,它确实可以工作,但不能反之亦然)。有没有办法让这种行为更加一致?
(部分代码取自此问题的答案:R Shiny:删除数据表中的行按钮)
library(shiny)
library(DT)
library(shinyWidgets)
library(tidyverse)
# 1) These two function allows for setting a remove function in the app.
# This code is taken from here: https://stackoverflow.com/questions/53908266/r-shiny-remove-row-button-in-data-table
getRemoveButton <- function(n, idS = "", lab = "Pit") {
if (stringr::str_length(idS) > 0) idS <- paste0(idS, "-")
ret <- shinyInput(actionButton, n,
'button_', label = "Remove",
onclick = sprintf('Shiny.onInputChange(\"%sremove_button_%s\", this.id)' ,idS, lab))
return (ret)
}
shinyInput <- function(FUN, n, id, ses, ...) {
as.character(FUN(paste0(id, n), ...))
}
shinyApp(
ui = fluidPage(
title = 'Radio button and a dropdown manue ',
sliderInput("n_rows_table", "Number of rows:",
min = 0, max = 10,
value = 5),
actionBttn(
inputId = "btnCancel",
label = "Make tables",
size = "sm",
color = "warning"
),
p("THIS EXAMPLE DOES NOT WORK!"),
DT::dataTableOutput("datatable"),
verbatimTextOutput('sel'),
#p("THIS SIMPLER EXAMPLE DOES WORK!"),
#DT::dataTableOutput("datatable2"),
#verbatimTextOutput('sel2'),
#("These of the R6 input class objects, the the ones from the first tabel do not show up"),
#verbatimTextOutput('sel_all'),
),
server = function(input, output, session) {
# Ideally instead of working with a counter,
# this would just override the old value so instead of a_1, a_2,
# everything you click the button it just sets input$a back to null
# until the users clicks again.
# But in the meantime this is a work around
counter <- reactiveValues(countervalue = 0) # Defining & initializing the reactiveValues object
observeEvent(input$btnCancel, {
counter$countervalue <- counter$countervalue + 1 # if the add button is clicked, increment the value by 1 and update it
})
values <- reactiveValues(tab = NULL)
# ----- Create a table based on the number of rows from the slider
# ----- and create it when the user clicks the button
observeEvent(
input$btnCancel,
{
values$tab <- tibble(
let_rowid = paste0(letters[1:input$n_rows_table], "_", counter$countervalue ),
val_1 = round(runif(input$n_rows_table, 0, 10), 1),
val_2 = round(rnorm(input$n_rows_table), 2),
val_3 = round(rnorm(input$n_rows_table), 2),
val_4 = letters[1:input$n_rows_table],
Yes = "Yes",
No = "No",
Maybe = "Maybe",
result = NA # ideally the what ever selection in yes/no/maybe shows up in this column (future improvement)
) %>%
mutate(oplus = "⊕") %>%
relocate(oplus) %>%
mutate(
Yes = sprintf('<input type="radio" name="%s" value="%s"/>', let_rowid , Yes),
No = sprintf('<input type="radio" name="%s" value="%s"/>', let_rowid , No),
Maybe = sprintf('<input type="radio" name="%s" value="%s"/>', let_rowid , Maybe)
) %>%
## THIS IS NEW ###################################################
mutate(id = 1:n()) %>% #
rowwise() %>% #
mutate(Remove = getRemoveButton(id, idS = "", lab = "Tab1"))%>% #
ungroup() %>% #
##################################################################
nest(datalist = c(val_3, val_4)) %>%
mutate(datalist = map(datalist, as.list)) %>%
mutate(datalist = map(datalist, list))
})
# add a proxy table
proxyTable <- DT::dataTableProxy("tab")
# ----- Render the table
# ----- The table renders ok.
output$datatable <- DT::renderDT({
parentRows <- which(values$tab[,1] != "")
# ------ This JS is neede to make the child/parent dropdown
callback <- JS(
sprintf("var parentRows = [%s];", toString(parentRows-1)),
sprintf("var j0 = %d;", 0),
"var nrows = table.rows().count();",
"for(let i = 0; i < nrows; ++i){",
" var $cell = table.cell(i,j0).nodes().to$();",
" if(parentRows.indexOf(i) > -1){",
" $cell.css({cursor: 'pointer'});",
" }else{",
" $cell.removeClass('details-control');",
" }",
"}",
"",
"// --- make the table header of the nested table --- //",
"var formatHeader = function(d, childId){",
" if(d !== null){",
" var html = ",
" '<table class=\"display compact hover\" ' + ",
" 'style=\"padding-left: 30px;\" id=\"' + childId + ",
" '\"><thead><tr>';",
" var data = d[d.length-1] || d.datalist;",
" for(let key in data[0]){",
" html += '<th>' + key + '</th>';",
" }",
" html += '</tr></thead></table>'",
" return html;",
" } else {",
" return '';",
" }",
"};",
"",
"// --- row callback to style rows of child tables --- //",
"var rowCallback = function(row, dat, displayNum, index){",
" if($(row).hasClass('odd')){",
" $(row).css('background-color', 'papayawhip');",
" $(row).hover(function(){",
" $(this).css('background-color', '#E6FF99');",
" }, function(){",
" $(this).css('background-color', 'papayawhip');",
" });",
" } else {",
" $(row).css('background-color', 'lemonchiffon');",
" $(row).hover(function(){",
" $(this).css('background-color', '#DDFF75');",
" }, function(){",
" $(this).css('background-color', 'lemonchiffon');",
" });",
" }",
"};",
"",
"// --- header callback to style header of child tables --- //",
"var headerCallback = function(thead, data, start, end, display){",
" $('th', thead).css({",
" 'border-top': '3px solid indigo',",
" 'color': 'indigo',",
" 'background-color': '#fadadd'",
" });",
"};",
"",
"// --- make the datatable --- //",
"var formatDatatable = function(d, childId){",
" var data = d[d.length-1] || d.datalist;",
" var colNames = Object.keys(data[0]);",
" var columns = colNames.map(function(x){",
" return {data: x.replace(/\\./g, '\\\\\\.'), title: x};",
" });",
" var id = 'table#' + childId;",
" if(colNames.indexOf('datalist') === -1){",
" var subtable = $(id).DataTable({",
" 'data': data,",
" 'columns': columns,",
" 'autoWidth': true,",
" 'deferRender': true,",
" 'info': false,",
" 'lengthChange': false,",
" 'ordering': data.length > 1,",
" 'order': [],",
" 'paging': false,",
" 'scrollX': false,",
" 'scrollY': false,",
" 'searching': false,",
" 'sortClasses': false,",
" 'rowCallback': rowCallback,",
" 'headerCallback': headerCallback,",
" 'columnDefs': [{targets: '_all', className: 'dt-center'}]",
" });",
" } else {",
" var subtable = $(id).DataTable({",
" 'data': data,",
" 'columns': columns,",
" 'autoWidth': true,",
" 'deferRender': true,",
" 'info': false,",
" 'lengthChange': false,",
" 'ordering': data.length > 1,",
" 'order': [],",
" 'paging': false,",
" 'scrollX': false,",
" 'scrollY': false,",
" 'searching': false,",
" 'sortClasses': false,",
" 'rowCallback': rowCallback,",
" 'headerCallback': headerCallback,",
" 'columnDefs': [",
" {targets: -1, visible: false},",
" {targets: 0, orderable: false, className: 'details-control'},",
" {targets: '_all', className: 'dt-center'}",
" ]",
" }).column(0).nodes().to$().css({cursor: 'pointer'});",
" }",
"};",
"",
"// --- display the child table on click --- //",
"// array to store id's of already created child tables",
"var children = [];",
"table.on('click', 'td.details-control', function(){",
" var tbl = $(this).closest('table'),",
" tblId = tbl.attr('id'),",
" td = $(this),",
" row = $(tbl).DataTable().row(td.closest('tr')),",
" rowIdx = row.index();",
" if(row.child.isShown()){",
" row.child.hide();",
" td.html('⊕');",
" } else {",
" var childId = tblId + '-child-' + rowIdx;",
" if(children.indexOf(childId) === -1){",
" // this child has not been created yet",
" children.push(childId);",
" row.child(formatHeader(row.data(), childId)).show();",
" td.html('⊖');",
" formatDatatable(row.data(), childId, rowIdx);",
" }else{",
" // this child has already been created",
" row.child(true);",
" td.html('⊖');",
" }",
" }",
"}); ",
"// --- add radio button functionality --- //",
"table.rows().every(function(i, tab, row) {",
" var $this = $(this.node());",
" $this.attr('id', this.data()[1]);",
" $this.addClass('shiny-input-radiogroup');",
" });",
" Shiny.unbindAll(table.table().node());",
" Shiny.bindAll(table.table().node());")
datatable(
values$tab,
escape = F,
rownames = F,
callback = callback,
options = list(
dom = 't',
paging = FALSE,
ordering = FALSE,
paging = FALSE,
searching = FALSE,
columnDefs = list(
list(
visible = FALSE,
targets = c(c(1, ncol(values$tab)-1)) # do not show certain ID variables, we do not need
),
list(
orderable = FALSE,
className = "details-control",
targets = 0
),
list(
className = "dt-left",
targets = "_all"
)
)
)
) },
server = F)
observeEvent(input$remove_button_Tab1, {
myTable <- values$tab
s <- as.numeric(strsplit(input$remove_button_Tab1, "_")[[1]][2])
myTable <- filter(myTable, id != s)
replaceData(proxyTable, myTable, resetPaging = FALSE)
values$tab <- myTable
})
list_results <- reactive({
list_values <- list()
for (i in unique( values$tab$let_rowid)) {
list_values[[i]] <- paste0(i, ": ", input[[i]])
}
list_values
})
output$sel = renderPrint({
list_results()
})
####################################
## this simpler version does work ##
####################################
# removed for now
}
)
我现在也尝试了这里给出的解决方案:https://stefanengineering.com/2019/07/06/delete-rows-from-shiny-dt-datatable/,但这给出了完全相同的问题。
您在 JavaScript 回调中分配了错误的
id
元素,因为在您的表中,第一列实际上是 &oplus
列,但您想要第二列。
因此改变这个
table.rows().every(function(i, tab, row) {
var $this = $(this.node());
$this.attr('id', this.data()[0]); // this.data()[0] refers to the firts column, i.e. ⊕
$this.addClass('shiny-input-radiogroup');
});
对此:
table.rows().every(function(i, tab, row) {
var $this = $(this.node());
$this.attr('id', this.data()[1]); // the id is in the second column in your case
$this.addClass('shiny-input-radiogroup');
});