父/子表在隐藏/重新展开后刷新



我的Shiny应用程序中有一个嵌套的DataTable。通过单击展开按钮可以查看子表。展开后,用户可以编辑一些列。编辑列后,如果用户想隐藏子表并移动到下一个父行,则第一个父行和子表中的数据都会刷新为原始值。

这里的问题是,我需要用户能够编辑子表,隐藏子表,并且这些编辑的值会被保存,这样当他们再次展开子表时,这些编辑过的值就会在那里。

我已经厌倦了使用observeEvent()isolate(),但它们似乎没有我想要的功能。

数据

structure(list(Market = c("ABILENE-SWEETWATER", "ALBANY-SCHENECTADY-TROY, NY"
), `Gross CPP` = c("$1.94", "$7.89"), `Gross CPM` = c("$1.02", 
"$0.82"), `Historical Composite Gross CPP (if applicable)` = c("$0", 
"$0"), `Historical Composite Gross CPM (if applicable)` = c("$0", 
"$0")), .Names = c("Market", "Gross CPP", "Gross CPM", "Historical Composite Gross CPP (if applicable)", 
"Historical Composite Gross CPM (if applicable)"), row.names = c(NA, 
-2L), class = "data.frame")

儿童

structure(list(Market = c("ABILENE-SWEETWATER", "ABILENE-SWEETWATER", 
"ABILENE-SWEETWATER", "ABILENE-SWEETWATER", "ABILENE-SWEETWATER", 
"ABILENE-SWEETWATER", "ABILENE-SWEETWATER", "ABILENE-SWEETWATER", 
"ABILENE-SWEETWATER", "ABILENE-SWEETWATER", "ABILENE-SWEETWATER", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY"), Daypart = c("Daytime", "Early Fringe", 
"Early Morning", "Early News", "Late Fringe", "Late News", "Prime Access", 
"Prime Time", "tv_2", "tv_3", "tv_cross_screen", "Daytime", "Early Fringe", 
"Early Morning", "Early News", "Late Fringe", "Late News", "Prime Access", 
"Prime Time", "tv_2", "tv_3", "tv_cross_screen"), `Mix (%)` = c(15, 
10, 15, 10, 5, 5, 10, 10, 0, 0, 0, 15, 10, 15, 10, 5, 5, 10, 
10, 0, 0, 0), `Spot:30 (%)` = c(15, 10, 15, 10, 5, 5, 10, 10, 
0, 0, 0, 15, 10, 15, 10, 5, 5, 10, 10, 0, 0, 0), `Spot:15 (%)` = c(0, 
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
), `Gross CPP ($)` = c(18, 18, 16, 23, 24, 40, 26, 44, 0, 0, 
0, 77, 71, 61, 78, 109, 145, 93, 213, 0, 0, 0), `Gross CPM ($)` = c(1.57, 
1.57, 1.39, 2, 2.09, 3.49, 2.27, 3.83, 23, 21, 13, 6.71, 6.19, 
5.32, 6.8, 9.5, 12.63, 8.1, 18.56, 23, 21, 13), `Historical Composite CPP ($)` = c(0, 
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
), `Historical Composite CPM ($)` = c(0, 0, 0, 0, 0, 0, 0, 0, 
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)), .Names = c("Market", 
"Daypart", "Mix (%)", "Spot:30 (%)", "Spot:15 (%)", "Gross CPP ($)", 
"Gross CPM ($)", "Historical Composite CPP ($)", "Historical Composite CPM ($)"
), class = "data.frame", row.names = c(NA, -22L))

代码

# The datatable callback
parentRows <- which(Dat[,1] != "")
callback_js = JS(
"function onUpdate(updatedCell, updatedRow, oldValue) {};",
sprintf("var parentRows = [%s];", toString(parentRows-1)),
sprintf("var j0 = %d;", colIdx),
"var nrows = table.rows().count();",
"for(var i=0; i < nrows; ++i){",
"  if(parentRows.indexOf(i) > -1){",
"    table.cell(i,j0).nodes().to$().css({cursor: 'pointer'});",
"  }else{",
"    table.cell(i,j0).nodes().to$().removeClass('details-control');",
"  }",
"}",
"",
"// make the table header of the nested table",
"var format = function(d, childId){",
"  if(d != null){",
"    var html = ",
"      '<table class="display compact hover" ' + ",
"      'style="padding-left: 30px;" id="' + childId + '"><thead><tr>';",
"    for(var key in d[d.length-1][0]){",
"      html += '<th>' + key + '</th>';",
"    }",
"    html += '</tr></thead><tfoot><tr>'",
"    for(var key in d[d.length-1][0]){",
"      html += '<th></th>';",
"    }",
"    return html + '</tr></tfoot></table>';",
"  } else {",
"    return '';",
"  }",
"};",
"",
"// row callback to style the rows of the child tables",
"var rowCallback = function(row, dat, displayNum, index){",
"  if($(row).hasClass('odd')){",
"    $(row).css('background-color', 'white');",
"    $(row).hover(function(){",
"      $(this).css('background-color', 'lightgreen');",
"    }, function() {",
"      $(this).css('background-color', 'white');",
"    });",
"  } else {",
"    $(row).css('background-color', 'white');",
"    $(row).hover(function(){",
"      $(this).css('background-color', 'lightblue');",
"    }, function() {",
"      $(this).css('background-color', 'white');",
"    });",
"  }",
"};",
"",
"// header callback to style the header of the child tables",
"var headerCallback = function(thead, data, start, end, display){",
"  $('th', thead).css({",
"    'border-top': '3px solid green',",
"    'color': 'black',",
"    'background-color': 'white'",
"  });",
"};",
"",
"// make the datatable",
"var format_datatable = function(d, childId, rowIdx){",
"  // footer callback to display the totals",
"  // and update the parent row",
"  var footerCallback = function(tfoot, data, start, end, display){",
"    $('th', tfoot).css('background-color', 'white');",
"    var api = this.api();",
"    api.columns().eq(0).each(function(index){",
"      if(index == 0) return $(api.column(index).footer()).html('Total');",
"      var coldata = api.column(index).data();",
"      var total = coldata",
"          .reduce(function(a, b){return parseFloat(a) + parseFloat(b)}, 0);",
"      if(index == 5 || index == 6 || index==7 || index==8) {",
"        $(api.column(index).footer()).html((total / coldata.count()).toFixed(2));",
"      } else {",
"        $(api.column(index).footer()).html(total);",
"     }",
"    })",
"    var col_share = api.column(2).data();",
"    var col_CPP = api.column(5).data();",
"    var col_CPM = api.column(6).data();",
"    var col_Historical_CPP = api.column(7).data();",
"    var col_Historical_CPM = api.column(8).data();",
"    var CPP = 0, CPM = 0, Historical_CPP = 0, Historical_CPM = 0;",
"    for(var i = 0; i < col_share.length; i++){",
"      CPP += (parseFloat(col_share[i])*parseFloat(col_CPP[i]).toFixed(2));",
"      CPM += (parseFloat(col_share[i])*parseFloat(col_CPM[i]).toFixed(2));",
"      Historical_CPP += (parseFloat(col_share[i])*parseFloat(col_Historical_CPP[i]).toFixed(2));",
"      Historical_CPM += (parseFloat(col_share[i])*parseFloat(col_Historical_CPM[i]).toFixed(2));",
"    }",
"    table.cell(rowIdx, j0+2).data((CPP/100).toFixed(2));",
"    table.cell(rowIdx, j0+3).data((CPM/100).toFixed(2));",
"    table.cell(rowIdx, j0+4).data((Historical_CPP/100).toFixed(2));",
"    table.cell(rowIdx, j0+5).data((Historical_CPM/100).toFixed(2));",
"  }",
"  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;",
"  if (Object.keys(d[n][0]).indexOf('_details') === -1) {",
"    var subtable = $(id).DataTable({",
"                 'data': dataset,",
"                 'autoWidth': true,",
"                 'deferRender': true,",
"                 'info': false,",
"                 'lengthChange': false,",
"                 'ordering': d[n].length > 1,",
"                 'order': [],",
"                 'paging': true,",
"                 'scrollX': false,",
"                 'scrollY': false,",
"                 'searching': false,",
"                 'sortClasses': false,",
"                 'pageLength': 50,",
"                 'rowCallback': rowCallback,",
"                 'headerCallback': headerCallback,",
"                 'footerCallback': footerCallback,",
"                 'columnDefs': [{targets: '_all', className: 'dt-center'}]",
"               });",
"  } else {",
"    var subtable = $(id).DataTable({",
"            'data': dataset,",
"            'autoWidth': true,",
"            'deferRender': true,",
"            'info': false,",
"            'lengthChange': false,",
"            'ordering': d[n].length > 1,",
"            'order': [],",
"            'paging': true,",
"            'scrollX': false,",
"            'scrollY': false,",
"            'searching': false,",
"            'sortClasses': false,",
"            'pageLength': 50,",
"            'rowCallback': rowCallback,",
"            'headerCallback': headerCallback,",
"            'footerCallback': footerCallback,",
"            'columnDefs': [",
"              {targets: -1, visible: false},",
"              {targets: 0, orderable: false, className: 'details-control'},",
"              {targets: '_all', className: 'dt-center'}",
"             ]",
"          }).column(0).nodes().to$().css({cursor: 'pointer'});",
"  }",
"  subtable.MakeCellsEditable({",
"    onUpdate: onUpdate,",
"    inputCss: 'my-input-class',",
"    columns: [2, 7, 8],",
"    confirmationButton: {",
"      confirmCss: 'my-confirm-class',",
"      cancelCss: 'my-cancel-class'",
"    }",
"  });",
"};",
"",
"// display the child table on click",
"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('&oplus;');",
"  } else {",
"    var childId = tblId + '-child-' + rowIdx;",
"    row.child(format(row.data(), childId)).show();",
"    td.html('&CircleMinus;');",
"    format_datatable(row.data(), childId, rowIdx);",
"  }",
"});")
# Module to create the nested structure of the table
NestedData <- function(dat, children){
stopifnot(length(children) == nrow(dat))
g <- function(d){
if(is.data.frame(d)){
purrr::transpose(d)
}else{
purrr::transpose(NestedData(d[[1]], children = d$children))
}
}
subdats <- lapply(children, g)
oplus <- sapply(subdats, function(x) if(length(x)) "&oplus;" else "")
cbind(" " = oplus, dat, "_details" = I(subdats), stringsAsFactors = FALSE)
}
# Shiny App
# Bind the market level and mix breakout data together for the final table
market_mix_table <- reactive({
markets <- market_costings_gross_net()
mix_breakout <- mix_breakout_digital_elements()
# Make the dataframe
# This must be met length(children) == nrow(dat)
Dat <- NestedData(
dat = markets,
children = split(mix_breakout, mix_breakout$Market)
)
return(Dat)
})
# Render the table
output$daypartTable <- DT::renderDataTable({
Server = FALSE
# Whether to show row names (set TRUE or FALSE)
rowNames <- FALSE
colIdx <- as.integer(rowNames)
# The data
Dat <- market_mix_table()
# Table
table <- DT::datatable(
Dat,
callback = callback_js,
rownames = rowNames,
escape = -colIdx-1,
style = "bootstrap4",
options = list(
lengthMenu = list(
c(-1, 10, 20),
c("All", 10, 20)
),
columnDefs = list(
list(width = '30px'),
list(width = '100px', targets = 1),
list(visible = FALSE, targets = ncol(Dat)-1+colIdx),
list(orderable = FALSE, className = 'details-control', targets = colIdx),
list(className = "dt-center", targets = "_all")
)
)
)
# Some faancy Java magic
path <- getwd()
# Call the html tools deps (js & css files in this directory)
dep <- htmltools::htmlDependency(
"CellEdit", "1.0.19", path,
script = "dataTables.cellEdit.js", stylesheet = "dataTables.cellEdit.css")
table$dependencies <- c(table$dependencies, list(dep))
# server = FALSE
return(table)
})
# Keep the edited cell values
observeEvent(input$daypartTable_cell_edit, {
expdf[input$daypartTable_cell_edit$row,input$daypartTable_cell_edit$col] <<- input$daypartTable_cell_edit$value
})

非常感谢您的帮助!!这是迄今为止我开发过的最先进的Shiny应用程序,我可能对此有点不知所措。

似乎是这样工作的。我修改了JS回调的最后一部分:

"// display the child table on click",
"var children = [];", # array to store the id's of the already created child tables
"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('&oplus;');",
"  } else {",
"    var childId = tblId + '-child-' + rowIdx;",
"    if(children.indexOf(childId) === -1){", # this child table has not been created yet
"      children.push(childId);",
"      row.child(format(row.data(), childId)).show();",
"      td.html('&CircleMinus;');",
"      format_datatable(row.data(), childId, rowIdx);",
"    }else{",
"      row.child(true);",
"      td.html('&CircleMinus;');",
"    }",
"  }",
"});")

以前,每次显示子表时,它都会重新创建子表

library(DT)
df_child <- structure(
list(
Daypart = c("Daytime", "Early Fringe", "Early Morning", "Early News", "Late Fringe", "Late News", "Prime Access", "Prime Time"), 
`Share (%)` = c(15, 15, 15, 15, 10, 10, 10, 10), 
`Spot:30 (%)` = c(0, 0, 0, 0, 0, 0, 0, 0), 
`Spot:15 (%)` = c(0, 0, 0, 0, 0, 0, 0, 0), 
`Gross CPP ($)` = c(0, 0, 0, 0, 0, 0, 0, 0),
`Gross CPM ($)` = c(0, 0, 0, 0, 0, 0, 0, 0)
), 
.Names = c("Daypart", "Share (%)", "Spot:30 (%)", "Spot:15 (%)", "Gross CPP ($)", "Gross CPM ($)"), 
row.names = c(NA, -8L), class = "data.frame")
df_parent <- structure(
list(
Market = c("ABILENE-SWEETWATER", "ALBANY-SCHENECTADY-TROY, NY"), 
`Gross CPP` = c("$0", "$0"), 
`Gross CPM` = c("$0", "$0"), 
`Historical Composite Gross CPP (if applicable)` = c("$0", "$0"), 
`Historical Composite Gross CPM (if applicable)` = c("$0", "$0")), 
.Names = c("Market", "Gross CPP", "Gross CPM", "Historical Composite Gross CPP (if applicable)", "Historical Composite Gross CPM (if applicable)"), 
row.names = c(NA, -2L), class = "data.frame")
# function to make the required dataframe
NestedData <- function(dat, children){
stopifnot(length(children) == nrow(dat))
g <- function(d){
if(is.data.frame(d)){
purrr::transpose(d)
}else{
purrr::transpose(NestedData(d[[1]], children = d$children))
}
}
subdats <- lapply(children, g)
oplus <- sapply(subdats, function(x) if(length(x)) "&oplus;" else "")
cbind(" " = oplus, dat, "_details" = I(subdats), stringsAsFactors = FALSE)
}
# make the required dataframe
# one must have: length(children) == nrow(dat)
Dat <- NestedData(
dat = df_parent, 
children = list(df_child, df_child)
)
## whether to show row names (set TRUE or FALSE)
rowNames <- FALSE
colIdx <- as.integer(rowNames)
## make the callback
parentRows <- which(Dat[,1] != "")
callback = JS(
"function onUpdate(updatedCell, updatedRow, oldValue) {};",
"table.MakeCellsEditable({",
"  onUpdate: onUpdate,",
"  inputCss: 'my-input-class',",
"  confirmationButton: {",
"    confirmCss: 'my-confirm-class',",
"    cancelCss: 'my-cancel-class'",
"  }",
"});",
sprintf("var parentRows = [%s];", toString(parentRows-1)),
sprintf("var j0 = %d;", colIdx),
"var nrows = table.rows().count();",
"for(var i=0; i < nrows; ++i){",
"  if(parentRows.indexOf(i) > -1){",
"    table.cell(i,j0).nodes().to$().css({cursor: 'pointer'});",
"  }else{",
"    table.cell(i,j0).nodes().to$().removeClass('details-control');",
"  }",
"}",
"",
"// make the table header of the nested table",
"var format = function(d, childId){",
"  if(d != null){",
"    var html = ", 
"      '<table class="display compact hover" ' + ",
"      'style="padding-left: 30px;" id="' + childId + '"><thead><tr>';",
"    for(var key in d[d.length-1][0]){",
"      html += '<th>' + key + '</th>';",
"    }",
"    html += '</tr></thead><tfoot><tr>'",
"    for(var key in d[d.length-1][0]){",
"      html += '<th></th>';",
"    }",
"    return html + '</tr></tfoot></table>';",
"  } else {",
"    return '';",
"  }",
"};",
"",
"// row callback to style the rows of the 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 the header of the 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 format_datatable = function(d, childId, rowIdx){",
"  // footer callback to display the totals",
"  // and update the parent row",
"  var footerCallback = function(tfoot, data, start, end, display){",
"    $('th', tfoot).css('background-color', '#fed8b1');",
"    var api = this.api();",
"    api.columns().eq(0).each(function(index){",
"      if(index == 0) return $(api.column(index).footer()).html('Total');",
"      var coldata = api.column(index).data();",
"      var total = coldata", 
"          .reduce(function(a, b){return parseFloat(a) + parseFloat(b)}, 0);",
"      $(api.column(index).footer()).html(total);",
"    })",
"    var col_share = api.column(1).data();",
"    var col_CPP = api.column(4).data();",
"    var col_CPM = api.column(5).data();",
"    var CPP = 0, CPM = 0;",
"    for(var i = 0; i < col_share.length; i++){",
"      CPP += parseFloat(col_share[i])*parseFloat(col_CPP[i]);",
"      CPM += parseFloat(col_share[i])*parseFloat(col_CPM[i]);",
"    }",
"    table.cell(rowIdx, j0+2).data('$' + (CPP/100));",
"    table.cell(rowIdx, j0+3).data('$' + (CPM/100)).draw();",
"  }",
"  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;",
"  if (Object.keys(d[n][0]).indexOf('_details') === -1) {",
"    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,",
"                 'footerCallback': footerCallback,",
"                 'columnDefs': [{targets: '_all', className: 'dt-center'}]",
"               });",
"  } else {",
"    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,",
"            'footerCallback': footerCallback,",
"            'columnDefs': [", 
"              {targets: -1, visible: false},", 
"              {targets: 0, orderable: false, className: 'details-control'},", 
"              {targets: '_all', className: 'dt-center'}",
"             ]",
"          }).column(0).nodes().to$().css({cursor: 'pointer'});",
"  }",
"  subtable.MakeCellsEditable({",
"    onUpdate: onUpdate,",
"    inputCss: 'my-input-class',",
"    confirmationButton: {",
"      confirmCss: 'my-confirm-class',",
"      cancelCss: 'my-cancel-class'",
"    }",
"  });",
"};",
"",
"// display the child table on click",
"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('&oplus;');",
"  } else {",
"    var childId = tblId + '-child-' + rowIdx;",
"    if(children.indexOf(childId) === -1){",
"      children.push(childId);",
"      row.child(format(row.data(), childId)).show();",
"      td.html('&CircleMinus;');",
"      format_datatable(row.data(), childId, rowIdx);",
"    }else{",
"      row.child(true);",
"      td.html('&CircleMinus;');",
"    }",
"  }",
"});")
## the datatable
dtable <- datatable(
Dat, callback = callback, rownames = rowNames, escape = -colIdx-1,
options = list(
columnDefs = list(
list(visible = FALSE, targets = ncol(Dat)-1+colIdx),
list(orderable = FALSE, className = 'details-control', targets = colIdx),
list(className = "dt-center", targets = "_all")
)
)
)
path <- "~/Work/R/DT" # folder containing the files dataTables.cellEdit.js
# and dataTables.cellEdit.css
dep <- htmltools::htmlDependency(
"CellEdit", "1.0.19", path, 
script = "dataTables.cellEdit.js", stylesheet = "dataTables.cellEdit.css")
dtable$dependencies <- c(dtable$dependencies, list(dep))
dtable

最新更新