am trying to combine two commonly shared JS callbacks into one R datatable shiny app (having radio buttons (see https://yihui.shinyapps.io/DT-radio/ and having nested rows in a child/parent table and having a delete button to remove rows. Individually they both work, but not together. I am not sure if I incorrectly bound the JS or that the JS somehow contradicts each other? For example, if I first click on the radio buttons and then the delete it does work, but not vice versa). This is because of the Javascript Index number, Is there a way to make this behaviour more consistent?
#(some of the code is taken from the answer on this question:
# https://stackoverflow.com/questions/53908266/r-shiny-remove-row-button-in-data-table)
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')
),
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()
})
}
)