Text Input in DT::datatable unbinds and I can't rebind it - r

I am working on a shiny application that allows users to enter comments about an observation. The comments are then saved in a SQL database on the back end. The code below is a working representation of my current application.
What is happening is the tables load with the subset of Cylinder = 4 (the radio buttons), the user can save comments, got to Cylinder = 6, save comments, and then Cylinder = 8, and save comments. But if I ever change the cylinder back to a value that I've already saved comments at, the text inputs are unbound and no comments are saved. In order to restore the functionality, I have to restart the application. I've found that irritates my users.
What do I need to do to make sure I can continue to save comments if I go back to a Cylinder value I've already used?
I'm sorry that it isn't a very concise example. When you enter a comment, the console will print the number of comments saved, and display the data frame that was altered so you can compare what is showing in the application.
library(shiny)
library(DT)
library(dplyr)
mtcars$comment <- rep("", nrow(mtcars))
mtcars$row_id <- seq_len(nrow(mtcars))
AppData <- split(mtcars, mtcars[c("cyl", "am")])
# Makes a text input column out of a data frame
make_inputtable <- function(df){
df$comment <-
mapply(
function(comment, id){
as.character(textInput(inputId = sprintf("txt_comment_%s", id),
label = "",
value = comment))
},
comment = df$comment,
id = df$row_id,
SIMPLIFY = TRUE)
df
}
ui <- shinyUI(
fluidPage(
radioButtons(inputId = "rdo_cyl",
label = "Cylinders",
choices = sort(unique(mtcars$cyl)),
inline = TRUE),
h3("Automatic"),
actionButton(inputId = "btn_save_automatic",
label = "Save Comments"),
DT::dataTableOutput("am0"),
hr(),
h3("Manual"),
actionButton(inputId = "btn_save_manual",
label = "Save Comments"),
DT::dataTableOutput("am1"),
# unbind a datatable. Needs to be done before a table is redrawn.
tags$script(HTML(
"Shiny.addCustomMessageHandler('unbind-DT', function(id) {
Shiny.unbindAll($('#'+id).find('table').DataTable().table().node());
})"))
)
)
server <- shinyServer(function(input, output, session){
reactiveData <- reactiveValues(
am0_cyl4 = AppData[["4.0"]],
am0_cyl6 = AppData[["6.0"]],
am0_cyl8 = AppData[["8.0"]],
am1_cyl4 = AppData[["4.1"]],
am1_cyl6 = AppData[["6.1"]],
am1_cyl8 = AppData[["8.1"]]
)
# Reactive Objects ------------------------------------------------
ref0 <- reactive({
sprintf("am0_cyl%s", input$rdo_cyl)
})
data0 <- reactive({
reactiveData[[ref0()]]
})
ref1 <- reactive({
sprintf("am1_cyl%s", input$rdo_cyl)
})
data1 <- reactive({
reactiveData[[ref1()]]
})
# Event Observers -------------------------------------------------
observeEvent(
input$btn_save_automatic,
{
in_field <- names(input)[grepl("^txt_comment_", names(input))]
in_field_id <- sub("^txt_comment_", "", in_field)
in_field_id <- as.numeric(in_field_id)
in_field_id <- in_field_id[in_field_id %in% data0()$row_id]
exist_frame <- data0()[c("row_id", "comment")]
new_frame <-
data.frame(
row_id = in_field_id,
comment = vapply(in_field_id,
function(id){ input[[sprintf("txt_comment_%s", id)]]},
character(1)),
stringsAsFactors = FALSE)
Compare <- left_join(exist_frame,
new_frame,
by = "row_id",
suffix = c("_exist", "_new")) %>%
filter(comment_exist != comment_new)
message(sprintf("* %s comment(s) saved", nrow(Compare)))
# Only perform the save operations if there are changes to be made.
if (nrow(Compare)){
session$sendCustomMessage("unbind-DT", "am0")
for(i in seq_len(nrow(Compare))){
row <- Compare$row_id
reactiveData[[ref0()]]$comment[reactiveData[[ref0()]]$row_id == row] <-
input[[sprintf("txt_comment_%s", row)]]
}
print(data0())
}
}
)
# Very similar to btn_save_automatic
observeEvent(
input$btn_save_manual,
{
in_field <- names(input)[grepl("^txt_comment_", names(input))]
in_field_id <- sub("^txt_comment_", "", in_field)
in_field_id <- as.numeric(in_field_id)
in_field_id <- in_field_id[in_field_id %in% data1()$row_id]
exist_frame <- data1()[c("row_id", "comment")]
new_frame <-
data.frame(
row_id = in_field_id,
comment = vapply(in_field_id,
function(id){ input[[sprintf("txt_comment_%s", id)]]},
character(1)),
stringsAsFactors = FALSE)
Compare <- left_join(exist_frame,
new_frame,
by = "row_id",
suffix = c("_exist", "_new")) %>%
filter(comment_exist != comment_new)
message(sprintf("* %s comment(s) saved", nrow(Compare)))
# Only perform the save operations if there are changes to be made.
if (nrow(Compare)){
session$sendCustomMessage("unbind-DT", "am1")
for(i in seq_len(nrow(Compare))){
row <- Compare$row_id
reactiveData[[ref1()]]$comment[reactiveData[[ref1()]]$row_id == row] <-
input[[sprintf("txt_comment_%s", row)]]
}
print(data1())
}
}
)
# Output Objects --------------------------------------------------
output$am0 <-
DT::renderDataTable({
make_inputtable(data0()) %>%
datatable(escape = -13,
options = list(preDrawCallback = DT::JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = DT::JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
})
output$am1 <-
DT::renderDataTable({
make_inputtable(data1()) %>%
datatable(escape = -13,
options = list(preDrawCallback = DT::JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = DT::JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
})
})
shinyApp(ui = ui, server = server)
Edits and updates
editable data tables are a potential solution, but would require upgrading our package library. We are currently using R 3.4.1 with shiny 1.0.4 and DT 0.2.12.
Yes, that's comparatively ancient. But the cost of upgrading is substantial given the sensitivity of the reports supported by this application and the quality assurance required by any upgrade.

Putting aside your version restrictions, here is how I'd approach this with the latest library(DT) version (Hopefully useful for future readers and maybe someday you will also update):
Edit: now using dataTableProxy to avoid re-rendering.
library(shiny)
library(DT)
ui <- shinyUI(
fluidPage(
radioButtons(inputId = "rdo_cyl",
label = "Cylinders",
choices = sort(unique(mtcars$cyl)),
inline = TRUE),
h3("Automatic"),
actionButton(inputId = "btn_save_automatic",
label = "Save Comments"), p(),
DTOutput("am0"),
hr(),
h3("Manual"),
actionButton(inputId = "btn_save_manual",
label = "Save Comments"), p(),
DTOutput("am1")
)
)
server <- shinyServer(function(input, output, session){
globalData <- mtcars
globalData$comment <- rep("", nrow(mtcars))
globalData$row_id <- seq_len(nrow(mtcars))
diabledCols <- grep("comment", names(globalData), invert = TRUE)
AppData <- reactiveVal(globalData)
automaticAppData <- reactive({
AppData()[AppData()[["cyl"]] %in% input$rdo_cyl & AppData()[["am"]] %in% "0", ]
})
manualAppData <- reactive({
AppData()[AppData()[["cyl"]] %in% input$rdo_cyl & AppData()[["am"]] %in% "1", ]
})
output$am0 <- DT::renderDT(
# isolate: render only once
expr = {isolate(automaticAppData())},
editable = list(target = "cell", disable = list(columns = diabledCols))
)
output$am1 <- DT::renderDT(
# isolate: render only once
expr = {isolate(manualAppData())},
editable = list(target = "cell", disable = list(columns = diabledCols))
)
observeEvent(input$btn_save_automatic, {
info = input$am0_cell_edit
str(info)
i = automaticAppData()$row_id[[info$row]]
j = info$col
v = info$value
globalData[i, j] <<- DT::coerceValue(v, globalData[i, j])
AppData(globalData)
# update database...
})
observeEvent(input$btn_save_manual, {
info = input$am1_cell_edit
str(info)
i = manualAppData()$row_id[[info$row]]
j = info$col
v = info$value
globalData[i, j] <<- DT::coerceValue(v, globalData[i, j])
AppData(globalData)
# update database...
})
am0Proxy <- dataTableProxy("am0")
am1Proxy <- dataTableProxy("am1")
observeEvent(automaticAppData(), {
replaceData(am0Proxy, automaticAppData(), resetPaging = FALSE)
})
observeEvent(manualAppData(), {
replaceData(am1Proxy, manualAppData(), resetPaging = FALSE)
})
})
shinyApp(ui = ui, server = server)
Here are some related infos.
Update for DT Version 0.2
Here is another solution closer to your initial code. I'm using isolate(), dataTableProxy() and replaceData() which are available since DT version 0.2 to avoid re-rendering the table, which resolves the binding issue and should be faster.
Another problem in your code was that you called session$sendCustomMessage("unbind-DT", "am0") twice instead of using it for "am1".
library(shiny)
library(DT)
library(dplyr)
mtcars$comment <- rep("", nrow(mtcars))
mtcars$row_id <- seq_len(nrow(mtcars))
AppData <- split(mtcars, mtcars[c("cyl", "am")])
# Makes a text input column out of a data frame
make_inputtable <- function(df){
df$comment <-
mapply(
function(comment, id){
as.character(textInput(inputId = sprintf("txt_comment_%s", id),
label = "",
value = comment))
},
comment = df$comment,
id = df$row_id,
SIMPLIFY = TRUE)
df
}
ui <- shinyUI(
fluidPage(
radioButtons(inputId = "rdo_cyl",
label = "Cylinders",
choices = sort(unique(mtcars$cyl)),
inline = TRUE),
h3("Automatic"),
actionButton(inputId = "btn_save_automatic",
label = "Save Comments"),
DT::dataTableOutput("am0"),
hr(),
h3("Manual"),
actionButton(inputId = "btn_save_manual",
label = "Save Comments"),
DT::dataTableOutput("am1"),
# unbind a datatable. Needs to be done before a table is redrawn.
tags$script(HTML(
"Shiny.addCustomMessageHandler('unbind-DT', function(id) {
Shiny.unbindAll($('#'+id).find('table').DataTable().table().node());
})"))
)
)
server <- shinyServer(function(input, output, session){
reactiveData <- reactiveValues(
am0_cyl4 = AppData[["4.0"]],
am0_cyl6 = AppData[["6.0"]],
am0_cyl8 = AppData[["8.0"]],
am1_cyl4 = AppData[["4.1"]],
am1_cyl6 = AppData[["6.1"]],
am1_cyl8 = AppData[["8.1"]]
)
# Reactive Objects ------------------------------------------------
ref0 <- reactive({
sprintf("am0_cyl%s", input$rdo_cyl)
})
data0 <- reactive({
reactiveData[[ref0()]]
})
ref1 <- reactive({
sprintf("am1_cyl%s", input$rdo_cyl)
})
data1 <- reactive({
reactiveData[[ref1()]]
})
# Event Observers -------------------------------------------------
observeEvent(
input$btn_save_automatic,
{
in_field <- names(input)[grepl("^txt_comment_", names(input))]
in_field_id <- sub("^txt_comment_", "", in_field)
in_field_id <- as.numeric(in_field_id)
in_field_id <- in_field_id[in_field_id %in% data0()$row_id]
exist_frame <- data0()[c("row_id", "comment")]
new_frame <-
data.frame(
row_id = in_field_id,
comment = vapply(in_field_id,
function(id){ input[[sprintf("txt_comment_%s", id)]]},
character(1)),
stringsAsFactors = FALSE)
Compare <- left_join(exist_frame,
new_frame,
by = "row_id",
suffix = c("_exist", "_new")) %>%
filter(comment_exist != comment_new)
message(sprintf("* %s comment(s) saved", nrow(Compare)))
# Only perform the save operations if there are changes to be made.
if (nrow(Compare)){
session$sendCustomMessage("unbind-DT", "am0")
for(i in seq_len(nrow(Compare))){
row <- Compare$row_id
reactiveData[[ref0()]]$comment[reactiveData[[ref0()]]$row_id == row] <-
input[[sprintf("txt_comment_%s", row)]]
}
print(data0())
}
}
)
# Very similar to btn_save_automatic
observeEvent(
input$btn_save_manual,
{
in_field <- names(input)[grepl("^txt_comment_", names(input))]
in_field_id <- sub("^txt_comment_", "", in_field)
in_field_id <- as.numeric(in_field_id)
in_field_id <- in_field_id[in_field_id %in% data1()$row_id]
exist_frame <- data1()[c("row_id", "comment")]
new_frame <-
data.frame(
row_id = in_field_id,
comment = vapply(in_field_id,
function(id){ input[[sprintf("txt_comment_%s", id)]]},
character(1)),
stringsAsFactors = FALSE)
Compare <- left_join(exist_frame,
new_frame,
by = "row_id",
suffix = c("_exist", "_new")) %>%
filter(comment_exist != comment_new)
message(sprintf("* %s comment(s) saved", nrow(Compare)))
# Only perform the save operations if there are changes to be made.
if (nrow(Compare)){
session$sendCustomMessage("unbind-DT", "am1")
for(i in seq_len(nrow(Compare))){
row <- Compare$row_id
reactiveData[[ref1()]]$comment[reactiveData[[ref1()]]$row_id == row] <-
input[[sprintf("txt_comment_%s", row)]]
}
print(data1())
}
}
)
# Output Objects --------------------------------------------------
output$am0 <-
DT::renderDataTable({
# isolate: render table only once!
make_inputtable(isolate(data0())) %>%
datatable(escape = -13,
options = list(preDrawCallback = DT::JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = DT::JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
}, server = TRUE)
output$am1 <-
DT::renderDataTable({
# isolate: render table only once!
make_inputtable(isolate(data1())) %>%
datatable(escape = -13,
options = list(preDrawCallback = DT::JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = DT::JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
}, server = TRUE)
am0Proxy <- dataTableProxy("am0")
am1Proxy <- dataTableProxy("am1")
observeEvent(data0(), {
replaceData(am0Proxy, make_inputtable(data0()), resetPaging = FALSE) # important
}, ignoreInit = TRUE)
observeEvent(data1(), {
replaceData(am1Proxy, make_inputtable(data1()), resetPaging = FALSE) # important
}, ignoreInit = TRUE)
})
shinyApp(ui = ui, server = server)

You are either unbinding too soon or too late, I am not certain from the code snippet you posted. Can you make multiple objects of the same type to bind to instead?
Edit:
I find this line suspicious:
# unbind a datatable. Needs to be done before a table is redrawn.
tags$script(HTML(
"Shiny.addCustomMessageHandler('unbind-DT', function(id) {
Shiny.unbindAll($('#'+id).find('table').DataTable().table().node());
})")) )
Seems like you are unbinding twice and binding only once.

Related

How to render datatable with radiobutton inputs per each row in R Shiny multiple times without generating new inputs each time

I have a R Shiny app with datatable in which there is an input field at each row. I was able to implement it whenever the data is static - generated one time. But in my app the user need to load the data multiple times and each time to fill in the input and submit, and then load another dataset.
In the first load the input works well, but on the change of data it is probably try to re-render the input fields, and they stop responding. I hacked it with creating a new input ids per data load, but this solution may end with thousands of inputs if the user have many dataset to go over.
I short, I would like to implement this without the values$j index: (paste0("answers_",values$j,"_", i)).
thanks,
p.s. with renderTable it works, but I need the render
I have a minimum reproducible example below:
library(shiny)
library(DT)
library(data.table)
ui <- fluidPage(
selectInput("selected_project", "Select project",choices=c("A","B"),multiple=FALSE,selected = "A")
,selectInput("maximum_questions_to_show", "Maximum questions to show",choices=c(5,10),multiple=FALSE,selected = 10)
,actionButton("submit_answers","Submit")
,verbatimTextOutput('answersText')
,DT::dataTableOutput("answerTable")
# ,tableOutput("answerTable")
)
server <- function(input, output, session){
values <- reactiveValues(j=0)
getData<-eventReactive(c(input$selected_project,input$maximum_questions_to_show), {
values$j=values$j+1
print("BBB")
if(input$selected_project=="A")
data<-data.frame("project"=rep("A",30),"id"=paste0("A_",1:30),"answers"=rep("n",30),stringsAsFactors=FALSE)
if(input$selected_project=="B")
data<-data.frame("project"=rep("B",50),"id"=paste0("B_",1:50),"answers"=rep("n",50),stringsAsFactors=FALSE)
nrows<-min(dim(data)[1],as.numeric(input$maximum_questions_to_show))
data=data[1:nrows,]
answers<-sapply(1:nrows,function(i) {
as.character(radioButtons(inputId=paste0("answers_",values$j,"_", i), label=paste0("answers_",values$j,"_", i)
, choices=c("n","y"),selected=data$answers[i],inline=TRUE))
})
# answers<-sapply(1:nrows,function(i) {
# as.character(radioButtons(inputId=paste0("answers_", i), label=paste0("answers_", i)
# , choices=c("n","y"),selected=data$answers[i],inline=TRUE))
# })
data$answers<-answers
return(list("data"=data))
}, ignoreNULL = FALSE)
output$answerTable <- DT::renderDataTable({
data<-getData()$data
data.table(
"project"=data[,"project"]
,"id"=data[,"id"]
,"answers" = data[,"answers"]
)
},editable = TRUE,escape=FALSE,selection = 'none', server = FALSE,rownames = FALSE,
,options = list(dom="Bftsp",lengthMenu = list(c(5, 15, -1), c('5', '15', 'All')),pageLength = 5,paging=TRUE
,preDrawCallback = JS('function() { Shiny.unbindAll(this.api().table().node()); }')
,drawCallback = JS('function() { Shiny.bindAll(this.api().table().node()); } '))
)
# output$answerTable <- renderTable({
# data<-getData()$data
# data[,c("id","answers")]
# },sanitize.text.function = function(x) x)
answers_results <- reactive({
data<-getData()$data
nrows<-dim(data)[1]
(sapply(1:nrows, function(i) input[[paste0("answers_",values$j,"_", i)]]))
# (sapply(1:nrows, function(i) input[[paste0("answers_", i)]]))
})
output$answersText = renderPrint({
unlist(lapply(answers_results() , function(x) ifelse(is.null(x),"n",x)))
})
observeEvent(input$submit_answers,{
print(unlist(lapply(answers_results() , function(x) ifelse(is.null(x),"n",x))))
})
}
shinyApp(ui, server)
You have to unbind each time there's a rendering. Here I run the unbinding in getData:
library(shiny)
library(DT)
ui <- fluidPage(
tags$head(tags$script(
HTML(
"Shiny.addCustomMessageHandler('unbindDT', function(id) {
var $table = $('#'+id).find('table');
if($table.length > 0){
Shiny.unbindAll($table.DataTable().table().node());
}
})")
)),
selectInput(
"selected_project",
"Select project",
choices = c("A", "B"),
multiple = FALSE,
selected = "A"
),
selectInput(
"maximum_questions_to_show",
"Maximum questions to show",
choices = c(5, 10),
multiple = FALSE,
selected = 10
),
actionButton("submit_answers", "Submit"),
verbatimTextOutput("answersText"),
DTOutput("answerTable")
)
server <- function(input, output, session){
getData <- eventReactive(
c(input$selected_project, input$maximum_questions_to_show),
{
session$sendCustomMessage("unbindDT", "answerTable")
print("BBB")
if(input$selected_project == "A"){
data <- data.frame(
"project" = rep("A", 30),
"id" = paste0("A_", 1:30),
"answers" = rep("n", 30),
stringsAsFactors = FALSE
)
}else{
data <- data.frame(
"project" = rep("B", 50),
"id" = paste0("B_", 1:50),
"answers" = rep("n", 50),
stringsAsFactors = FALSE
)
}
nrows <- min(nrow(data), as.numeric(input$maximum_questions_to_show))
data <- data[1:nrows, ]
answers <- sapply(1:nrows, function(i){
as.character(
radioButtons(inputId=paste0("answers_", i),
label=paste0("answers_", i),
choices=c("n","y"),
selected=data$answers[i],
inline=TRUE)
)
})
data$answers <- answers
return(list("data" = data))
},
ignoreNULL = FALSE
)
output$answerTable <- renderDT(
{
data <- getData()$data[, c("project", "id", "answers")]
datatable(
data,
editable = TRUE,
escape = FALSE,
selection = "none",
rownames = FALSE,
options = list(
dom = "Bftsp",
lengthMenu = list(c(5, 15, -1), c("5", "15", "All")),
pageLength = 5,
paging = TRUE,
preDrawCallback =
JS("function() { Shiny.unbindAll(this.api().table().node()); }"),
drawCallback =
JS("function() { Shiny.bindAll(this.api().table().node()); } ")
)
)
},
server = FALSE
)
answers_results <- reactive({
data <- getData()$data
nrows <- nrow(data)
(sapply(1:nrows, function(i) input[[paste0("answers_", i)]]))
})
output$answersText <- renderPrint({
unlist(lapply(answers_results(), function(x) ifelse(is.null(x), "n", x)))
})
observeEvent(input$submit_answers, {
print(unlist(lapply(answers_results(), function(x) ifelse(is.null(x), "n", x))))
})
}
shinyApp(ui, server)

Register all inputs inside a multi-page data table

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
)

Updating PostgreSQL database from R shiny app

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

User input in DataTable used for recalculation and update of column in Shiny

I want to create a web app, which allows user to enter input in numericInput object, which is embedded in DataTable and recalculates result (multiplication of column with some static values and a user input column) in another column.
I believe that when I set a reactive function which wraps around merging dataset and user input column and later I call it from RenderDataTable, that I somehow break the reactivity and I don't have a clue how to keep reactivity within table dependent on user input (which is also in the table). Please help.
Reproducible example to where I am stuck:
library(shiny)
library(DT)
set.seed(21)
db <- data.frame(ent = rep(x = 1,5),
group = c("G","M","O","F","L"),
val = sample(1:100, 5, replace=TRUE))
ui <- fluidPage(
titlePanel(paste0("entity - ", unique(db$ent))),
sidebarLayout(
sidebarPanel(
helpText("Shiny app calculation")
),
mainPanel(
DT::dataTableOutput("table")
))
)
numericText <- function(FUN, id_nums, id_base, label, value, ...) {
inputs <- 1:length(id_nums)
for (i in 1:length(inputs)) {
inputs[i] <- as.character(FUN(paste0(id_base,
id_nums[i]), label, value, ...))
}
return(inputs)
}
inputs <- numericText(numericInput,
id_nums = as.character(1:5),
id_base = "input_",
label = NULL,
value = 0)
db <- data.frame(db,
num = inputs)
server <- function(input, output, session) {
shinyValue = function(id, len) {
unlist(lapply(seq_len(len), function(i) {
value = input[[paste0(id, i)]]
if (is.null(value)) NA else value
}))
}
output_table <- reactive({
data.frame(db, calc = shinyValue("input_", 5))
})
output$table <- renderDataTable({
datatable(output_table(), rownames = FALSE, escape = FALSE, selection
= 'none', options = list(paging = FALSE, ordering = FALSE, searching
= FALSE, preDrawCallback = JS('function() {
Shiny.unbindAll(this.api().table().node()); }'), drawCallback =
JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
})
}
shinyApp(ui = ui, server = server)
Also maybe it helps - I was able to do this if I remove reactive expression from the dataframe and if I write result in another output type(however this is not a solution, since my main purpose is to write it in another column in DataTable):
library(shiny)
library(DT)
set.seed(21)
db <- data.frame(ent = rep(x = 1,5),
group = c("G","M","O","F","L"),
val = sample(1:100, 5, replace=TRUE))
ui <- fluidPage(
titlePanel(paste0("entity - ", unique(db$ent))),
sidebarLayout(
sidebarPanel(
helpText("Shiny app calculation")
),
mainPanel(
DT::dataTableOutput("table"),
verbatimTextOutput("text")
))
)
numericText <- function(FUN, id_nums, id_base, label, value, ...) {
inputs <- 1:length(id_nums)
for (i in 1:length(inputs)) {
inputs[i] <- as.character(FUN(paste0(id_base,
id_nums[i]), label, value, ...))
}
return(inputs)
}
inputs <- numericText(numericInput,
id_nums = as.character(1:5),
id_base = "input_",
label = NULL,
value = 0)
db <- data.frame(db,
num = inputs)
server <- function(input, output, session) {
shinyValue = function(id, len) {
unlist(lapply(seq_len(len), function(i) {
value = input[[paste0(id, i)]]
if (is.null(value)) NA else value
}))
}
output_table <- db
output$table <- renderDataTable({
datatable(output_table, rownames = FALSE, escape = FALSE, selection
= 'none', options = list(paging = FALSE, ordering = FALSE, searching
= FALSE, preDrawCallback = JS('function() {
Shiny.unbindAll(this.api().table().node()); }'), drawCallback =
JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
})
}
output$text <- reactive({shinyValue("input_", 5) * db$val
})
shinyApp(ui = ui, server = server)
I couldn't fully understand your code so I've myself produced another reproducible example based on a bunch of other answers especially this one.
library(shiny)
library(data.table)
library(rhandsontable)
DF = data.frame(num = 1:10, qty = rep(0,10), total = 1:10,
stringsAsFactors = FALSE)
#DF = rbind(DF, c(0,0,0))
ui = fluidPage(
titlePanel("Reactive Table "),
fluidRow(box(rHandsontableOutput("table", height = 400)))
)
server = function(input, output) {
data <- reactiveValues(df=DF)
observe({
input$recalc
data$df <- as.data.frame(DF)
})
observe({
if(!is.null(input$table))
data$df <- hot_to_r(input$table)
})
output$table <- renderRHandsontable({
rhandsontable(data$df)
})
output$table <- renderRHandsontable({
data$df$total <- data$df$num * data$df$qty
print(sum(data$df$num*data$df$price) )
rhandsontable(data$df, selectCallback = TRUE)
})
}
shinyApp(ui, server)
The very first idea is to use rhandsontable which is specifically for this kind of purpose.

Error: "Invalid JSON response" when I try to update data with DT::replaceData() [shiny]

I'm trying to update a table by changing the class of each variable (string or character). I have tried several methods but most of them just don't render something. With the method used in the code bellow, I can just change one time the type of the variable and then, the table is not reactive anymore.
Do you have an idea of what should I do?
library(DT)
library(shiny)
library(tidyverse)
server = shinyServer(function(input, output, session) {
current_stage <- reactiveValues(data=NULL)
mydata <- reactive({
df <- mtcars %>% rownames_to_column("model")
current_stage$data <- df
df
})
updateData = reactive({
df = mydata()
map(1:ncol(df), function(i){
if(length(input[[paste0("col", i)]])>0){
if (input[[paste0("col", i)]]=="num"){
df[,i] <<- unlist(df[,i]) %>% as.numeric
} else if (input[[paste0("col", i)]]=="cat"){
df[,i] <<- unlist(df[,i]) %>% as.character
}
}
})
#current_stage$data <- df
df
})
output$tableau <- DT::renderDataTable({
df <- current_stage$data
class <- map_df(df, typeof)
class <- gsub("double", "numeric", class)
class <- gsub("integer", "numeric", class)
tableSelectInput <- map(1:ncol(df),
function(i) {
if (class[i] =="numeric"){
opt1 <- "num"
opt2 <- "cat"
}else{
opt1 <- "cat"
opt2 <- "num"
}
selectInput(
inputId = paste0("col", i),
label = NULL, selected = opt1,
choices = c(opt1, opt2))
}
)
# I didn't find a more elegant way to turn '[[ ]]' in '[ ]'
l <- length(tableSelectInput)
selectin <- 1:l
type_cat <- 1:l
for (i in 1:l) {
selectin[i] = as.character(tableSelectInput[[i]])
pos=gregexpr("selected>",selectin[i])[[1]][1]
type_cat[i] = substr(selectin[i],(pos+9),(pos+11))
}
col_names = paste0(colnames(df), " <br/><em>(",type_cat,")</em><br/>", selectin)
isolate(DT::datatable(
current_stage$data,
options = list(
preDrawCallback = JS("function() {
Shiny.unbindAll(this.api().table().node()); }"),
drawCallback = JS("function() { Shiny.bindAll(this.api().table().node());
}")
),
colnames =col_names,
editable = TRUE,
escape=FALSE,
selection = list(target = 'column')))
}, server=FALSE)
output$log <- renderPrint({
updateData()
})
output$log2 <- renderPrint({
map_df(updateData(),class)
})
})
ui = shinyUI(
fluidPage(
titlePanel("My Awesome Shiny App"),
# Show a plot of the generated distribution
mainPanel(
DT::DTOutput("tableau"),
# Show log
verbatimTextOutput("log"),
verbatimTextOutput("log2")
)
)
)
runApp(list(ui = ui, server = server))
I think there is something wrong with the input button call "col1", "col2", etc..
I should maybe create them in a different way but I don't I am just stuck for the moment. Someone could give me an advice?
Kevin
Update:
I have tried this code but it looks to have some trouble with Ajax:
DataTables warning: table id=DataTables_Table_0 - Invalid JSON response. For more information about this error, please see http://datatables.net/tn/1"
library(DT)
library(shiny)
library(tidyverse)
server = shinyServer(function(input, output, session) {
current_stage <- reactiveValues(data=NULL, init=NULL,n=0)
mydata <- reactive({
df <- mtcars %>% rownames_to_column("model")
current_stage$data <- df
current_stage$init <- df
df
})
updateData = reactive({
# input$refresh
# df$ID <<- c(df$ID[n], df$ID[-n])
df = mydata()
if( !is.null(current_stage$data)){
df <- current_stage$data
map(1:ncol(df), function(i){
if(length(input[[paste0("col", i)]])>0){
if (input[[paste0("col", i)]]=="num"){
df[,i] <<- unlist(current_stage$init [,i]) %>% as.numeric
} else if (input[[paste0("col", i)]]=="cat"){
df[,i] <<- unlist(current_stage$init [,i]) %>% as.character
}
}
})}
# if(length(input[[paste0("col", 1)]])>0){
# if (input[[paste0("col", 1)]]=="num"){
# # message(proxy)
# df[,1] <- unlist(df[,1]) %>% as.numeric
# # # mydata()[,1] <- paste(proxy[,1],"ok")
# } else if (input[[paste0("col", 1)]]=="cat"){
# # # message(proxy)
# df[,1] <- unlist(df[,1]) %>% as.character
# }}
current_stage$data <- df
df
})
output$tableau <- DT::renderDataTable({
#df <- mtcars %>% rownames_to_column("model")
df <- mydata()
class <- map_df(df, typeof)
class <- gsub("double", "numeric", class)
class <- gsub("integer", "numeric", class)
tableSelectInput <- map(1:ncol(df),
function(i) {
if (class[i] =="numeric"){
opt1 <- "num"
opt2 <- "cat"
}else{
opt1 <- "cat"
opt2 <- "num"
}
selectInput(
inputId = paste0("col", i),
label = NULL, selected = opt1,
choices = c(opt1, opt2))
}
)
# I didn't find a more elegant way to turn '[[ ]]' in '[ ]'
l <- length(tableSelectInput)
selectin <- 1:l
type_cat <- 1:l
for (i in 1:l) {
selectin[i] = as.character(tableSelectInput[[i]])
pos=gregexpr("selected>",selectin[i])[[1]][1]
type_cat[i] = substr(selectin[i],(pos+9),(pos+11))
}
col_names = paste0( colnames(df), " <br/><em>(",type_cat,")</em><br/>", selectin)
DT::datatable(isolate(
updateData()),
options = list(
preDrawCallback = JS("function() {
Shiny.unbindAll(this.api().table().node()); }"),
drawCallback = JS("function() { Shiny.bindAll(this.api().table().node());
}")
),
colnames =col_names,
editable = TRUE,
escape=FALSE,
selection = list(target = 'column'))
}, server=FALSE)
proxy <- dataTableProxy('tableau')
observe({
replaceData(proxy, updateData(), resetPaging = TRUE )
})
output$log <- renderPrint({
updateData()
})
})
ui = shinyUI(
fluidPage(
titlePanel("My Awesome Shiny App"),
# Show a plot of the generated distribution
mainPanel(
DT::DTOutput("tableau"),
# Show log
verbatimTextOutput("log")
)
)
)
runApp(list(ui = ui, server = server))

Resources