I 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 Extracting user input values from radio buttons in Shiny DT into a dataframe or list) and having nested rows in a child/parent table (see https://stackoverflow.com/a/56599838/10624798 and many other places). 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? Either way, I am not able to access the user input anymore (just a null returns if I look at the structure of the input$xxx). I have included a small example (although still quite lengthy) of what I mean.
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() }) } )
EDIT (ADDITIONAL QUESTION)
An answer was given that solved my original question for the MWE. However, diving into it a bit more and by adding extra types of buttons to the table the radio buttons still break (for example by adding a delete button, if I first click on the radio buttons and then the delete it does work, but not vice versa). Is there a way to make this behaviour more consistant?
(some of the code is taken from the answer on this question: 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'), #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 } )
One more thing I tried (but did not work)
I now also tried the solution given here: https://stefanengineering.com/2019/07/06/delete-rows-from-shiny-dt-datatable/, but that gives the exact same problem.
Advertisement
Answer
You assign the wrong id
element in your JavaScript callback, because in your Table, the first column is actually the &oplus
column, but you want the second column.
Thus change this
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'); });
to this:
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'); });