I have a shiny app that utilizes a data table with drop down menus to control the updates of select columns. When I modularize the shiny app, I am no longer able to capture the results of the updated table. The approach I am following for the drop down menus is based on a response I received to an earlier question (#ismirsehregal).
< https://stackoverflow.com/questions/69959720/edit-datatable-in-shiny-with-dropdown-selection-for-dt-v0-19>
Below I’ve provided two versions of my app, nonmod2_app and mod2_app. The first is without modules and works as desired. The second is a modularized version and I am getting NULL in the output.
When the User runs the app they are presented with a Load Data button which loads the cars data as cars_df and creates a new table I call cars_meta with three values. It then creates a list called cars_object to which cars_df and cars_meta are added. This is then made a reactive value, cars_reactive.
The User is then presented with a an editable data table (initTbl) of the reactive cars_reactive$cars_meta for which they can update the values of two fields via drop down menus. When the User is done making updates and selects the Commit button, the results of the selections are saved as cars_reactive$cars_meta. I display the updated cars_reactive$cars_meta as verbatimTextOutput so one can see how it was updated.
In both examples, I update the first class value from “numeric” to “character” and select Commit. In the nonmod2_app version the results table holds the original values and reflects the one I updated.
In the mod2_app version, I get NULL for all values of the two selectable fields.
I suspect it has something to do with namespace, but am at a loss to figure out what is missing.
Here is the ui and server code for the non modularized version.
#------- LIBRARIES ---------------------
library(dplyr)
library(tidyselect)
library(stringr)
library(purrr)
library(shinyjs)
library(DT)
# ------ UI ------------------
shiny_ui <- function() {
fluidPage(
actionButton("new_data", "Load Data"),
br(),
DT::dataTableOutput("main_table"),
br(),
actionButton("commit_meta", "Commit"),
br(),
verbatimTextOutput("cars_meta")
)
}
# -------- SERVER ---------------
shiny_server <- function(input, output, session) {
v <- reactiveValues()
#place holders
selectInputIDclass <- "class"
selectInputIDusage <- "usage"
observeEvent(input$new_data, once = TRUE, {
cars_df <- mtcars
#simulate creating meta table
cars_meta <- dplyr::tibble(variable = names(cars_df), class = sapply(cars_df, class), usage = c("val1", "val2", "val3","val1", "val2", "val3","val1", "val2", "val3","val1", "val2"))
cars_meta$class <- factor(cars_meta$class, c("numeric", "character", "factor"))
cars_meta$usage <- factor(cars_meta$usage, c("val1", "val2", "val3"))
#simulate creating the cars_object
cars_object <- list()
cars_object$cars_df <- cars_df
cars_object$cars_meta <- cars_meta
#make initTbl
selectInputIDclass <<- paste0("sel_class", 1:nrow(cars_object$cars_meta))
selectInputIDusage <<- paste0("sel_usage", 1:nrow(cars_object$cars_meta))
v$initTbl <- dplyr::tibble(
variable = cars_object$cars_meta$variable,
class = sapply(selectInputIDclass, function(x){as.character(selectInput(inputId = x, label = "", choices = c("character","numeric", "factor"),
selected = cars_object$cars_meta$class[which(selectInputIDclass == x)]))}),
usage = sapply(selectInputIDusage, function(x){as.character(selectInput(inputId = x, label = "", choices = c("val1", "val2", "val3"),
selected = cars_object$cars_meta$usage[which(selectInputIDusage == x)]))})
)
v$cars_reactive <- reactive({
cars_object
})
})
displayTbl <- reactive({
dplyr::tibble(
variable = v$cars_reactive()$cars_meta$variable,
class = sapply(selectInputIDclass, function(x){as.character(selectInput(inputId = x, label = "", choices = c("numeric", "character", "factor"), selected = input[[x]]))}),
usage = sapply(selectInputIDusage, function(x){as.character(selectInput(inputId = x, label = "", choices = c("val1", "val2", "val3"), selected = input[[x]]))})
)
})
resultTbl <- reactive({
dplyr::tibble(
variable = v$cars_reactive()$cars_meta$variable,
class = sapply(selectInputIDclass, function(x){input[[x]]}),
usage = sapply(selectInputIDusage, function(x){input[[x]]})
)
})
output$main_table = DT::renderDataTable({
req(isTruthy(input$new_data))
DT::datatable(
v$initTbl, escape = FALSE, selection = 'none', rownames = FALSE,
options = list(paging = FALSE, ordering = FALSE, scrollx = TRUE, dom = "t",
preDrawCallback = DT::JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = DT::JS('function() { Shiny.bindAll(this.api().table().node()); } ')
)
)
}, server = TRUE)
main_table_proxy <- DT::dataTableProxy(outputId = "main_table", session = session)
observeEvent({sapply(selectInputIDclass, function(x){input[[x]]})}, {
replaceData(proxy = main_table_proxy, data = displayTbl(), rownames = FALSE)
}, ignoreInit = TRUE)
observeEvent({sapply(selectInputIDusage, function(x){input[[x]]})}, {
replaceData(proxy = main_table_proxy, data = displayTbl(), rownames = FALSE)
}, ignoreInit = TRUE)
observeEvent(input$commit_meta, {
object = v$cars_reactive()
table = resultTbl()
object$cars_meta <- table
v$cars_reactive <- reactive({object})
})
output$cars_meta <- renderPrint({
req (input$commit_meta > 0)
isolate({v$cars_reactive()$cars_meta})
})
}
# ------- APP ----------
nonmod2_app <- function(...) {
app <- shiny::shinyApp(
ui = shiny_ui,
server = shiny_server
)
shiny::runApp(app, ...)
}
Here is the code for the modularized version.
#------- LIBRARIES ---------------------
library(dplyr)
library(tidyselect)
library(stringr)
library(purrr)
library(shinyjs)
library(DT)
# ------ UI MODULE ------------------
mod_ui <- function(id) {
fluidPage(
actionButton(NS(id,"new_data"), "Load Data"),
br(),
DT::dataTableOutput(NS(id, 'main_table')),
br(),
actionButton(NS(id, "commit_meta"), "Commit"),
br(),
verbatimTextOutput(NS(id, "cars_meta"))
)
}
# -------- SERVER MODULE ---------------
mod_server <- function(id) {
shiny::moduleServer(id, function(input, output,session){
v <- reactiveValues()
#place holders
selectInputIDclass <- "class"
selectInputIDusage <- "usage"
observeEvent(input$new_data, once = TRUE, {
cars_df <- mtcars
#simulate creating meta table
cars_meta <- dplyr::tibble(variable = names(cars_df), class = sapply(cars_df, class), usage = c("val1", "val2", "val3","val1", "val2", "val3","val1", "val2", "val3","val1", "val2"))
cars_meta$class <- factor(cars_meta$class, c("numeric", "character", "factor"))
cars_meta$usage <- factor(cars_meta$usage, c("val1", "val2", "val3"))
#simulate creating the cars_object
cars_object <- list()
cars_object$cars_df <- cars_df
cars_object$cars_meta <- cars_meta
#make initTbl
selectInputIDclass <<- paste0("sel_class", 1:nrow(cars_object$cars_meta))
selectInputIDusage <<- paste0("sel_usage", 1:nrow(cars_object$cars_meta))
v$initTbl <- dplyr::tibble(
variable = cars_object$cars_meta$variable,
class = sapply(selectInputIDclass, function(x){as.character(selectInput(inputId = x, label = "", choices = c("character","numeric", "factor"),
selected = cars_object$cars_meta$class[which(selectInputIDclass == x)]))}),
usage = sapply(selectInputIDusage, function(x){as.character(selectInput(inputId = x, label = "", choices = c("val1", "val2", "val3"),
selected = cars_object$cars_meta$usage[which(selectInputIDusage == x)]))})
)
v$cars_reactive <- reactive({
cars_object
})
})
displayTbl <- reactive({
dplyr::tibble(
variable = v$cars_reactive()$cars_meta$variable,
class = sapply(selectInputIDclass, function(x){as.character(selectInput(inputId = x, label = "", choices = c("numeric", "character", "factor"), selected = input[[x]]))}),
usage = sapply(selectInputIDusage, function(x){as.character(selectInput(inputId = x, label = "", choices = c("val1", "val2", "val3"), selected = input[[x]]))})
)
})
resultTbl <- reactive({
dplyr::tibble(
variable = v$cars_reactive()$cars_meta$variable,
class = sapply(selectInputIDclass, function(x){input[[x]]}),
usage = sapply(selectInputIDusage, function(x){input[[x]]})
)
})
output$main_table = DT::renderDataTable({
req(isTruthy(input$new_data))
DT::datatable(
v$initTbl, escape = FALSE, selection = 'none', rownames = FALSE,
options = list(paging = FALSE, ordering = FALSE, scrollx = TRUE, dom = "t",
preDrawCallback = DT::JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = DT::JS('function() { Shiny.bindAll(this.api().table().node()); } ')
)
)
}, server = TRUE)
main_table_proxy <- DT::dataTableProxy(outputId = "main_table", session = session)
observeEvent({sapply(selectInputIDclass, function(x){input[[x]]})}, {
replaceData(proxy = main_table_proxy, data = displayTbl(), rownames = FALSE)
}, ignoreInit = TRUE)
observeEvent({sapply(selectInputIDusage, function(x){input[[x]]})}, {
replaceData(proxy = main_table_proxy, data = displayTbl(), rownames = FALSE)
}, ignoreInit = TRUE)
observeEvent(input$commit_meta, {
object = v$cars_reactive()
table = resultTbl()
object$cars_meta <- table
v$cars_reactive <- reactive({object})
})
output$cars_meta <- renderPrint({
req (input$commit_meta > 0)
isolate({v$cars_reactive()$cars_meta})
})
})
}
# ------- UI SERVER APP ----------
shiny_ui <- function() {
fluidPage(
mod_ui("data")
)
}
shiny_server <- function(input, output, session) {
sv <- mod_server("data")
}
mod2_app <- function(...) {
app <- shiny::shinyApp(
ui = shiny_ui,
server = shiny_server
)
shiny::runApp(app, ...)
}
Your help is much appreciated.
Try this in your new mod_server, it works for me.
ns <- session$ns
v$initTbl <- dplyr::tibble(
variable = cars_object$cars_meta$variable,
class = sapply(selectInputIDclass, function(x){as.character(selectInput(inputId = ns(x), label = "", choices = c("character","numeric", "factor"),
selected = cars_object$cars_meta$class[which(selectInputIDclass == x)]))}),
usage = sapply(selectInputIDusage, function(x){as.character(selectInput(inputId = ns(x), label = "", choices = c("val1", "val2", "val3"),
selected = cars_object$cars_meta$usage[which(selectInputIDusage == x)]))})
)
Related
I'm trying to update row-wise filter in datatable basis on the inputs we receive from user on every row, so that only relevant values in sub-sequent inputs can be selected.
I have tried to replicate my scenario using below code, where in if User selects "setosa" as "spieces_selector" hence only "1-50" values should appear in "New_Data_selector". Similarly if a User selects "versicolor" in 2nd row hence for 2nd row "New_Data_selector" should have the values from "51-100".
Would appreciate your help on this.
library(shiny)
library(DT)
iris$New_Data <- c(1:150)
ui <- fluidPage(
title = 'Selectinput column in a table',
h3("Source:", tags$a("Yihui Xie", href = "https://yihui.shinyapps.io/DT-radio/")),
numericInput('num', "enter a number", value = 5, min = 1, max = 10, step = 1),
DT::dataTableOutput('foo'),
verbatimTextOutput('sel'),
actionButton(
"saveBtn",
"Submit Request",
style = "color: #fff; background-color: #282364;
border-color: #2e6da4",
class = "btn btn-primary"
)
)
server <- function(input, output, session) {
data <- reactive({
df <- head(iris, input$num)
for (i in 1:nrow(df)) {
df$species_selector[i] <- as.character(selectInput(paste0("sel1", i),
"",
choices = unique(iris$Species),
width = "100px"))
df$New_Data_selector[i] <- as.character(selectInput(paste0("sel2", i),
"",
choices = unique(iris$New_Data),
width = "100px"))
}
df
})
output$foo = DT::renderDataTable(
data(), escape = FALSE, selection = 'none', server = FALSE,
options = list(dom = 't', paging = FALSE, ordering = FALSE),
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());")
)
output$sel = renderPrint({
str(sapply(1:nrow(data()), function(i) input[[paste0("sel", i)]]))
})
observeEvent(input$saveBtn, {
Test_Data <- sapply(1:nrow(data()), function(i) input[[paste0("sel", i)]])
Test_Data <- as.data.frame(Test_Data)
print(Test_Data)})
}
shinyApp(ui, server)
The following works (based on my earlier answer) - but it's pretty slow. Will need to investigate further.
library(DT)
library(shiny)
library(datasets)
library(data.table)
myIris <- copy(iris)
setDT(myIris)
myIris[, Index := seq_len(.N)]
selectInputIDs_A <- paste0("sel_A", myIris$Index)
selectInputIDs_B <- paste0("sel_B", myIris$Index)
myIris[, selectInputs_A := sapply(selectInputIDs_A, function(x){as.character(selectInput(inputId = x, label = "", choices = unique(myIris$Species), selected = "setosa"))})]
myIris[, selectInputs_B := sapply(selectInputIDs_B, function(x){as.character(selectInput(inputId = x, label = "", choices = unique(myIris[Species == "setosa"]$Index), selected = "setosa"))})]
initTbl <- copy(myIris)
ui <- fluidPage(
DT::dataTableOutput(outputId = 'my_table')
)
server <- function(input, output, session) {
displayTbl <- reactive({
myIris[, selectInputs_A := sapply(selectInputIDs_A, function(x){as.character(selectInput(inputId = x, label = "", choices = unique(Species), selected = input[[x]]))}),]
myIris[, selectInputs_B := sapply(seq_along(selectInputs_B), function(x){as.character(selectInput(inputId = selectInputIDs_B[x], label = "", choices = unique(myIris[Species == input[[selectInputIDs_A[x]]]]$Index), selected = input[[selectInputIDs_A[x]]]))})]
})
output$my_table = DT::renderDataTable({
DT::datatable(
initTbl, escape = FALSE, selection = 'none', rownames = FALSE,
options = list(paging = FALSE, ordering = FALSE, scrollx = TRUE, dom = "t",
preDrawCallback = JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = JS('function() { Shiny.bindAll(this.api().table().node()); } ')
)
)
}, server = TRUE)
my_table_proxy <- dataTableProxy(outputId = "my_table", session = session)
observeEvent({sapply(selectInputIDs_A, function(x){input[[x]]})}, {
replaceData(proxy = my_table_proxy, data = displayTbl(), rownames = FALSE) # must repeat rownames = FALSE see ?replaceData and ?dataTableAjax
}, ignoreInit = TRUE)
}
shinyApp(ui = ui, server = server)
I have a datatable in which I've added checkboxes for my users to select various options. Unfortunately, the only inputs that shiny seems to see are ones that have been displayed in the table. So if I have multiple pages, I'm only able to see the first 10 inputs.
In the example below, I've printed all of the inputs that I can see registered above the datatable object. At the moment, I only see the first 10 inputs (A - J). I'd like to be able to see all 26 when the table first loads (without having to toggle through the pages).
In my actual application, I have multiple columns of checkboxes, so row selection wouldn't be sufficient. Any tips or suggestions on how to register all 26 inputs at once?
library(shiny)
library(DT)
shinyInput <- function (FUN, id_base, suffix, label = "", ...)
{
inputId <- paste0(id_base, suffix)
args <- list(...)
args <- c(list(label = label), args)
args <- lapply(args, function(a) rep(a, length.out = length(inputId)))
rv <- character(length(inputId))
for (i in seq_along(rv)) {
this_arg <- lapply(args, `[`, i)
ctrl <- do.call(FUN, c(list(inputId = inputId[i]), this_arg))
rv[i] <- as.character(ctrl)
}
rv
}
X <- data.frame(id = LETTERS,
selected = sample(c(TRUE, FALSE),
size = length(LETTERS),
replace = TRUE))
X$IsSelected <-
shinyInput(
shiny::checkboxInput,
id_base = "new_input_",
suffix = X$id,
value = X$selected
)
shinyApp(
ui = fluidPage(
verbatimTextOutput("value_check"),
textOutput("input_a_value"),
DT::dataTableOutput("dt")
),
server = shinyServer(function(input, output, session){
Data <- reactiveValues(
X = X
)
output$value_check <-
renderPrint({
sort(names(input))
})
output$dt <-
DT::renderDataTable({
DT::datatable(X,
selection = "none",
escape = FALSE,
filter = "top",
#rownames = FALSE,
class = "compact cell-border",
options = list(preDrawCallback = JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
})
})
)
ADDENDUM
This next example is a bit more complex, but illustrates a bit more of the motivation for the question. It seems the biggest issue is that I would like to utilize buttons such as "select all." Additionally, I'm not processing any actions immediately when a box is interacted with. Instead, the user makes their selections, and the selections are not saved until the "Save Selections" button is clicked.
What is happening is I click on the "Select All" button, and it checks all of the boxes for inputs that have been drawn already. If I've only viewed the first page of the table, it updates only those inputs, and none of the inputs on the next few pages. This is really the behavior I need to change.
# Set up environment ------------------------------------------------
library(shiny)
library(DT)
library(magrittr)
# Example of data coming from the database. -------------------------
set.seed(pi^2)
SourceData <-
data.frame(sample_id = 1:25,
is_selected = sample(c(TRUE, FALSE), 25, replace = TRUE))
# Support Functions -------------------------------------------------
# These would exist, for example, in an internal package
shinyInput <- function (FUN, id_base, suffix, label = "", ...)
{
inputId <- paste0(id_base, suffix)
args <- list(...)
args <- c(list(label = label), args)
args <- lapply(args, function(a) rep(a, length.out = length(inputId)))
rv <- character(length(inputId))
for (i in seq_along(rv)) {
this_arg <- lapply(args, `[`, i)
ctrl <- do.call(FUN, c(list(inputId = inputId[i]), this_arg))
rv[i] <- as.character(ctrl)
}
rv
}
prepareDataForDisplay <- function(Data){
Data$is_selected <-
shinyInput(shiny::checkboxInput,
id_base = "is_selected_",
suffix = Data$sample_id,
value = Data$is_selected)
Data
}
# User Interface ----------------------------------------------------
ui <-
fluidPage(
verbatimTextOutput("value_check"),
actionButton(inputId = "btn_saveSelection",
label = "Save Selection"),
actionButton(inputId = "btn_selectAll",
label = "Select All"),
actionButton(inputId = "btn_unselectAll",
label = "Unselect All"),
actionButton(inputId = "btn_restoreDefault",
label = "Restore Default (select odd only)"),
DT::dataTableOutput("dt")
)
# Server ------------------------------------------------------------
server <-
shinyServer(function(input, output, session){
# Event Observers -----------------------------------------------
observeEvent(
input$btn_selectAll,
{
check_input <- names(input)[grepl("is_selected_", names(input))]
lapply(check_input,
function(ci){
updateCheckboxInput(session = session,
inputId = ci,
value = TRUE)
})
}
)
observeEvent(
input$btn_unselectAll,
{
check_input <- names(input)[grepl("is_selected_", names(input))]
lapply(check_input,
function(ci){
updateCheckboxInput(session = session,
inputId = ci,
value = FALSE)
})
}
)
observeEvent(
input$btn_restoreDefault,
{
check_input <- names(input)[grepl("is_selected_", names(input))]
lapply(check_input,
function(ci){
id <- as.numeric(sub("is_selected_", "", ci))
updateCheckboxInput(session = session,
inputId = ci,
value = id %% 2 == 1)
})
}
)
observeEvent(
input$btn_saveSelection,
{
check_input <- names(input)[grepl("is_selected_", names(input))]
id <- as.numeric(sub("is_selected_", "", check_input))
for (i in seq_along(check_input)){
SourceData$is_selected[SourceData$sample_id == id[i]] <-
input[[check_input[i]]]
}
# At this point, I would also save changes to the remote database.
DT::replaceData(proxy = dt_proxy,
data = prepareDataForDisplay(SourceData))
}
)
# Output elements -----------------------------------------------
output$value_check <-
renderPrint({
sort(names(input))
})
output$dt <-
DT::renderDataTable({
SourceData %>%
prepareDataForDisplay() %>%
DT::datatable(selection = "none",
escape = FALSE,
filter = "top",
class = "compact cell-border",
options = list(preDrawCallback = JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
})
dt_proxy <- DT::dataTableProxy("dt")
})
# Run the application -----------------------------------------------
shinyApp(
ui = ui,
server = server
)
Here is a workaround based on your addendum (not sure if you need the changes regarding btn_restoreDefault and btn_saveSelection), but the general procedure should be clear:
# Set up environment ------------------------------------------------
library(shiny)
library(DT)
library(magrittr)
# Example of data coming from the database. -------------------------
set.seed(pi^2)
SourceData <-
data.frame(sample_id = 1:25,
is_selected = sample(c(TRUE, FALSE), 25, replace = TRUE))
# Support Functions -------------------------------------------------
# These would exist, for example, in an internal package
shinyInput <- function (FUN, id_base, suffix, label = "", ...)
{
inputId <- paste0(id_base, suffix)
args <- list(...)
args <- c(list(label = label), args)
args <- lapply(args, function(a) rep(a, length.out = length(inputId)))
rv <- character(length(inputId))
for (i in seq_along(rv)) {
this_arg <- lapply(args, `[`, i)
ctrl <- do.call(FUN, c(list(inputId = inputId[i]), this_arg))
rv[i] <- as.character(ctrl)
}
rv
}
prepareDataForDisplay <- function(Data){
Data$is_selected <-
shinyInput(shiny::checkboxInput,
id_base = "is_selected_",
suffix = Data$sample_id,
value = Data$is_selected)
Data
}
# User Interface ----------------------------------------------------
ui <-
fluidPage(
verbatimTextOutput("value_check"),
actionButton(inputId = "btn_saveSelection",
label = "Save Selection"),
actionButton(inputId = "btn_selectAll",
label = "Select All"),
actionButton(inputId = "btn_unselectAll",
label = "Unselect All"),
actionButton(inputId = "btn_restoreDefault",
label = "Restore Default (select odd only)"),
DT::dataTableOutput("dt")
)
# Server ------------------------------------------------------------
server <-
shinyServer(function(input, output, session){
# Event Observers -----------------------------------------------
observeEvent(
input$btn_selectAll,
{
TmpData <- SourceData
TmpData$is_selected <- TRUE
replaceData(dt_proxy, prepareDataForDisplay(TmpData))
}
)
observeEvent(
input$btn_unselectAll,
{
TmpData <- SourceData
TmpData$is_selected <- FALSE
replaceData(dt_proxy, prepareDataForDisplay(TmpData))
}
)
observeEvent(
input$btn_restoreDefault,
{
replaceData(dt_proxy, prepareDataForDisplay(SourceData))
}
)
observeEvent(
input$btn_saveSelection,
{
check_input <- names(input)[grepl("is_selected_", names(input))]
id <- as.numeric(sub("is_selected_", "", check_input))
TmpData <- SourceData
for (i in seq_along(check_input)){
TmpData$is_selected[TmpData$sample_id == id[i]] <-
input[[check_input[i]]]
}
# At this point, I would also save changes to the remote database.
DT::replaceData(proxy = dt_proxy,
data = prepareDataForDisplay(TmpData))
}
)
# Output elements -----------------------------------------------
output$value_check <-
renderPrint({
sort(names(input))
})
output$dt <-
DT::renderDataTable({
SourceData %>%
prepareDataForDisplay() %>%
DT::datatable(selection = "none",
escape = FALSE,
filter = "top",
class = "compact cell-border",
options = list(preDrawCallback = JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
})
dt_proxy <- DT::dataTableProxy("dt")
})
# Run the application -----------------------------------------------
shinyApp(
ui = ui,
server = server
)
edit: Here is the solution to the original problem. I found it after scouring stack and the other part, persistent filters was found on a blog. May anyone who finds this never have to suffer like I have.
source_data <-
iris %>%
mutate(Species = as.factor(Species))
source_data$Date <- Sys.time() + seq_len(nrow(source_data))
# default global search value
if (!exists("default_search")) default_search <- ""
# default column search values
if (!exists("default_search_columns")) default_search_columns <- NULL
shinyApp(
ui = fluidPage(
DT::dataTableOutput('dataTable')
),
server = function(input, output, session) {
reactive_values <- reactiveValues(source_data = NULL)
observe({
reactive_values$source_data <- source_data
})
output$dataTable <- DT::renderDataTable(
reactive_values$source_data,
editable = list(target = "cell", disable = list(columns = c(1, 2))),
filter = "top",
selection = 'none',
options = list(
scrollX = TRUE,
stateSave = FALSE,
searchCols = default_search_columns,
search = list(
regex = FALSE,
caseInsensitive = FALSE,
search = default_search
)
)
)
proxy <- dataTableProxy('dataTable')
observe({
input$dataTable_cell_edit
# when it updates, save the search strings so they're not lost
isolate({
# update global search and column search strings
default_search <- input$dataTable_search
default_search_columns <- c("", input$dataTable_search_columns)
# update the search terms on the proxy table (see below)
proxy %>%
updateSearch(keywords =
list(global = default_search,
columns = default_search_columns))
})
})
observeEvent(input$dataTable_cell_edit, {
info = input$dataTable_cell_edit
str(info)
i <- info$row
j <- info$col
v <- info$value
reactive_values$source_data[i, j] <<- DT:::coerceValue(v, reactive_values$source_data[i, j])
source_data[i, j] <<- DT:::coerceValue(v, reactive_values$source_data[i, j])
replaceData(proxy, source_data, resetPaging = FALSE, rownames = FALSE)
})
}
)
I have spent days trying to find just the right solution to this problem and while I've seen many discussions nothing quite "works" how I need it to.
I need my solution to meet these requirements;
the table is editable
There are filters that are reactive to the contents of the table
When new values are entered into the table the edits are a) saved into the data b) reflected in the filters
I've tried DT while it has the nicest looking output I couldn't get the DT filters to update and if you made an edit and filtered the table the edit would be reverted.
rHandsOnTable had a better looking edit option but same issues as above.
dqshiny, an augment for rHandsonTable enables me to save the data and it updates the filter, but the filter options weren't good, the "select" input doesn't seem let me select nothing to display all results. And because my actual data has a lot of text in each box as I horizontally scroll the height of the cells change and this makes the filters and cell widths desync.
With that said here is what I've tried, I hope someone can help me figure out
### DT that doesn't update filters but saves content
shinyApp(
ui = fluidPage(
DT::dataTableOutput('x1')
),
server = function(input, output, session) {
x = iris
x$Date = Sys.time() + seq_len(nrow(x))
output$x1 = DT::renderDataTable(x, editable = TRUE, filter = "top", selection = 'none', rownames = FALSE)
proxy = dataTableProxy('x1')
observeEvent(input$x1_cell_edit, {
info = input$x1_cell_edit
str(info)
i = info$row
j = info$col + 1
v = info$value
x[i, j] <<- DT:::coerceValue(v, x[i, j])
replaceData(proxy, x, resetPaging = FALSE, rownames = FALSE)
})
}
)
dqShiny "works" but in my full dataset when I set each column's filter type something must be wrong with how it processes the data because it's discarding a lot of rows out of hand and I can't figure out why. Also can't turn off filters for specific columns. all or nothing as far as I can tell.
# library(tidyverse)
# library(shiny)
# library(rhandsontable)
# install.packages("remotes")
# library(remotes)
# remotes::install_github("daqana/dqshiny")
# library(dqshiny)
shinyApp(
ui = fluidPage(
dq_handsontable_output("randomTable", 9L)
),
server = function(input, output, session) {
hw <- c("Hello", "my", "funny", "world!")
data <- data.frame(A = rep(hw, 500), B = hw[c(2,3,4,1)],
C = 1:500, D = Sys.Date() - 0:499, stringsAsFactors = FALSE)
dq_render_handsontable(
"randomTable",
data = data,
width_align = TRUE,
filters = c("Select"),
table_param =
list(
height = 800,
readOnly = TRUE,
stretchH = "all",
highlightCol = TRUE,
highlightRow = TRUE
),
col_param =
list(
list(col = c("A", "B"), readOnly = FALSE, colWidths = "100%"),
list(col = c("C", "D"), colWidths = 300)
),
horizontal_scroll = TRUE
)
}
)
and then simple hands on table that I can't get to work even a little.
shinyApp(
ui = fluidPage(
rHandsontableOutput("randomTable")
),
server = function(input, output, session) {
hw <- c("Hello", "my", "funny", "world!")
data <- data.frame(
A = rep(hw, 500),
B = hw[c(2, 3, 4, 1)],
C = 1:500,
D = Sys.Date() - 0:499,
stringsAsFactors = FALSE
)
output$randomTable <- renderRHandsontable({
data %>%
rhandsontable(
height = 800,
readOnly = TRUE,
stretchH = "all",
colWidths = "100%"
) %>%
hot_col(c("A", "B"), readOnly = FALSE) %>%
hot_col(c("C", "D"), colWidths = 300) %>%
hot_table(highlightCol = TRUE, highlightRow = TRUE)
})
}
)
Perhaps you are looking for this
### DT updates filters
shinyApp(
ui = fluidPage(
DT::dataTableOutput('x1')
),
server = function(input, output, session) {
dfx <- reactiveValues(data=NULL)
observe({
x <- iris
x$Date = Sys.time() + seq_len(nrow(x))
dfx$data <- x
})
output$x1 = renderDT(dfx$data, editable = TRUE, filter = "top", selection = 'none', rownames = FALSE)
#proxy = dataTableProxy('x1')
observeEvent(input$x1_cell_edit, {
info = input$x1_cell_edit
str(info)
i = info$row
j = info$col + 1
v = info$value
dfx$data[i, j] <<- DT:::coerceValue(v, dfx$data[i, j])
#replaceData(proxy, x, resetPaging = FALSE, rownames = FALSE)
})
}
)
Please suggest how to update PostgreSQL database from R shiny app. I would like to be able to update values in table "testUpdate" in a PostgreSQL database:
Update "YN" after a checkbox is checked in 'x1' Data-table.
Update "Note" after "save_changes" button is pressed.
I've created fake data so you could see how the app works. Alternatively, I've included the data source. I haven't found one method that works well with R. Please suggest an implementation.
library(dplyr)
library(dbplyr)
library(DBI)
library(DT)
library(data.table)
library(shinyjs)
library(shinydashboard)
library(shinycssloaders)
library(tidyr)
library(tableHTML)
library(shiny)
library(RPostgreSQL)
pool <- pool::dbPool(drv = dbDriver("PostgreSQL"),
dbname = "postgreDatabase",
host = "11.111.11.1",
port = '12342',
user = "fdc",
password = "password")
shinyApp(
ui = fluidPage(
tabPanel("Test",
sidebarLayout(position = "right",
sidebarPanel(id="sidebar",
(DT::dataTableOutput("y1"))),
mainPanel(
(DT::dataTableOutput("x1")))
))),
server = function(input, output, session) {
buttonInput <- function(FUN, len, id, ...) {
inputs <- character(len)
for (i in seq_len(len)) {
inputs[i] <- as.character(FUN(paste0(id, i), ...))
}
inputs
}
# create a character vector of shiny inputs
shinyInput = function(FUN, len, id, value, width) {
if (length(value) == 1) value <- rep(value, len)
inputs = character(len)
for (i in seq_len(len)) {
inputs[i] =
as.character(FUN(paste0(id, i), label = NULL, value = value[i], width = width))
}
inputs
}
# obtain the values of inputs
shinyValue = function(id, len, initial) {
vapply(seq_len(len), function(i) {
value = input[[paste0(id, i)]]
if (is.null(value)) initial[i] else value
}, FUN.VALUE = logical(1))
}
#created fake data so you can run the app without the db.
n = 10
YN = rep(c(FALSE, TRUE), times = c(5,5))
df1 = data.frame(
cb = shinyInput(checkboxInput, n, 'cb_',
value = YN, width='30px'),
month = month.abb[1:n],
YN = YN,
ID = seq_len(n),
stringsAsFactors = FALSE
)
#####alternatively data comes from table called "testUpdate"
testUpdate <- tbl(db_pool,"testUpdate") %>% collect()
testUpdate_cols <- testUpdate %>%
select(ID, month, YN, Note)
vals <- reactiveValues()
vals$Data <- data.table(
ID = seq_len(n),
Note = c("test notes", "testing", "changed", "serial number", "", "", "", "", "testing", ""),
'Update Note' = buttonInput(
FUN = actionButton,
len = n,
id = 'button_',
label = "?",
onclick = 'Shiny.onInputChange(\"GoToNoteClick\", this.id)'
)
)
observeEvent(input$GoToNoteClick, {
showModal(modal_modify)
})
modal_modify<-modalDialog(
fluidPage(
textAreaInput(
"run_notes",
label = "Notes:",
width = "100%",
height = "100px"
),
actionButton("save_changes", "Save changes")
),
size="l"
)
get_sel <- reactive({
w <- input$x1_rows_selected
df1[w,] -> out
print(out)
out
})
filterMain <- reactive({
req(input$x1_rows_selected)
w <- input$x1_rows_selected
id_sel <- df1[w,'ID']
print(id_sel)
vals$Data %>% filter(ID %in% id_sel) -> out
out
})
output$y1 <- DT::renderDataTable(
datatable(
{
filterMain()
}
,escape = FALSE,
#class = "display compact",
rownames=F,
selection='none',
options = list(
dom = 't', paging = FALSE, ordering = FALSE)
))
loopData = reactive({
values = shinyValue('cb_', n, initial = YN)
dat = df1
dat$cb = shinyInput(checkboxInput, n, 'cb_',
value = values,
width = '30px')
dat$YN = values
dat
})
observeEvent(input$save_changes, {
req(vals$Data)
selected_row=as.numeric(gsub("button_","",input$GoToNoteClick))
print(selected_row)
curid <- vals$Data[selected_row,1]
print(curid)
print(input$run_notes)
vals$Data$Note[vals$Data$ID %in% curid] <- input$run_notes
##write changes
#write data back to postgreSQL
qry = paste0("UPDATE SET Note = '';")
print(qry)
dbSendQuery(conn = db_pool, statement = qry)
removeModal()
#dbDisconnect(db_pool)
})
output$x1 = renderDT(
df1, class = "display compact",
escape = FALSE, selection = 'single', rownames=F,
options = list(
dom = 't', paging = FALSE, ordering = FALSE,
preDrawCallback = JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = JS('function() { Shiny.bindAll(this.api().table().node()); } ')
))
proxy = dataTableProxy('x1')
observe({
replaceData(proxy, loopData(), resetPaging = FALSE, rownames=F)
})
}
)
The database was updated using the following function from here:database bulk update
updateDB <- function(editedValue, id, field, pool, tbl){
conn <- poolCheckout(pool)
id = id
col = field
value = editedValue
query <- glue::glue_sql("UPDATE {`tbl`} SET
{`col`} = {value}
WHERE runid = {id}
", .con = conn)
dbExecute(conn, sqlInterpolate(ANSI(), query))
poolReturn(conn)
return(invisible())
}
onStop(function() {
poolClose(db_pool)
})
The functionally works great in Rstudio Server Pro, however doesn't work in a published app via Rstudio Connect. Any suggestion on how to make this work in Rstudio connect would be extremely helpful.
Thanks
What I am attempting to do, is to allow the user to pass in a configuration/lookup excel table into shiny, display this table in shiny, allow the user to make cells edits in shiny, and use the values that were edited from the editable table for calculations. My problem arises for the last step "use the values that were edited from the editable table for calculations".
The excel file consists of 2 tabs with data of the following content:
Tab1 Name: "parameters"
data.frame(Name = c("a", "b", "c"), Value = c(1:3))
Tab2 Name: "parameters2"
data.frame(Name = c("a", "b", "c"), Value = c(4:6))
The ideal shiny app would do the following:
1) At upload, perform a calculation adding the unchanged first values of Tab 1 and Tab 2. This would be 1 + 4 = 5.
2) If user edits Tab 1's value of 1 to 8, then the calculation would result in 8 + 4 = 12.
Effectively, I want to use the edited tables values to update all my calculations if the user makes any edits to it. I know this can be done by simply uploading a new file in shiny, but I would rather allow them to do this in shiny as opposed to uploading a new file.
Here is my shiny app. Appreciate any help/guidance!
library(shiny)
library(DT)
shinyApp(
ui <- fluidPage(
fileInput(inputId = "config", label = "Upload Configuration File",
multiple = F, accept = c(".xlsx", ".xls")),
verbatimTextOutput("txt"),
tagList(tags$head(tags$style(type = 'text/css','.navbar-brand{display:none;}')),
navbarPage(title = "",
tabPanel(title = "Parameters",
dataTableOutput(outputId = "edit.param", width = 2)),
tabPanel(title = "Parameters2",
dataTableOutput(outputId = "edit.param2", width = 2))
)
)
),
server = function(input, output, session) {
config.path = reactive({
inFile = input$config
if(is.null(inFile)) {
return(NULL)
} else {
return(inFile$datapath)
}
})
df.param = reactive({
read_excel(path = config.path(), sheet = "parameters")
})
df.param2 = reactive({
read_excel(path = config.path(), sheet = "parameters2")
})
output$edit.param = renderDT(df.param(), selection = "none", server = F, editable = "cell")
output$edit.param2 = renderDT(df.param2(), selection = "none", server = F, editable = "cell")
observeEvent(input$edit.param_cell_edit, {
df.param()[input$edit.param_cell_edit$row, input$edit.param_cell_edit$col] <<- input$edit.param_cell_edit$value
})
observeEvent(input$edit.param2_cell_edit, {
df.param()[input$edit.param2_cell_edit$row, input$edit.param2_cell_edit$col] <<- input$edit.param2_cell_edit$value
})
output$txt = reactive({
df.param()$value[1] + df.param2()$value[1]
})
}
)
I also tried this for the server section and had no luck either:
output$edit.param = renderDT(df.param(), selection = "none", server = F, editable = "cell")
output$edit.param2 = renderDT(df.param2(), selection = "none", server = F, editable = "cell")
observe(input$edit.param_cell_edit)
observe(input$edit.param2_cell_edit)
Could you try this? (I have not tried).
library(shiny)
library(DT)
shinyApp(
ui <- fluidPage(
fileInput(inputId = "config", label = "Upload Configuration File",
multiple = F, accept = c(".xlsx", ".xls")),
verbatimTextOutput("txt"),
tagList(tags$head(tags$style(type = 'text/css','.navbar-brand{display:none;}')),
navbarPage(title = "",
tabPanel(title = "Parameters",
dataTableOutput(outputId = "edit_param", width = 2)),
tabPanel(title = "Parameters2",
dataTableOutput(outputId = "edit_param2", width = 2))
)
)
),
server = function(input, output, session) {
config.path = reactive({
inFile = input$config
if(is.null(inFile)) {
return(NULL)
} else {
return(inFile$datapath)
}
})
df_param <- reactiveVal()
observe({
req(config.path())
df_param(read_excel(path = config.path(), sheet = "parameters"))
})
df_param2 <- reactiveVal()
observe({
req(config.path())
df_param2(read_excel(path = config.path(), sheet = "parameters2"))
})
output$edit_param = renderDT({
req(df_param())
datatable(isolate(df_param()), selection = "none", editable = "cell")
})
output$edit_param2 = renderDT({
req(df_param2())
datatable(isolate(df_param2()), selection = "none", editable = "cell")
})
proxy <- dataTableProxy("edit_param")
proxy2 <- dataTableProxy("edit_param2")
observeEvent(input$edit_param_cell_edit, {
info <- input$edit_param_cell_edit
df_param(editData(df_param(), info, proxy, resetPaging = FALSE))
})
observeEvent(input$edit_param2_cell_edit, {
info <- input$edit_param2_cell_edit
df_param2(editData(df_param2(), info, proxy2, resetPaging = FALSE))
})
output$txt = renderPrint({
df_param()$value[1] + df_param2()$value[1]
})
}
)