In the below code, I took one of the examples from https://community.rstudio.com/t/shiny-app-with-dynamic-number-of-datatables/2405/4 for dynamically adding tables. The example used tables rendered with DT and I made minor modifications to use it for rhandsontable.
However, I'm having trouble making the tables independent of one another. When adding a new table, it should be "seeded" with the default values per dataframe data1 and its related rowNames1, but thereafter they should be independent as illustrated below. I'm fairly sure the solution has something to do with creating a dynamic equivalent of the reactive uiTable1 used in the code for holding table values, but I don't know how to do this.
Any ideas for accomplishing this?
Inputs into the base (master) and added tables will be used elsewhere in the full code this is intended for.
Code:
library(rhandsontable)
library(shiny)
rowNames1 <- c('A','B','C','Sum')
data1 <- data.frame(row.names = rowNames1, 'Col 1' = c(1,1,0,2), check.names = FALSE)
ui <- fluidPage(
rHandsontableOutput('hottable1'), # undeletable base table
actionButton("addTbl", "Add table"), # adds new table
tags$div(id = "placeholder")
)
server <- function(input, output) {
uiTable1 <- reactiveVal(data1) # undeletable base table
rv <- reactiveValues() # used for dynamic table add/removal
# records changes to base table and will need same for added tables:
observeEvent(input$hottable1,{uiTable1(hot_to_r(input$hottable1))})
output$hottable1 <- renderRHandsontable({
rhandsontable(uiTable1(),rowHeaderWidth = 100, useTypes = TRUE)
})
# adds column summation to last row of table, will need for all added tables too:
observe({
req(input$hottable1)
DF <- hot_to_r(input$hottable1)
DF[setdiff(rowNames1, "Sum"),]
DF["Sum",] <- colSums(DF[setdiff(rowNames1, "Sum"),, drop = FALSE], na.rm = TRUE)
uiTable1(DF)
})
# dynamically add/remove tables:
observeEvent(input$addTbl, {
divID <- gsub("\\.", "", format(Sys.time(), "%H%M%OS3"))
dtID <- paste0(divID, "DT")
btnID <- paste0(divID, "rmv")
insertUI(
selector = "#placeholder",
ui = tags$div(id = divID,
actionButton(btnID, "Remove table", class = "pull-left btn btn-danger"),
rHandsontableOutput(dtID),
hr()
)
)
output[[dtID]] <- renderRHandsontable({
rhandsontable(uiTable1(),rowHeaderWidth = 100, useTypes = TRUE)
})
# remove table from the app when remove button clicked
observeEvent(input[[btnID]], {
removeUI(selector = paste0("#", divID))
rv[[divID]] <- NULL
}, ignoreInit = TRUE, once = TRUE)
})
}
shinyApp(ui,server)
We can use reactiveValues to store the information of each new table. These tables will start with the values of the base table available at that time. Afterwards they will stop reacting to changes inside the main table.
First we create the base table uiTable1 <- reactiveValues(table_base = data1) # undeletable base table
And finally all the subsequent tables will be created as uiTable1[[glue("{divID}table")]] <- uiTable$table_base
library(rhandsontable)
library(shiny)
library(glue)
rowNames1 <- c("A", "B", "C", "Sum")
data1 <- data.frame(row.names = rowNames1, "Col 1" = c(1, 1, 0, 2), check.names = FALSE)
ui <- fluidPage(
rHandsontableOutput("hottable1"), # undeletable base table
actionButton("addTbl", "Add table"), # adds new table
tags$div(id = "placeholder")
)
server <- function(input, output) {
uiTable1 <- reactiveValues(table_base = data1) # undeletable base table
rv <- reactiveValues() # used for dynamic table add/removal
# records changes to base table and will need same for added tables:
observeEvent(input$hottable1, {
uiTable1$table_base <- hot_to_r(input$hottable1)
})
output$hottable1 <- renderRHandsontable({
rhandsontable(uiTable1$table_base, rowHeaderWidth = 100, useTypes = TRUE)
})
# adds column summation to last row of table, will need for all added tables too:
observe({
req(input$hottable1)
DF <- hot_to_r(input$hottable1)
DF[setdiff(rowNames1, "Sum"), ]
DF["Sum", ] <- colSums(DF[setdiff(rowNames1, "Sum"), , drop = FALSE], na.rm = TRUE)
uiTable1$table_base <- DF
})
# dynamically add/remove tables:
observeEvent(input$addTbl, {
divID <- gsub("\\.", "", format(Sys.time(), "%H%M%OS3"))
dtID <- paste0(divID, "DT")
btnID <- paste0(divID, "rmv")
# capture the current state of the main table
uiTable1[[glue("{divID}table")]] <- uiTable1$table_base
insertUI(
selector = "#placeholder",
ui = tags$div(
id = divID,
actionButton(btnID, "Remove table", class = "pull-left btn btn-danger"),
rHandsontableOutput(dtID),
hr()
)
)
output[[dtID]] <- renderRHandsontable({
req(uiTable1[[glue("{divID}table")]])
rhandsontable(uiTable1[[glue("{divID}table")]], rowHeaderWidth = 100, useTypes = TRUE)
})
# adds column summation to last row of table, will need for all added tables too:
observeEvent(input[[dtID]], {
DF <- hot_to_r(input[[dtID]])
DF[setdiff(rowNames1, "Sum"), ]
DF["Sum", ] <- colSums(DF[setdiff(rowNames1, "Sum"), , drop = FALSE], na.rm = TRUE)
uiTable1[[glue("{divID}table")]] <- DF # update the table with the sum
})
# remove table from the app when remove button clicked
observeEvent(input[[btnID]],
{
removeUI(selector = paste0("#", divID))
rv[[divID]] <- NULL
uiTable1[[glue("{divID}table")]] <- NULL
},
ignoreInit = TRUE,
once = TRUE
)
})
}
shinyApp(ui, server)
Related
I'm running two expandable rhandsontables who should always have the same number of columns and the same column headers, though the rows differ. One of the tables (myDF1 rendered in "hottable1") is the master where the user adds/deletes columns from the tabPanel() housing that table and the second table (myDF2 rendered in "hottable2") parrots the first table in terms of number of columns and column headers but is placed in a separate tabPanel() reacting to the action buttons in the first tabPanel(). The strange thing is, this linked column addition/deletion works fine when the two tables are rendered in Shiny's fluidPage() or when using Shiny's pageWithSidebar() the two tables are housed in the same tabPanel(). However, when the two tables are in separate tabPanels() (as shown in the code below), column addition works fine but the second table in tab "Slave" crashes when deleting columns from tab "Master".
I must be missing something very basic about tabPanels(). What am I doing wrong?
I've always assumed reactivity cuts across tabPanels().
Code:
library(dplyr)
library(rhandsontable)
library(shiny)
myDF1 <- data.frame('Series 1' = c(1,24,0), check.names = FALSE)
rownames(myDF1) <- c('Term A','Term B','Term C')
myDF2 <- data.frame('Series 1' = c(20,15), check.names = FALSE)
rownames(myDF2) <- c('Boy','Girl')
ui <- pageWithSidebar(
headerPanel(""),sidebarPanel(""),
mainPanel(
tabsetPanel(
tabPanel("Master table", hr(),
rHandsontableOutput('hottable1'),br(),
actionButton("addSeries", "Add", width = 80),
fluidRow(
column(2,actionButton("delSeries","Delete", width = 80)),
column(3,uiOutput("delSeries2"))
),
),
tabPanel("Slave table", hr(),rHandsontableOutput('hottable2'))
)
)
)
server <- function(input, output) {
emptyTbl1 <- reactiveVal(myDF1)
emptyTbl2 <- reactiveVal(myDF2)
observeEvent(input$hottable1, {emptyTbl1(hot_to_r(input$hottable1))})
observeEvent(input$hottable2, {emptyTbl2(hot_to_r(input$hottable2))})
output$hottable1 <- renderRHandsontable({
rhandsontable(emptyTbl1(),rowHeaderWidth = 100, useTypes = TRUE)%>%
hot_context_menu(allowRowEdit = FALSE, allowColEdit = FALSE)
})
output$hottable2 <- renderRHandsontable({
rhandsontable(emptyTbl2(),rowHeaderWidth = 100, useTypes = TRUE)%>%
hot_context_menu(allowRowEdit = FALSE, allowColEdit = FALSE)
})
observeEvent(input$addSeries, {
newCol1 <- data.frame(c(1,24,0))
newCol2 <- data.frame(c(20,15))
names(newCol1) <- paste("Series", ncol(hot_to_r(input$hottable1)) + 1)
names(newCol2) <- paste("Series", ncol(hot_to_r(input$hottable2)) + 1)
emptyTbl1(cbind(emptyTbl1(), newCol1))
emptyTbl2(cbind(emptyTbl2(), newCol2))
})
observeEvent(input$delSeries3, {
tmp1 <- emptyTbl1()
tmp2 <- emptyTbl2()
if(ncol(tmp1) > 1){
delCol <- input$delSeries3
tmp1 <- tmp1[ , !(names(tmp1) %in% delCol), drop = FALSE]
tmp2 <- tmp2[ , !(names(tmp2) %in% delCol), drop = FALSE]
newNames <- sprintf("Series %d",seq(1:ncol(tmp1)))
names(tmp1) <- newNames
names(tmp2) <- newNames
emptyTbl1(tmp1)
emptyTbl2(tmp2)
}
})
output$delSeries2 <-
renderUI(
selectInput("delSeries3",
label = NULL,
choices = colnames(hot_to_r(input$hottable1)),
selected = "", width = '100px',
multiple = TRUE)
)
}
shinyApp(ui,server)
The below "resolved code" resolves the issue. The few changes from OP code are commented below and are summarized as follows:
Insert outputOptions(output, 'hottable2', suspendWhenHidden = FALSE) in the server() section in order to update the 2nd table located in a separate tab panel from the action buttons driving that table from another tab panel; allows reactivity to instantly cross tab panels that aren't being viewed
Even with the above fix, the "hottable2" table had to be clicked on in order to completely render it. R whiz Stéphane Laurent pointed out that there's a known bug in Shiny when re-rendering this way, his html solution is accordingly included and commented in the revised code below for the rhandsontable() function used for "hottable2" in the server() section
Resolved code:
library(dplyr)
library(rhandsontable)
library(shiny)
myDF1 <- data.frame('Series 1' = c(1,24,0), check.names = FALSE)
rownames(myDF1) <- c('Term A','Term B','Term C')
myDF2 <- data.frame('Series 1' = c(20,15), check.names = FALSE)
rownames(myDF2) <- c('Boy','Girl')
ui <- pageWithSidebar(
headerPanel(""),sidebarPanel(""),
mainPanel(
tabsetPanel(
tabPanel("Master table", hr(),
rHandsontableOutput('hottable1'),br(),
actionButton("addSeries", "Add", width = 80),
fluidRow(
column(2,actionButton("delSeries","Delete", width = 80)),
column(3,uiOutput("delSeries2"))
),
),
tabPanel("Slave table", hr(),rHandsontableOutput('hottable2'))
)
)
)
server <- function(input, output) {
emptyTbl1 <- reactiveVal(myDF1)
emptyTbl2 <- reactiveVal(myDF2)
observeEvent(input$hottable1, {emptyTbl1(hot_to_r(input$hottable1))})
observeEvent(input$hottable2, {emptyTbl2(hot_to_r(input$hottable2))})
output$hottable1 <- renderRHandsontable({
rhandsontable(emptyTbl1(),rowHeaderWidth = 100, useTypes = TRUE)%>%
hot_context_menu(allowRowEdit = FALSE, allowColEdit = FALSE) %>%
hot_cols(colWidths = 80)
})
output$hottable2 <- renderRHandsontable({
rhandsontable(emptyTbl2(),rowHeaderWidth = 100, width = 800, height = 450,useTypes = TRUE)%>%
hot_context_menu(allowRowEdit = FALSE, allowColEdit = FALSE) %>%
hot_cols(colWidths = 80) %>%
# next section of html addresses issue of correcltly rendering the slave table:
htmlwidgets::onRender(
"function(el, x){
var hot = this.hot;
$('a[data-value=\"Slave table\"').on('click', function(){
setTimeout(function(){ hot.render(); }, 200);
});
}"
)
})
observeEvent(input$addSeries, {
newCol1 <- data.frame(c(1,24,0))
newCol2 <- data.frame(c(20,15))
names(newCol1) <- paste("Series", ncol(hot_to_r(input$hottable1)) + 1)
names(newCol2) <- paste("Series", ncol(hot_to_r(input$hottable2)) + 1)
emptyTbl1(cbind(emptyTbl1(), newCol1))
emptyTbl2(cbind(emptyTbl2(), newCol2))
})
observeEvent(input$delSeries3, {
tmp1 <- emptyTbl1()
tmp2 <- emptyTbl2()
if(ncol(tmp1) > 1){
delCol <- input$delSeries3
tmp1 <- tmp1[ , !(names(tmp1) %in% delCol), drop = FALSE]
tmp2 <- tmp2[ , !(names(tmp2) %in% delCol), drop = FALSE]
newNames <- sprintf("Series %d",seq(1:ncol(tmp1)))
names(tmp1) <- newNames
names(tmp2) <- newNames
emptyTbl1(tmp1)
emptyTbl2(tmp2)
}
})
output$delSeries2 <-
renderUI(
selectInput("delSeries3",
label = NULL,
choices = colnames(hot_to_r(input$hottable1)),
selected = "", width = '100px',
multiple = TRUE)
)
outputOptions(output, 'hottable2', suspendWhenHidden = FALSE) # this updates slave panel even when hidden
}
shinyApp(ui,server)
The code posted at the bottom allows the user to dynamically add and delete tables. You'll see when adding tables that their column headers are automatically sequentially numbered "Col 1", "Col 2", etc. Remaining tables are automatically renumbered after any table is deleted.
How would I capture, in a vector, the nested names of all of these tables ("Col 1", "Col 2", for example)? As shown in the illustration below, a screenshot of the R studio console when running the code and clicking the "Add table" button once. I use print(tables_list) to see the contents of the list. I just don't know how to move around that dynamic list.
I'm having trouble understanding how to subset a dynamic list like this one. I also wonder if I'll be able to reference other values in the list by referring to these element names of Col 1, Col 2, etc.
Illustration:
Code:
library(shiny)
library(rhandsontable)
data1 <- data.frame(row.names = c("A","B","C","Sum"),"Col 1"=c(1,1,0,2),check.names=FALSE)
ui <- fluidPage(
br(),
actionButton("addTbl","Add table"),
br(),br(),
tags$div(id = "placeholder",
tags$div(
style = "display: inline-block",
rHandsontableOutput("hottable1")
)
)
)
server <- function(input, output, session) {
uiTbl <- reactiveValues(div_01_tbl = data1)
rv <- reactiveValues()
observeEvent(input$hottable1, {uiTbl$div_01_tbl <- hot_to_r(input$hottable1)})
observe({
divID <- paste0("div_", sprintf("%02d", input$addTbl+1))
dtID <- paste0(divID, "_DT")
btnID <- paste0(divID, "_rmv")
uiTbl[[paste0(divID,"_tbl")]] <- data1 # captures initial dataframe values
insertUI(
selector = "#placeholder",
ui = tags$div(
id = divID,
style = "display:inline-block;",
rHandsontableOutput(dtID),
actionButton(btnID, "Delete", class = "pull-left btn btn-danger"),
)
)
output[[dtID]] <- renderRHandsontable({
req(uiTbl[[paste0(divID,"_tbl")]])
rhandsontable(uiTbl[[paste0(divID,"_tbl")]], useTypes = TRUE)
})
observeEvent(input[[dtID]], {uiTbl[[paste0(divID,"_tbl")]] <- hot_to_r(input[[dtID]])})
observeEvent(input[[btnID]],{
removeUI(selector = paste0("#", divID))
rv[[divID]] <- NULL
uiTbl[[paste0(divID,"_tbl")]] <- NULL
},
ignoreInit = TRUE,
once = TRUE
)
})
observe({
tables_list <- reactiveValuesToList(uiTbl)
tables_list <- tables_list[order(names(tables_list))]
table_lengths <- lengths(tables_list)
cumsum_table_lengths <- cumsum(table_lengths)[table_lengths != 0L]
for(i in seq_along(cumsum_table_lengths)){
names(uiTbl[[names(cumsum_table_lengths[i])]]) <- paste("Col", cumsum_table_lengths[i])
}
print(tables_list) ### PRINT ###
})
}
shinyApp(ui, server)
We can create the needed vector in the observe() call and pass it to updateSelectizeInput if you need it somewhere else you could pass it to a reactiveVal instead:
library(shiny)
library(rhandsontable)
data1 <- data.frame(row.names = c("A","B","C","Sum"),"Col 1"=c(1,1,0,2),check.names=FALSE)
ui <- fluidPage(
br(),
actionButton("addTbl","Add table"),
br(), br(),
tags$div(id = "placeholder",
tags$div(
style = "display: inline-block",
rHandsontableOutput("hottable1")
)
),
br(),
selectizeInput(inputId = "select_deletion",
label = "Select deletion",
choices = NULL,
selected = NULL,
multiple = TRUE)
)
server <- function(input, output, session) {
uiTbl <- reactiveValues(div_01_tbl = data1)
rv <- reactiveValues()
observeEvent(input$hottable1, {uiTbl$div_01_tbl <- hot_to_r(input$hottable1)})
observe({
divID <- paste0("div_", sprintf("%02d", input$addTbl+1))
dtID <- paste0(divID, "_DT")
btnID <- paste0(divID, "_rmv")
uiTbl[[paste0(divID,"_tbl")]] <- data1 # captures initial dataframe values
insertUI(
selector = "#placeholder",
ui = tags$div(
id = divID,
style = "display:inline-block;",
rHandsontableOutput(dtID),
actionButton(btnID, "Delete", class = "pull-left btn btn-danger"),
)
)
output[[dtID]] <- renderRHandsontable({
req(uiTbl[[paste0(divID,"_tbl")]])
rhandsontable(uiTbl[[paste0(divID,"_tbl")]], useTypes = TRUE)
})
observeEvent(input[[dtID]], {uiTbl[[paste0(divID,"_tbl")]] <- hot_to_r(input[[dtID]])})
observeEvent(input[[btnID]],{
removeUI(selector = paste0("#", divID))
rv[[divID]] <- NULL
uiTbl[[paste0(divID,"_tbl")]] <- NULL
},
ignoreInit = TRUE,
once = TRUE
)
})
observe({
tables_list <- reactiveValuesToList(uiTbl)
tables_list <- tables_list[order(names(tables_list))]
table_lengths <- lengths(tables_list)
cumsum_table_lengths <- cumsum(table_lengths)[table_lengths != 0L]
table_names <- paste("Col", cumsum_table_lengths)
for(i in seq_along(cumsum_table_lengths)){
names(uiTbl[[names(cumsum_table_lengths[i])]]) <- table_names[i]
}
# print(tables_list) ### PRINT ###
# browser() ### use browser() to analyse your observer
freezeReactiveValue(input, "select_deletion")
updateSelectizeInput(session, inputId = "select_deletion", choices = table_names, selected = NULL)
})
}
shinyApp(ui, server)
PS: Please remember to avoid <<- and renderUI wherever you can.
Below is one long-winded way of doing this (also using dplyr for a mutate()), by reverting back to my familiarity with data frames. See the additions of "tmp" objects in the below which replaces the last observe() in the OP. Note that rather than using print() to see the vector as I did in my OP, I send it to the global environment via "tmp.R" for reviewing more complicated input sequences. I hope better solutions to this are posted! I'd like to learn how to easily navigate nested lists. Also, I leave in, but comment-out, object "test1" which is a good way to view the contents of the list neatly organized as a dataframe.
observe({
tables_list <- reactiveValuesToList(uiTbl)
tables_list <- tables_list[order(names(tables_list))]
table_lengths <- lengths(tables_list)
cumsum_table_lengths <- cumsum(table_lengths)[table_lengths != 0L]
for(i in seq_along(cumsum_table_lengths)){
names(uiTbl[[names(cumsum_table_lengths[i])]]) <- paste("Col", cumsum_table_lengths[i])
}
tmp <- data.frame(cumsum_table_lengths)
tmp <- data.frame(origTbl = rownames(tmp), tblCnt = tmp[,1])
tmp <- tmp %>% mutate(tblCode = paste("Col",tblCnt))
tmp <- tmp[,3]
tmp.R <<- tmp
# test1 <- as.data.frame(do.call(cbind, tables_list)) ## this is also useful
})
The below code is "dynamic" in allowing the user to render additional independent tables with the click of the action button. For the sake of brevity, the below code has certain features removed such as table deletion, table column summation, etc.
When running the code note how added tables are rendered vertically. How could I render added tables horizontally instead of vertically?
I ordinarily render horizontally by using Shiny's grid layout, setting up columns, etc. But since these tables are dynamically rendered with the number of tables an unknown, I don't know how to do this.
Code:
library(rhandsontable)
library(shiny)
rowNames1 <- c("A", "B", "C", "Sum")
data1 <- data.frame(row.names = rowNames1, "Col 1" = c(1, 1, 0, 2), check.names = FALSE)
ui <- fluidPage(
rHandsontableOutput("hottable1"),
actionButton("addTbl", "Add table"),
tags$div(id = "placeholder")
)
server <- function(input, output) {
uiTbl1 <- reactiveValues(base = data1)
observeEvent(input$hottable1,{uiTbl1$base <- hot_to_r(input$hottable1)})
output$hottable1 <- renderRHandsontable({
rhandsontable(uiTbl1$base, rowHeaderWidth = 100, useTypes = TRUE)
})
observeEvent(input$addTbl, {
divID <- gsub("\\.", "", format(Sys.time(), "%H%M%OS3"))
dtID <- paste0(divID, "DT")
uiTbl1[[paste0(divID,"tbl")]] <- data1
insertUI(
selector = "#placeholder",
ui = tags$div(
id = divID,
rHandsontableOutput(dtID),
hr()
)
)
output[[dtID]] <- renderRHandsontable({
req(uiTbl1[[paste0(divID,"tbl")]])
rhandsontable(uiTbl1[[paste0(divID,"tbl")]], rowHeaderWidth = 100, useTypes = TRUE)
})
})
}
shinyApp(ui, server)
Try this:
insertUI(
selector = "#placeholder",
ui = tags$div(
id = divID,
style = "display: inline-block;",
rHandsontableOutput(dtID),
hr()
)
)
Not tested... perhaps you'll have to set the width.
I am building app where a user can make edits to a datatable and the hit a button to reflect the changes in a non-editable copy of this datatable (in the final project, I will need to have two datasets that need to be matched manually), but for now this small MWE shows the problem I have with making a copy of the reactive table in which changes can be made, without changing the data of the original reactive table. I would like to make this app work, where you click edit a cell in the table dat_joined$data/output$mytable and that those changes do reflect in a new table mydf$data/output$table2. To do mydf$data initially (before any changes are made) needs to be a copy of dat_joined$data This is a follow up on this question and answer: how to make a copy of a reactive value in shiny server function
library(shiny)
library(DT)
library(shinyWidgets)
library(tidyverse)
# create master dataframe
dat_total <- tibble(ID_1 = 1:10, names = letters[1:10],
ID_2 = 11:20, names_2 = LETTERS[c(3:5, 1, 2, 6:8, 10, 9)])
shinyApp(
ui = fluidPage(
title = 'Radio button and a dropdown manue ',
sliderInput("n_rows_table", "Number of rows:",
min = 0, max = 10,
value = 5),
actionBttn(
inputId = "button_1",
label = "Make tables",
size = "sm",
color = "warning"
),
DT::dataTableOutput("mytable"),
actionBttn(
inputId = "button_2",
label = "Process",
size = "sm",
color = "success"),
DT::dataTableOutput("table2")),
server = function(input, output, session) {
# set up reactive values
dat_left <- reactiveValues(data=NULL)
dat_right <- reactiveValues(data=NULL)
dat_joined <- reactiveValues(data=NULL)
# create reactive daraframe
dat <- eventReactive(input$button_1, {
dat_total[1:input$n_rows_table, ] %>%
rowid_to_column()})
# Split the data into a right and a left set
observe({
dat_left$data <- dat() %>%
select(rowid, ID_1, names)
})
observe({
dat_right$data <- dat() %>%
select(rowid, ID_2, names_2,ID_1)
})
# join these again
# This is needed because my actual app will
# be used to manually match 2 datasets
observe({
if (is.null( dat_right$data )) {
NULL
}else{
dat_joined$data <- left_join(dat_left$data,
dat_right$data,
by = "rowid")
}
})
# Print the the datasets
output$mytable <- renderDT({
datatable(dat_joined$data ,
rownames = F,
editable = "cell")
})
# I want to make a copy of the dat_joined$data dataset into dat$mydf
# none of these function as expected
#mydf <- reactiveValues(data=isolate(dat_joined$data))
#mydf <- reactiveValues(data=local(dat_joined$data))
#mydf <- reactiveValues(data=dat_joined$data)
#mydf <- reactiveValues(data=NULL)
# This works, but only saves the cells to w
mydf <- reactiveValues(data=matrix(NA, nrow=10, ncol = 5))
# Ideally the computation only happens when this both an edit is made
# and the button is pressed (now I need to press it between every edit)
# validate_event <- reactive({
# req(input$mytable_cell_edit) & req(input$button_2)
# })
#observeEvent(input$button_2validate_event(), { DOes not work
observeEvent(input$button_2,{
info = input$mytable_cell_edit
str(info)
i = info$row
j = info$col
v = info$value
mydf$data[i, j] <- DT::coerceValue(v, mydf$data[i, j])
})
# print
output[["table2"]] <- renderDT({
datatable(mydf$data)
})
}
)
Any changes you make in the top table is reflected in the bottom table after you press the button "Process". Try this
library(shiny)
library(DT)
library(shinyWidgets)
library(tidyverse)
# create master dataframe
dat_total <- tibble(ID_1 = 1:10, names = letters[1:10],
ID_2 = 11:20, names_2 = LETTERS[c(3:5, 1, 2, 6:8, 10, 9)])
shinyApp(
ui = fluidPage(
title = 'Radio button and a dropdown manue ',
sliderInput("n_rows_table", "Number of rows:",
min = 0, max = 10,
value = 5),
actionBttn(
inputId = "button_1",
label = "Make tables",
size = "sm",
color = "warning"
),
DT::dataTableOutput("mytable"),
actionBttn(
inputId = "button_2",
label = "Process",
size = "sm",
color = "success"),
DT::dataTableOutput("table2")),
server = function(input, output, session) {
# set up reactive values
dat_left <- reactiveValues(data=NULL)
dat_right <- reactiveValues(data=NULL)
dat_joined <- reactiveValues(data=NULL)
dfon <- reactiveValues(top=NULL,
bottom=NULL)
# create reactive daraframe
dat <- eventReactive(input$button_1, {
dat_total[1:input$n_rows_table, ] %>%
rowid_to_column()})
# Split the data into a right and a left set
observe({
req(dat())
dat_left$data <- dat() %>%
dplyr::select(rowid, ID_1, names)
})
observe({
req(dat())
dat_right$data <- dat() %>%
dplyr::select(rowid, ID_2, names_2,ID_1)
})
# join these again
# This is needed because my actual app will
# be used to manually match 2 datasets
observe({
req(dat())
if (!is.null( dat_right$data )) {
dat_joined$data <- left_join(dat_left$data,
dat_right$data,
by = "rowid")
}
})
observe({ ###assign your orig data to a reactiveValues object
req(dat_joined$data)
if (!is.null(dat_joined$data)) {
dfon$top <- dat_joined$data
}
})
# Print the the datasets
output$mytable <- renderDT({
datatable(dfon$top,
rownames = F,
editable = "cell")
})
# Ideally the computation only happens when this both an edit is made
# and the button is pressed (now I need to press it between every edit)
observeEvent(input$mytable_cell_edit, {
info = input$mytable_cell_edit
str(info)
#i = info$row
#j = info$col + 1 # offset by 1
#v = info$value
#dfon$top[i, j] <<- DT::coerceValue(v, dfon$top[i, j])
dfon$top <<- editData(dfon$top, info)
})
observeEvent(input$button_2,{
dfon$bottom <- dfon$top
output$table2 <- renderDT({
datatable(dfon$bottom)
})
})
## further editing of dfon$bottom is performed below...with...observeEvent(input$table2_cell_edit, {...
}
)
In the output below, I have entered cccc for 3rd element in names column, but I have not clicked on the button Process. Therefore, the edited cell is not reflected in the bottom table.
I am populating a table by using Insert UI elements. I also want to delete both table entries and the inserted panels by using the remove UI elements.
I could delete the panels but as you can see in my demo App the corresponding table values are not deleted and the length of the table remains the same even after clicking the delete button.
How can I delete both the panels and their corresponding table values at the same time?
Why table values are not getting deleted?
library(shiny)
library(tidyverse)
DT <- data.frame(Year = c(1980,1985,1985,1990,1990,1995),
Events = c("Storm", "Earthquake", "Flood", "Draught",
"Earthquake", "Earthquake"),
Area_Loss = c(100, 200, 400, 500, 450,300),
Money = c(1000,2000,3000,4000,5000,6000))
ui <- fluidPage( h4("Updating InserUIs",
selectInput("events","Events",choices=as.character(DT$Events)),
tags$div(id = "Panels"),
actionButton("add","Add"),
tableOutput("table"),
verbatimTextOutput("text")
))
server <- function(session, input, output){
# Reactive values for the number of input panels
vals <- reactiveValues(btn = list(), observers = list())
observeEvent(input$add,ignoreNULL = FALSE,{
l <- length(vals$btn) +1
# Add Panels
for(i in l){
vals$btn[[i]]= insertUI(selector = "#Panels",
ui = splitLayout(id = paste0("Selection",i), where ="afterEnd",
cellWidths = rep("33.33%",3),
selectInput(paste0("year",i), "Year", choices = DT$Year,
selected = ""),
numericInput(paste0("area",i), "Area", min = 0, max = 10000,
value ="", step = 1),
numericInput(paste0("money",i), "Money", min = 0, max = 10000,
value = "", step =1),
div(id ="delete_div",actionButton(paste0("delete",i), "Delete"))
))}
# Update panels
for(i in l){
vals$observers = lapply(l, function(i)
observeEvent(input[[paste0("year",i)]],{
updateNumericInput(session,paste0("area",i),
"Area",min= 0, max= 50000,value = DT$Area_Loss
[DT$Year == input[[paste0("year",i)]]& DT$Events==
input$events] ,step = 0.1)
}))}
for(i in l){
vals$observers = lapply(l, function(i)
observeEvent(input[[paste0("year",i)]],{
updateNumericInput(session,paste0("money",i),
"Money",min= 0, max= 50000,value = DT$Money
[DT$Year == input[[paste0("year",i)]]& DT$Events==
input$events] ,step = 0.1)
}))}
# Delete Panels
for(i in l){
observeEvent(input[[paste0("delete",i)]],{
shiny::removeUI(selector = paste0("#Selection",i))
i <- length(vals$btn) - 1
})}
})
# Reactive table generated from the user inputs
Table <- reactive({
l <- 1:length(vals$btn)
for(i in l){
Year <- unlist(lapply(l, function(i)input[[paste0("year",i)]]))
Area <- unlist(lapply(l, function(i)input[[paste0("area",i)]]))
Money <- unlist(lapply(l, function(i)input[[paste0("money",i)]]))
}
DF0 <- data.frame(Event = input$events,
Year = Year,
Area_loss = Area,
Money = Money
)
DF0
})
# Visualizing the raective table
output$table <- renderTable({
Table()
})
}
shinyApp(ui,server)
Thanks all of you in advance, any suggestion will help me to progress in my app.
I think your problem can be quiet elegantly solved with modules. See comments in the code for details.
library(shiny)
library(dplyr)
DT <- data.frame(Year = c(1980,1985,1985,1990,1990,1995),
Events = c("Storm", "Earthquake", "Flood", "Draught",
"Earthquake", "Earthquake"),
Area_Loss = c(100, 200, 400, 500, 450,300),
Money = c(1000,2000,3000,4000,5000,6000))
##############################Module#############################
## a module consists of all elements which belong together
## i.e. year, area, money and delete button
## take note about the ns() construct which allows for
## namespacing and through this mechanism we can have several
## instances of this module
YAM_ui <- function(id) {
ns <- NS(id)
fluidRow(
id = id,
h3(id),
column(width = 3,
selectInput(ns("year"),
"Year",
DT$Year,
"")),
column(width = 4,
numericInput(ns("area"),
"Area",
0,
0,
10000,
1)),
column(width = 4,
numericInput(ns("money"),
"Money",
0,
0,
10000,
1)),
column(width = 1,
actionButton(ns("delete"), "Delete"))
)
}
## in the server you can access the elements simply by input$element_name
## we have one input reactive (event) which comes from the main app and
## holds the value of the event selectInput
## we return
## - a killSwitch to signal the main app to delete this module
## - a reactive which returns the data from all inputs organized in a data frame
YAM_server <- function(input, output, session, event) {
killMe <- reactiveVal(FALSE)
observe({
req(input$year)
req(event())
updateNumericInput(session,
"area",
min = 0,
max = 50000,
value = DT$Area_Loss[DT$Year == input$year &
DT$Events == event()] ,
step = 0.1)
updateNumericInput(session,
"money",
min = 0,
max = 50000,
value = DT$Money[DT$Year == input$year &
DT$Events == event()] ,
step = 0.1)
})
get_data <- reactive({
req(!is.null(input$year), !is.null(input$area), !is.null(input$money), event())
data.frame(event = event(),
year = input$year,
area = ifelse(input$area == "", NA, input$area),
money = ifelse(input$money == "", NA, input$money))
})
observeEvent(input$delete,
killMe(TRUE))
return(list(delete = killMe,
get_data = get_data))
}
##############################MainApp##############################
ui <- fluidPage(
titlePanel("Modules"),
sidebarLayout(
sidebarPanel(
h4("Updating Inserted UIs"),
selectInput("events",
"Events",
unique(DT$Events)),
actionButton("add",
"Add"),
tableOutput("table")
),
mainPanel(
tags$div(id = "Panels")
)
)
)
## in the main App we have
## - a reactive (handlers) which holds all reactives of all the modules
## - a list (observers) where we create (and delete) observers for the kill
## switch
## When we add a row, we use insertUI to create the html and callModule
## to switch on the modules server logic. We pass the event reactive to
## the module to make it available within the module.
## When we observe a press to the delete button, we remove the handler
## from the lists and remove the corresponding html via removeUI.
## The data table is then updated automatically, because we removed the handler
## and it is not seen in the loop
## To get the table all we have to do is to loop through all handlers and
## call the get_data reactive from the modules to get the data
server <- function(input, output, session) {
handlers <- reactiveVal(list())
observers <- list()
n <- 1
get_event <- reactive({
input$events
})
observeEvent(input$add, {
id <- paste0("row_", n)
n <<- n + 1
insertUI("#Panels",
"beforeEnd",
YAM_ui(id)
)
new_handler <- setNames(list(callModule(YAM_server,
id,
get_event)),
id)
handler_list <- c(handlers(), new_handler)
handlers(handler_list)
})
observe({
hds <- handlers()
req(length(hds) > 0)
new <- setdiff(names(hds),
names(observers))
obs <- setNames(lapply(new, function(n) {
observeEvent(hds[[n]]$delete(), {
removeUI(paste0("#", n))
hds <- handlers()
hds[n] <- NULL
handlers(hds)
observers[n] <<- NULL
}, ignoreInit = TRUE)
}), new)
observers <<- c(observers, obs)
})
output$table <- renderTable({
hds <- req(handlers())
req(length(hds) > 0)
tbl_list <- lapply(hds, function(h) {
h$get_data()
})
do.call(rbind, tbl_list)
})
}
shinyApp(ui, server)
I agree with #thothal that modules help when adding and removing sections of UI and the corresponding data. I've taken a lot of inspiration from their answer and come up with a slightly cleaner (IMHO) implementation.
I've only modified the final server function, where I have managed to do away with the need to keep a list of observers and have captured most of the lifecycle functionality into the add_module function
# utility to hide away the mess of updating the reactiveVal(list())
update_values <- function(values, name, value) {
vals <- values()
vals[[name]] <- value
values(vals)
}
add_module <- function(values, name, server, delete_hook = NULL, remove_selector = NULL) {
# add module server's return to values list
update_values(values, name, server)
# if module has a reactive we should monitor for deleting, do so
if (!is.null(delete_hook)) {
observeEvent(
server[[delete_hook]](), {
removeUI(selector = remove_selector) # remove the ui
update_values(values, name, NULL) # remove the server from our values list
},
ignoreInit = TRUE,
once = TRUE
)
}
}
server <- function(input, output, session) {
handlers <- reactiveVal(list())
get_event <- reactive({
input$events
})
# new
observeEvent(input$add, {
id <- paste0("row_", input$add)
insertUI("#Panels", "beforeEnd", YAM_ui(id))
add_module(
handlers,
name = id,
server = callModule(YAM_server, id, get_event),
delete_hook = "delete",
remove_selector = paste0("#", id)
)
})
# unchanged
output$table <- renderTable({
hds <- req(handlers())
req(length(hds) > 0)
tbl_list <- lapply(hds, function(h) {
h$get_data()
})
do.call(rbind, tbl_list)
})
}
shinyApp(ui, server)