I am trying to edit dynamically a data.frame using a Shiny App. The table describes a project's experiment structure and will thus include variable experimental factors, each with variable levels. For multi-level factors, I would like to allow value selection from a drop-down menu. For practical reasons, I want users to also be able to edit the table in Excel/any other table editor, so will be saving it as csv too. So the App should:
- Reload the local csv table,
- Create in the appropriate cells drop-down selections for factors using allowed values (factor levels),
- Also allow manual editing of values in other columns,
- When the "Save" button is clicked, the app should save the updated csv table then close However, while manual edits are saved as expected, as demonstrated in the example below, drop-down selection does not work.
require(shiny)
require(DT)
# Prepare mock data
wd <- getwd()
Factors <- c("Experiment", "Condition", "Replicate", "Genotype")
ExpMapNm <- "Experiment map"
ExpMapPath <- paste0(wd, "/", ExpMapNm, ".csv")
ExpMap <- data.frame(Experiment = "Exp1",
Sample = paste0("Sample", 1:20),
Condition = as.character(sapply(c("Treated", "Mock"), function(x) { rep(x, 10) })),
Genotype = as.character(sapply(c("KO", "WT"), function(x) { rep(x, 5) })),
Replicate = 1:5)
FactorsList <- setNames(lapply(Factors, function(x) { unique(ExpMap[[x]]) }), Factors)
#
# Users should be able to edit this table in two ways:
# - in Excel manually, hence why a local version is saved below and in server when closing the app
# - within the Shiny app
# In either case, the edited table will then be reloaded into r as a data.frame which will guide further data processing
if (!file.exists(ExpMapPath)) { write.csv(ExpMap, ExpMapPath, row.names = FALSE) }
ui <- shiny::fluidPage(shiny::titlePanel(ExpMapNm), # This is the name of the table
shiny::mainPanel(shiny::br(),
shiny::actionButton("saveBtn", "Save"),
DT::dataTableOutput("Data"),
verbatimTextOutput(Factors)
))
server <- function(input, output) {
Data <- read.csv(ExpMapPath)
for (Fact in Factors) {
if (length(FactorsList[[Fact]]) > 1) { # We only want to have a drop-down selection if a factor has more than one level.
# Thus, in this example we do not create a drop-down selection for factor Experiment.
lvls <- FactorsList[[Fact]] # Allowed values
for (i in 1:nrow(Data)) {
val <- Data[[Fact]][i]
dflt <- c(lvls[1], val)[(val %in% lvls)+1] # If the current value is not a valid level, revert to default (1st) level
# Otherwise keep current value, but still apply drop-down selection so choices can be changed
Data[[Fact]][i] <- as.character(selectInput(paste0(Fact, "_", i), NULL, lvls, dflt, width = "100px"))
}
} else { Data[[Fact]] <- FactorsList[[Fact]] }
}
output$Data <- DT::renderDataTable(
Data, escape = FALSE, selection = "none", server = FALSE,
editable = TRUE, # Non-drop-down fields can still be edited: this works
options = list(paging = TRUE, searching = TRUE, fixedColumns = TRUE, autoWidth = TRUE,
ordering = TRUE, dom = "Bfrtip"),
callback = JS("table.rows().every(function(i, tab, row) {
var $this = $(this.node());
$this.attr('id', this.data()[0]);
$this.addClass('shiny-input-container');
});
Shiny.unbindAll(table.table().node());
Shiny.bindAll(table.table().node());"),
class = "display")
shiny::observeEvent(input$Data_cell_edit, {
Data[input$Data_cell_edit$row, input$Data_cell_edit$col] <<- input$Data_cell_edit$value
})
shiny::observeEvent(input$saveBtn, {
# Hacky HTML cleanup
# I am not interested in getting cells with html formatting in my table, I just want the value!
# Moreover, this would clash with the code above when initiliazing the table and comparing values.
#print(Data$Condition[[1]])
for (Fact in Factors) {
if (length(FactorsList[[Fact]]) > 1) {
for (i in 1:nrow(Data)) {
val <- Data[[Fact]][i]
val <- gsub("^option value=\"[^\"]+\" selected>", "",
grep("^option value=\"[^\"]+\" selected>", unlist(strsplit(a, " *<|>[\n ]*<|> *$")), value = TRUE))
Data[[Fact]][i] <- val
}
}
}
# Check if I have managed to change from the default value:
print(Data$Condition[[1]])
# When testing, I change from "Treated" to "Mock" in the table, hit save, but it prints "Treated", and sure enough the table saved contains "Treated".
#
write.csv(Data, ExpMapPath, row.names = FALSE)
tstExpMap <<- Data # Another way to check table values
stopApp()
})
}
print(shiny::shinyApp(ui, server, options = list(launch.browser = TRUE)))
> sessionInfo()
R version 4.2.3 (2023-03-15 ucrt)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows Server x64 (build 20348)
Matrix products: default
locale:
[1] LC_COLLATE=English_United Kingdom.utf8 LC_CTYPE=English_United Kingdom.utf8 LC_MONETARY=English_United Kingdom.utf8
[4] LC_NUMERIC=C LC_TIME=English_United Kingdom.utf8
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] DT_0.26 shiny_1.7.4
loaded via a namespace (and not attached):
[1] Rcpp_1.0.9 rstudioapi_0.14 magrittr_2.0.3 xtable_1.8-4 R6_2.5.1 rlang_1.0.6 fastmap_1.1.0 tools_4.2.3
[9] aRmel_4.0.0.13 cli_3.5.0 jquerylib_0.1.4 htmltools_0.5.4 crosstalk_1.2.0 ellipsis_0.3.2 yaml_2.3.6 digest_0.6.31
[17] lifecycle_1.0.3 crayon_1.5.2 later_1.3.0 sass_0.4.4 htmlwidgets_1.6.0 promises_1.2.0.1 memoise_2.0.1 cachem_1.0.6
[25] mime_0.12 compiler_4.2.3 bslib_0.4.2 jsonlite_1.8.4 httpuv_1.6.7
I have looked at many similar posts on StackOverflow, such as https://github.com/ejbeaty/CellEdit/tree/master/js, DT: Dynamically change column values based on selectinput from another column in R shiny app and Edit datatable in shiny with dropdown selection (for DT v0.19), which I based this app on actually, but I must be missing something as it just isn't working for me. Unfortunately, I know nothing about JavaScript so it is very difficult for me to figure out what is missing.
DataTable
including the dropdowns (displayTbl
) and the other one holds the "raw" values (resultTbl
) - you did not follow this concept. It could be done in a single table hiding the data columns, but I guess the "two tables"-approach is simpler. – Iridissaeditable = TRUE
in thereplaceData
calls (just as I did withrownames = FALSE
. Regarding the different tables: I'm sure this can be further simplified - I was short on time. Maybe I'll revise the code in the near future. Cheers – Iridissa