Add and delete rows of DT Datatable in R Shiny - r

I'm trying to add a "save inputs" feature to my Shiny app where the saved inputs would be saved in a DT data table. If a user clicks an Add button, the inputs would be appended to a data table. A user then can delete a row from this data table by selecting a row and clicking the Delete button. I also need to have this table's values be saved as a global variable so it stays persistent across all sessions.
The example code is shown below. When I close the session, the table (this_table) is correctly updated, however, those changes don't appear realtime during the app. I've tried putting both of these input buttons in an eventReactive function, but this did not work when one of the buttons was selected more than once.
Any ideas?
Global table:
this_table = data.frame(bins = c(30, 50), cb = c(T, F))
Shiny app code:
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
sliderInput("bins",
"Number of bins:",
min = 1,
max = 50,
value = 30),
checkboxInput("cb", "T/F"),
actionButton("add_btn", "Add"),
actionButton("delete_btn", "Delete")
),
mainPanel(
DTOutput("shiny_table")
)
)
)
server <- function(input, output) {
observeEvent(input$add_btn, {
t = rbind(data.frame(bins = input$bins,
cb = input$cb), this_table)
this_table <<- t
})
observeEvent(input$delete_btn, {
t = this_table
print(nrow(t))
if (!is.null(input$shiny_table_rows_selected)) {
t <- t[-as.numeric(input$shiny_table_rows_selected),]
}
this_table <<- t
})
output$shiny_table <- renderDT({
datatable(this_table, selection = 'single', options = list(dom = 't'))
})
}
shinyApp(ui = ui, server = server)

You can use reactiveVal to add server side variables that are observable and mutable at the same time. The syntax for those variables is to initialize them as
rV <- reactiveValue("init_value")
and update them with
rV("new_value")
Those variables can be accessed inside reactive contexts (basically like inputs) with
rV()
The syntax is quite unusual for R and might take time to get used to, but it is definitely the recommended way to solve issues like these. You might also want to take a look at reactiveValues for a similar functionality but with a semantic closer to the R class list.
Here is how this technique can be applied to your question
library(shiny)
library(DT)
this_table = data.frame(bins = c(30, 50), cb = c(T, F))
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
sliderInput("bins",
"Number of bins:",
min = 1,
max = 50,
value = 30),
checkboxInput("cb", "T/F"),
actionButton("add_btn", "Add"),
actionButton("delete_btn", "Delete")
),
mainPanel(
DTOutput("shiny_table")
)
)
)
server <- function(input, output) {
this_table <- reactiveVal(this_table)
observeEvent(input$add_btn, {
t = rbind(data.frame(bins = input$bins,
cb = input$cb), this_table())
this_table(t)
})
observeEvent(input$delete_btn, {
t = this_table()
print(nrow(t))
if (!is.null(input$shiny_table_rows_selected)) {
t <- t[-as.numeric(input$shiny_table_rows_selected),]
}
this_table(t)
})
output$shiny_table <- renderDT({
datatable(this_table(), selection = 'single', options = list(dom = 't'))
})
}
shinyApp(ui = ui, server = server)
Finally, I would like to add that # Vishesh Shrivastavs recommendation to use the rhandsontable package is also a viable approach, although you will definitely loose some flexibility in doing so.

Related

R shiny: choose a row to start displaying datatable

below there is a shiny app that renders a datatable using DT. Rather than have the table start the display at row 1 I'd like to have the table render with a specific top row selected by the user (using input$startRow in this example).
E.g., if the user chose 50 in the slider the first row shown in the table would be row 50 rather than row 1.
Any tips for getting a dynamic starting row appreciated.
Edit for clarity: I do not want to subset the table, I want to display to begin at input$startRow but the user could scroll up and down and still see the entire dataset (e.g., faithful in this example).
Edit 2: It looks like the issue is that the displayStart option is what I want but that there is a known bug as of May 21 with Scroller starting the display mid table.
library(shiny)
library(DT)
data("faithful")
ui <- fluidPage(
h2("Select the first row of the table to show"),
sliderInput(inputId="startRow",
label="Choose a row to start the datatable display",
min = 1,
max = 200,
value = 100,
step=5),
# show a datatable
h2("Show a table"),
dataTableOutput("table1")
)
server <- function(input, output) {
output$table1 <- renderDataTable({
# use input$startRow to begin the table display?
datatable(faithful,
extensions = "Scroller",
options = list(scrollY = 300,
scroller = TRUE))
})
}
# Run the application
shinyApp(ui = ui, server = server)
Success. Following the link in the comment. I was able to use initComplete to start the table on the row from input$startRow. This appears to work.
library(shiny)
library(DT)
data("faithful")
ui <- fluidPage(
h2("Select the first row of the table to show"),
sliderInput(inputId="startRow",
label="Choose a row to start the datatable display",
min = 1,
max = 200,
value = 10,
step=5),
# show a datatable
h2("Show a table"),
dataTableOutput("table1")
)
server <- function(input, output) {
output$table1 <- renderDataTable({
datatable(faithful,
extensions = "Scroller",
options = list(scrollY = 300,
scroller = TRUE,
initComplete = JS('function() {this.api().table().scroller.toPosition(',
input$startRow-1,');}')))})
}
shinyApp(ui = ui, server = server)
Yes, use input$startRow to begin the table display to generate the selected table.
library(shiny)
library(DT)
data("faithful")
ui <- fluidPage(
h2("Select the first row of the table to show"),
sliderInput(inputId="startRow",
label="Choose a row to start the datatable display",
min = 1,
max = 200,
value = 100,
step=5),
# show a datatable
h2("Show a table"),
dataTableOutput("table1")
)
server <- function(input, output) {
topDF <- reactive({
# use input$startRow to begin the table display
topRow <- input$startRow
selectedDf <- faithful[-(1:(topRow-1)), ]
return(selectedDf)
})
output$table1 <- renderDataTable({
datatable(topDF(),
extensions = "Scroller",
options = list(scrollY = 300,
scroller = TRUE))
})
}
# Run the application
shinyApp(ui = ui, server = server)

selectizeInput: allowing one element per group

I have a selectizeInput with some grouped elements with multiple selection. Is there an elegant way (e.g. using the options argument) of allowing just one element per group, so that a whole group will discarded (or disabled) when an element of this specific group is selected?
So far I tried it programmatically, but than the dropdown menu of the selectizeInput will be closed when updating the selectizeInput.
Minimal example:
library(shiny)
ui <- fluidPage(
selectizeInput("selInput", "Default",
choices=list(g1 = c(A="A",B="B"),
g2 = c(C="C",D="D")),
multiple=T),
selectizeInput("oneElementPerGroup", "One element per group",
choices=list(g1 = c(A="A",B="B"),
g2 = c(C="C",D="D")),
multiple=T)
)
server <- function(session, input, output) {
#Removes the corresponding groups of selected items
observeEvent(input$oneElementPerGroup, ignoreNULL = F, {
plusChoice <- input$oneElementPerGroup
names(plusChoice) <- input$oneElementPerGroup
choices <- list(g1 = c(A="A",B="B"),
g2 = c(C="C",D="D"))
if(any(input$oneElementPerGroup %in% c("A", "B"))){
choices[["g1"]] <- NULL
}
if(any(input$oneElementPerGroup %in% c("C", "D"))){
choices[["g2"]] <- NULL
}
choices$we <- plusChoice
updateSelectizeInput(session,"oneElementPerGroup",
choices = choices,
selected=input$oneElementPerGroup)
})
}
shinyApp(ui = ui, server = server)
You can use pickerInput from {shinyWidgets}. Then we can add a little javascript to do what you want. No server code is needed, very simple. Read more about the data-max-options option: https://developer.snapappointments.com/bootstrap-select/options/.
We need to add the limit to each group, not an overall limit, so we can't add it through the options argument in pickerInput, have to do it in raw HTML or use some js code to inject like what I do.
Be sure your inputId="pick" matches the id in the script #pick. Rename pick to whatever you want.
ui <- fluidPage(
shinyWidgets::pickerInput(
inputId = "pick", label = "Selected",
choices =list(g1 = c(A="A",B="B"), g2 = c(C="C",D="D")),
multiple = TRUE
),
tags$script(
'
$(function(){
$("#pick optgroup").attr("data-max-options", "1");
})
'
)
)
server <- function(input, output, session){}
shinyApp(ui, server)
updates:
If you need to update, we need to run the script again but from server. We can send js by using {shinyjs}. Imagine an observer triggers the update event.
library(shinyjs)
ui <- fluidPage(
useShinyjs(),
shinyWidgets::pickerInput(
inputId = "pick", label = "Selected",
choices =NULL,
multiple = TRUE
)
)
server <- function(input, output, session){
observe({
shinyWidgets::updatePickerInput(session, "pick", choices = list(g1 = c(A="A",B="B"), g2 = c(C="C",D="D")))
observeEvent(once = TRUE, reactiveValuesToList(session$input), {
runjs('$("#pick optgroup").attr("data-max-options", "1");')
}, ignoreInit = TRUE)
})
}
shinyApp(ui, server)

Combining editable DT with `add row` functionality

I am trying to address two specific needs:
- User needs to be able to add a row to the DataTable
- Datatable needs to be editable
Currently, I have the 'add row' functionality set up, as well as the editable functionality, but the edits don't persist table sorting, or when new rows are added.
When you run the below code, notice how you can add/delete rows, but when you edit a field and then add/delete a row, you lose the edits.
I am hoping someone can help me combine these two requirements! Appreciate the help.
this_table = data.frame(bins = c(30, 50), cb = c(T, F))
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
sliderInput("bins",
"Number of bins:",
min = 1,
max = 50,
value = 30),
checkboxInput("cb", "T/F"),
actionButton("add_btn", "Add"),
actionButton("delete_btn", "Delete")
),
mainPanel(
DTOutput("shiny_table")
)
)
)
server <- function(input, output) {
this_table <- reactiveVal(this_table)
observeEvent(input$add_btn, {
t = rbind(data.frame(bins = input$bins,
cb = input$cb), this_table())
this_table(t)
})
observeEvent(input$delete_btn, {
t = this_table()
print(nrow(t))
if (!is.null(input$shiny_table_rows_selected)) {
t <- t[-as.numeric(input$shiny_table_rows_selected),]
}
this_table(t)
})
output$shiny_table <- renderDT({
datatable(this_table(), selection = 'single', options = list(dom = 't'), editable = T)
})
}
shinyApp(ui = ui, server = server)
You will need to use observeEvent to update the table data after an edit. You can extract the relevant table row, column, and newly edited value from input$shiny_table_cell_edit. Try adding this code to your server function:
observeEvent(input$shiny_table_cell_edit, {
info <- input$shiny_table_cell_edit
edit_row <- info$row
edit_col <- info$col
edit_value <- info$value
t = this_table()
t[edit_row,edit_col] <- edit_value
this_table(t)
})

Shiny Modules with Observes and reactiveValues

I have been trying to reconstruct the following simplistic Shiny app using modules since I believe that will be the best way to organize this code inside a much larger application where I will use these kinds of linked-slider-numeric inputs in many places.
However, I cannot figure out how to achieve the same kind of functionality from within a module.
Here's an example app that works exactly as intended, but not using modules:
library(shiny)
# Let's build a linked Slider and Numeric Input
server <- function(input, output) {
values <- reactiveValues(numval=1)
observe({
values$numval <- input$slider
})
observe({
values$numval <- input$number
})
output$slide <- renderUI({
sliderInput(
inputId = 'slider'
,label = 'SN'
,min = 0
,max = 10
,value = values$numval
)})
output$num <- renderUI({
numericInput(
inputId = 'number'
,label = 'SN'
,value = values$numval
,min = 0
,max = 10
)
})
}
ui <- fluidPage(
uiOutput('slide'),
uiOutput('num')
)
shinyApp(ui, server)
Here's my attempt. (Note that "mortalityRate" and associated strings are just an example of the variable name(s) I'll be using later). I have tried several variations on this attempt, but inevitably I get errors, usually indicating I'm doing something that can only be done inside a reactive context:
numericSliderUI <- function(id, label = "Enter value", min = 1, max = 40, value) {
ns <- NS(id)
tagList(
sliderInput(inputId = paste0(ns(id), "Slider"), label = label, min = min, max = max, value = value),
numericInput(inputId = paste0(ns(id), "Numeric"), label = label, min = min, max = max, value = value)
)
}
numericSlider <-
function(input,
output,
session,
value,
mortalityRateSlider,
mortalityRateNumeric
) {
values <- reactiveValues(mortalityRate = value())
observe({
values[['mortalityRate']] <- mortalityRateSlider()
})
observe({
values[['mortalityRate']] <- mortalityRateNumeric()
})
return( reactive( values[['mortalityRate']] ) )
}
library(shiny)
# source("modules.R") # I keep the modules in a separate file, but they're just pasted above for convenience here on StackOverflow.
ui <- fluidPage(
uiOutput('mortalityRate')
)
server <- function(input, output) {
values <- reactiveValues(mortalityRate = 1)
mortalityRateValue <- callModule(
numericSlider,
id = 'mortalityRate',
value = values[['mortalityRate']],
mortalityRateSlider = reactive( input$mortalityRateSlider ),
mortalityRateNumeric = reactive( input$mortalityRateNumeric )
)
values[['mortalityRate']] <- reactive( mortalityRateValue() )
output$mortalityRate <- renderUI(numericSliderUI('mortalityRate', value = values[['mortalityRate']]))
}
shinyApp(ui = ui, server = server)
I know that I must be doing something wrong with the reactiveValues and the way I'm using the observe statements inside the module, but this is my best attempt at using the module structure, so any help figuring out what I'm doing wrong would be very helpful.
Here is working code. There are a variety of changes, so I'll direct you to this Github page that also sets up a structure for using renderUI with modules. In general, I think the problems in your code involved trying to define reactive values inside the callModule function, and in passing the values of the sliders and numeric box back and forth.
Other features of using modules are that in your actual UI call, you need to call the UI module, where in turn you can call uiOutput. Inside renderUI is where you can set up the inputs. Additionally, inside modules you don't need the session namespaces, but you do need to wrap those ids in session$ns() to ensure they work across modules.
UI and Server Modules:
numericSliderUI <- function(id) {
ns <- NS(id)
uiOutput(ns('mortalityRate'))
}
numericSlider <- function(input, output, session) {
values <- reactiveValues(mortalityRate = 1)
observe({
values[['mortalityRate']] <- input$Slider
})
observe({
values[['mortalityRate']] <- input$Numeric
})
output$mortalityRate <- renderUI(
tagList(
sliderInput(inputId = session$ns("Slider"), label = "Enter value:", min = 1, max = 40, value = values[['mortalityRate']]),
numericInput(inputId = session$ns("Numeric"), label = "Enter value:", min = 1, max = 40, value = values[['mortalityRate']])
)
)
return(list(value = reactive({values[['mortalityRate']]})))
}
UI and Server functions:
ui <- fluidPage(
numericSliderUI('mortalityRate')
)
server <- function(input, output, session) {
mortalityRateValue <- callModule(numericSlider, 'mortalityRate')
}
shinyApp(ui = ui, server = server)

Iteratively loading and filtering table [R] [Shiny]

I'm having trouble iteratively loading and filtering a datatable in Shiny. The ideal workflow would be as follows:
User pushes button to confirm loading data
Data is retrieved from MySql query. Note this should only occur once
(optional) filter buttons/sliders become visible/available
User interacts with buttons/sliders to filter datatable
1 and 2 work fine, but I am having particular issue with 4 (also any input for 3 would be appreciated as well).
The initial code that is not working is as follows:
get_data=function(){ # note that this is for sample purpose, real function is MySQL query
df=data.frame(x=1:10,Age=1:100)
print("loading data...")
return(df)
}
ui = bootstrapPage(
fluidPage(
fluidRow(
actionButton(
inputId = "confirm_button",
label = "Confirm"
)
)
,
fluidRow(
column(4,
sliderInput("slider_age", label = h4("Age"), min = 0,
max = 100, value = c(0, 100))
)
),
hr(),
fluidRow(
DT::dataTableOutput("all_background_table")
)
)
)
server = function(input, output){
observeEvent(input$confirm_button, {
req(input$confirm_button)
output$all_background_table <- DT::renderDataTable({
all_background=get_data() # <- MySQL function to laod data
# if all_background filter function put here:
#--> data is re-loaded by MySQL query
# if all_background filter function is put here surrounded by observeEvent(input$slider_age, {...:
#--> there is no change when input$slider_age is changed
datatable(all_background,
rownames = FALSE,
style = "bootstrap")
})
})
observeEvent(input$slider_age, {
## this will throw an error requiring all_background
#--> Error in observeEventHandler: object 'all_background' not found
req(input$confirmation_load_pts)
all_background=all_background[(all_background$Age > as.numeric(input$slider_age[1]) & all_background$Age < as.numeric(input$slider_age[2])),]
})
}
shinyApp(ui, server)
I am not sure about get_data(), but I will be using df to make it easier. With eventReactive you can create a new data frame after using the slider and only after clicking on the confirm button. Your observeEventwould not be necessary for this scenario.
library(shiny)
library(DT)
get_data=function(){ # note that this is for sample purpose, real function is MySQL query
df=data.frame(x=1:10,Age=1:100)
print("loading data...")
return(df)
}
ui = bootstrapPage(
fluidPage(
fluidRow(
actionButton(
inputId = "confirm_button",
label = "Confirm"
)
)
,
fluidRow(
column(4,
sliderInput("slider_age", label = h4("Age"), min = 0,
max = 100, value = c(0, 100))
)
),
hr(),
fluidRow(
DT::dataTableOutput("all_background_table")
)
)
)
server = function(input, output){
test <- eventReactive(input$confirm_button, {
df=get_data()
})
observeEvent(input$confirm_button, {
output$all_background_table <- DT::renderDataTable({
df=test()
all_background2=df[(df$Age > as.numeric(input$slider_age[1]) & df$Age < as.numeric(input$slider_age[2])),]
datatable(all_background2,
rownames = FALSE,
style = "bootstrap")
})
})
}
shinyApp(ui, server)

Resources