I have a shiny app, that allows user to refresh the data in the front end via a button, and shows the data. My app.R is as below:
library(shiny)
file_name <- "sample.csv"
bkg_color <- "red"
# Define UI for application
ui <- fluidPage(
actionButton("refresh", "", icon("refresh") ),
tableOutput("table"),
uiOutput("slider")
)
# Define server logic required
server <- function(input, output, session) {
observeEvent(input$refresh,{
source("updatedata.R")
showModal(modalDialog(
title = "",
"Data refreshed",
easyClose = TRUE,
footer = NULL
))
})
# observe the raw file, and refresh if there is change every 5 seconds
raw <- reactivePoll(5000, session,
checkFunc = function(){
if (file.exists(file_name))
file.info(file_name)$mtime[1]
else
""
},
valueFunc = function(){
read.csv(file_name)
})
output$table <- renderTable(raw())
output$slider <- renderUI({
req(raw())
tagList(
# styling slider bar
tags$style(HTML(paste0(".js-irs-0 .irs-single, .js-irs-0 .irs-bar-edge, .js-irs-0 .irs-bar {background: ",
bkg_color,";border-top: ",bkg_color,";border-bottom: ",bkg_color,"; border: ",bkg_color,"}"))),
sliderInput("date","",
min = min(raw()$v1),
max = max(raw()$v1),
value = max(raw()$v1))
)
})
}
# Run the application
shinyApp(ui = ui, server = server)
I also have another updatedata.R script that does the data update, as below:
file_name <- "sample.csv"
temp <- data.frame(v1 =runif(10, min = 0, max = 100), v2 = Sys.time() )
write.csv(x =temp, file = file_name,row.names = FALSE )
Sys.sleep(10)
Whenever the user clicks the refresh button from the front end, it will performs data update.
After the data finishes refreshing, there is a window prompt says that the data is refreshed.
My problem is that I would also like to have 'some indication' while the data is being refreshed.
I tried with shinycssloaders package, and used withSpinner(tableOutput("table")), but this does not meet my needs. Is there any option that I can explore?
Here is solution for measuring progress every line of source and informing which line is being evaluated.
Assuming that your updatedata.R file:
file_name <- "sample.csv"
temp <- data.frame(v1 =runif(10, min = 0, max = 100), v2 = Sys.time() )
write.csv(temp,file_name,row.names = FALSE )
Sys.sleep(10)
Shiny app will use withProgress() and incProgress inside the loop - Like in the example and prints which line of source is evaluated. Source is evaluated line-by-line in the loop using eval(parse( text = l[i] ))
library(shiny)
file_name <- "sample.csv"
bkg_color <- "red"
# Define UI for application
ui <- fluidPage(
actionButton("refresh", "", icon("refresh") ),
tableOutput("table"),
uiOutput("slider")
)
# Define server logic required
server <- function(input, output, session) {
observeEvent(input$refresh,{
l <- readLines("~/Documents/eclipse_projects/stackoverflow/updatedata.R")
n <- length(l)
withProgress(message = 'Making plot', value = 0, {
for (i in 1:n) {
eval(parse(text=l[i]))
incProgress(1/n, detail = paste("Doing part", l[i]))
}
})
showModal(modalDialog(
title = "",
"Data refreshed",
easyClose = TRUE,
footer = NULL
))
})
# observe the raw file, and refresh if there is change every 5 seconds
raw <- reactivePoll(5000, session,
checkFunc = function(){
if (file.exists(file_name))
file.info(file_name)$mtime[1]
else
""
},
valueFunc = function(){
read.csv(file_name)
})
output$table <- renderTable(raw())
output$slider <- renderUI({
req(raw())
tagList(
# styling slider bar
tags$style(HTML(paste0(".js-irs-0 .irs-single, .js-irs-0 .irs-bar-edge, .js-irs-0 .irs-bar {background: ",
bkg_color,";border-top: ",bkg_color,";border-bottom: ",bkg_color,"; border: ",bkg_color,"}"))),
sliderInput("date","",
min = min(raw()$v1),
max = max(raw()$v1),
value = max(raw()$v1))
)
})
}
# Run the application
shinyApp(ui = ui, server = server)
Alternatively, you can put incProgress() in your source (in the loop or between the lines).
Enjoy
Related
I need a shiny app to do the following:
The user clicks a button
N pop-ups appear to the user asking for input
Then the user downloads the information displayed in the app with a download button
I've been able to achieve points 1 & 2, however I haven't been able to get to 3 because of the fact that the user inputs are reactive values. Here is a sample of code that almost works:
library(shiny)
library(shinyalert)
test <- c("C", "D", "F")
NUM_MODALS <- length(test)
ui <- fluidPage(
shinyalert::useShinyalert(),
actionButton("show", "Show modal dialog"),
lapply(seq(NUM_MODALS), function(id) {
div(id, ":", textOutput(paste0("modal", id), inline = TRUE))
}),
downloadButton("downloadData", "Download")
)
server <- function(input, output) {
observeEvent(input$show, {
for(id in 1:NUM_MODALS){
shinyalert::shinyalert(
type = "input",
text = paste("¿Cuál es la industria de la siguiente empresa?:", test[id]),
inputPlaceholder = "Cuidado con mayúsculas/minúsculas",
inputId = paste0("modal", id)
)
}
})
lapply(seq(NUM_MODALS), function(id) {
output[[paste0("modal", id)]] <- renderText({paste(test[id],input[[paste0("modal", id)]])})
})
export <- reactive(c(input$modal1, input$modal2, input$modal3))
export2 <- isolate(export)
print(export2)
#browser()
output$downloadData <- downloadHandler(
filename = function() {
paste('data-', Sys.Date(), '.csv', sep='')
},
content = function(filesillo) {
fs <- c()
tmpdir <- tempdir()
setwd(tempdir())
path <- paste("prueba.txt", sep = "")
fs <- c(fs, path)
write.csv(export2, filesillo)
}
)
}
shinyApp(ui = ui, server = server)
Instead of the inputs being assigned as a reactive, you can assign to reactiveValues in an observe.
export <- reactiveValues(
dat = NULL
)
observe({
export$dat <- dplyr::bind_rows(
modal1 = input$modal1,
modal2 = input$modal2,
modal3 = input$modal3
)
})
# export <- reactive(c(input$modal1, input$modal2, input$modal3))
# export2 <- isolate(export)
# print(export2)
#browser()
Then in your downloadHandler
#write.csv(export2, filesillo)
write.csv(export$dat, filesillo)
This will output a csv with modal inputs as columns
I am a beginer of shiny, and I am building a shiny app using win10 system, rstudio, and shiny version 1.7.1. I would like to make it more user oriented. It means that other parts of the application will be hid unless user uploads correct data. After many attempts, I decided to use session$userData and shinyjs::toggle to develop this app. But I am confused by session$userData. In the beginning, by reading the official documentation, I think it just like the global environment of r. But obviously not. So I just want to know how to use it correctly, or how to realize the features I want. There are three examples I had tried, they are for your reference.
Please note that the third example is almost what I want, but I don't think it's elegant since the continue button is somewhat redundant.
Examples 1:
I would like to check whether there is data input or whether the input data is a csv format, if true, show the data, and if not, the rest part of the app will be hid. In this case you can see, although you data have passed the check, the tablepanel b will still show nothing, unless before input data you have clicked tablepanel b, or unless after data checking you clicked button go again.
##### 1. packages #####
library(shiny)
library(shinyjs)
##### 2. ui #####
ui <- fluidPage(
useShinyjs(),
tabsetPanel(
tabPanel("a",
sidebarLayout(
sidebarPanel(uiOutput("ui_p1_sidebar1"), uiOutput("ui_p1_sidebar2")),
mainPanel(uiOutput("ui_p1_main"))
)),
tabPanel("b",
sidebarLayout(
sidebarPanel(uiOutput("ui_p2_sidebar")),
mainPanel(uiOutput("ui_p2_main"))
))
)
)
##### 3. server #####
server <- function(input, output, session) {
output$ui_p1_sidebar1 <- renderUI({
fileInput(inputId = "p1s_inputdata",
label = "Input data",
multiple = FALSE,
accept = ".csv")
})
output$ui_p1_sidebar2 <- renderUI({
shiny::actionButton(inputId = "p1s_go",
label = "go",
icon = icon("play"))
})
observeEvent(input$p1s_go,{
isolate({
data <- input$p1s_inputdata
})
output$ui_p1_main <- renderUI({
tagList(
h3("Data check: "),
verbatimTextOutput(outputId = "p1m_datacheck", placeholder = T),
h3("Data show: "),
verbatimTextOutput(outputId = "p1m_datashow", placeholder = T),
)
})
output$p1m_datacheck <- renderPrint({
# data check part, the result of checking is stored by session$userData$sig
if(is.null(data)){
cat("There is no data input! \n")
session$userData$sig <- F
} else{
dataExt <- tools::file_ext(data$name)
if(dataExt != "csv"){
cat("Please input csv data! \n")
session$userData$sig <- F
} else{
cat("Data have passed the check!")
session$userData$data <- read.csv(data$datapath)
session$userData$sig <- T
}
}
})
output$p1m_datashow <- renderPrint({
if(session$userData$sig){
print(session$userData$data)
} else{
cat("Please check the data!")
}
})
output$ui_p2_sidebar <- renderUI({
radioButtons("aaa", "aaa", choices = c("a", "b", "c"))
})
output$ui_p2_main <- renderUI({
verbatimTextOutput(outputId = "p2m_print", placeholder = T)
})
output$p2m_print <- renderPrint({print(letters[1:10])})
observe({
toggle(id = "ui_p2_sidebar", condition = session$userData$sig)
toggle(id = "ui_p2_main", condition = session$userData$sig)
})
})
}
##### 4. app #####
shinyApp(ui = ui, server = server)
Example 2:
In this small case you can see, in a samle module, session$userData$... changed timely, but in another module, it will not change unless you click the button again. It that means session$userData$... could have different values at the same time?
##### 1. packages #####
library(shiny)
##### 2. ui #####
ui <- fluidPage(
sidebarLayout(
sidebarPanel(uiOutput("ui_sidebar")),
mainPanel(uiOutput("ui_main1"), uiOutput("ui_main2"))
)
)
##### 3. server #####
server <- function(input, output, session) {
output$ui_sidebar <- renderUI({
tagList(
radioButtons("s_letter", "letters", choices = c("a", "b", "c")),
shiny::actionButton(inputId = "go1",
label = "GO1",
icon = icon("play"))
)
})
observeEvent(input$go1, {
output$ui_main1 <- renderUI({
tagList(
h3("module 1: shared value changes timely."),
verbatimTextOutput(outputId = "m1", placeholder = T),
h3("module 2: shared value changes by button."),
verbatimTextOutput(outputId = "m2", placeholder = T)
)
})
output$m1 <- renderPrint({
out <- switch (input$s_letter,
"a" = "choose a",
"b" = "choose b",
"c" = "choose c")
session$userData$sharedout <- out
cat("out: \n")
print(out)
cat("sharedout: \n")
print(session$userData$sharedout)
})
output$m2 <- renderPrint({
cat("sharedout: \n")
print(session$userData$sharedout)
})
})
}
##### 4. app #####
shinyApp(ui = ui, server = server)
Example 3: I also tried other solutions. There is a modification of example 1, I have added a continue button to realize my thought. It works well, but I hope the hidden action is based on conditions rather than events. So how to remove the button and let the rest part displayed automatically if data passed checking?
##### 1. packages #####
library(shiny)
##### 2. ui #####
ui <- fluidPage(
tabsetPanel(
tabPanel("a",
sidebarLayout(
sidebarPanel(uiOutput("ui_p1_sidebar1"), uiOutput("ui_p1_sidebar2")),
mainPanel(uiOutput("ui_p1_main"))
)),
tabPanel("b",
sidebarLayout(
sidebarPanel(uiOutput("ui_p2_sidebar")),
mainPanel(uiOutput("ui_p2_main"))
))
)
)
##### 3. server #####
server <- function(input, output, session) {
output$ui_p1_sidebar1 <- renderUI({
fileInput(inputId = "p1s_inputdata",
label = "Input data",
multiple = FALSE,
accept = ".csv")
})
output$ui_p1_sidebar2 <- renderUI({
shiny::actionButton(inputId = "p1s_go",
label = "go",
icon = icon("play"))
})
observeEvent(input$p1s_go,{
isolate({
data <- input$p1s_inputdata
})
output$ui_p1_main <- renderUI({
tagList(
h3("Data check: "),
verbatimTextOutput(outputId = "p1m_datacheck", placeholder = T),
uiOutput("ispass"),
h3("Data show: "),
verbatimTextOutput(outputId = "p1m_datashow", placeholder = T)
)
})
output$p1m_datacheck <- renderPrint({
if(is.null(data)){
cat("There is no data input! \n")
session$userData$sig <- F
} else{
dataExt <- tools::file_ext(data$name)
if(dataExt != "csv"){
cat("Please input csv data! \n")
session$userData$sig <- F
} else{
cat("Data have passed the check!")
session$userData$data <- read.csv(data$datapath)
session$userData$sig <- T
}
}
})
output$ispass <- renderUI({
if(isFALSE(session$userData$sig)){
return()
} else{
shiny::actionButton(inputId = "ispass",
label = "continue",
icon = icon("play"))
}
})
})
observeEvent(input$ispass,{
output$p1m_datashow <- renderPrint({
if(session$userData$sig){
print(session$userData$data)
} else{
cat("Please check the data!")
}
})
output$ui_p2_sidebar <- renderUI({
radioButtons("aaa", "aaa", choices = c("a", "b", "c"))
})
output$ui_p2_main <- renderUI({
verbatimTextOutput(outputId = "p2m_print", placeholder = T)
})
output$p2m_print <- renderPrint({print(letters[1:10])})
})
}
##### 4. app #####
shinyApp(ui = ui, server = server)
I hope the following refactoring will help and does what you want.
An essential tool for hiding,showing and updating UI elements can be the renderUI, but often this is overkill because of rerenderings.
But I would suggest using the shinyjs-package which gives you functions like shinyjs::show and shinyjs::hide for showing and hiding. For updating UI-elements, there are functions like shiny::updateActionButton,shiny::updateCheckboxInput, shiny::updateRadioButtons, ....
It is (always) useful to give your UI-elements IDs, like the tabsetPanel.
Moreover, a nice tool too is shiny::conditionalPanel, but you will dive into all this stuff when programming more apps. :)
##### 1. packages #####
library(shiny)
myapp <- function() {
##### 2. ui #####
ui <- fluidPage(
tabsetPanel(
tabPanel("a",
sidebarLayout(
sidebarPanel(
fileInput(inputId = "p1s_inputdata", label = "Input data", multiple = FALSE, accept = ".csv")
),
mainPanel(uiOutput("ui_p1_main"))
)),
tabPanel("b",
sidebarLayout(
sidebarPanel(radioButtons("aaa", "aaa", choices = c("some", "placeholder", "stuff"))),
mainPanel(verbatimTextOutput(outputId = "p2m_print", placeholder = T))
)),
id = "TABSETPANEL"
)
)
##### 3. server #####
server <- function(input, output, session) {
shiny::hideTab(inputId = "TABSETPANEL", target = "b", session = session)
observeEvent(input$p1s_inputdata, {
data <- input$p1s_inputdata
dataCheckText <- NULL
if(is.null(data)){
dataCheckText <- "There is no data input!"
session$userData$sig <- F
} else{
dataExt <- tools::file_ext(data$name)
if(dataExt != "csv"){
dataCheckText <- "Please input csv data!"
session$userData$sig <- F
} else{
dataCheckText <- "Data have passed the check!"
session$userData$data <- read.csv(data$datapath)
session$userData$sig <- T
}
}
output$p1m_datacheck <- renderPrint(dataCheckText)
if(session$userData$sig) shiny::showTab(inputId = "TABSETPANEL", target = "b", session = session)
else shiny::hideTab(inputId = "TABSETPANEL", target = "b", session = session)
main1Taglist <- tagList(
h3("Data check: "),
verbatimTextOutput(outputId = "p1m_datacheck", placeholder = T)
)
if(session$userData$sig) {
shiny::showTab(inputId = "TABSETPANEL", target = "b", session = session)
output$p1m_datashow <- renderPrint({
print(session$userData$data)
})
main1Taglist <- c(main1Taglist, tagList(
h3("Data show: "),
verbatimTextOutput(outputId = "p1m_datashow", placeholder = T)
))
#Update stuff in panel b according to the new data
updateRadioButtons(session = session, inputId = "aaa", choices = names(session$userData$data))
output$p2m_print <- renderPrint({print(letters[1:10])})
}
output$ui_p1_main <- renderUI(main1Taglist)
})
}
##### 4. app #####
shinyApp(ui = ui, server = server)
}
myapp()
You're somewhat on the right track. Try something like this:
observeEvent(input$go1, {
# Perform data validation here.
# This would look similar to what you have inside output$p1m_datacheck <- renderPrint({})
# If data file is no good, do nothing, exit this function: return()
# Else, data file is good, continue
# Do your output$* <- render*() functions here
})
You don't need to isolate() inside the handlerExpr of observeEvent(). It will already be executed in an isolate() scope.
I am looking to add a click event to a shiny app that has 2 sund2b plots on the page. I would like the text output to just be in reference to whichever plot they clicked last, however it only seems to react to the first plot and not the second. The output is great for the one but I would like it to work for both. This is a simplified example of what I have:
library(shiny)
library(sunburstR)
sequences <- read.csv(
system.file("examples/visit-sequences.csv",package="sunburstR")
,header = FALSE
,stringsAsFactors = FALSE
)[1:200,]
sequences2 <- read.csv(
system.file("examples/visit-sequences.csv",package="sunburstR")
,header = FALSE
,stringsAsFactors = FALSE
)[201:400,]
server <- function(input,output,session){
#sunburst1
output$sunburst <- renderSund2b({
add_shiny(sund2b(sequences))
})
#sunburst2
output$sunburst2 <- renderSund2b({
add_shiny(sund2b(sequences2))
})
#sunburst click event
selection <- reactive({
input$sunburst_click
})
output$selection <- renderText(selection())
}
ui<-fluidPage(
sidebarLayout(
sidebarPanel(
),
# plot sunburst
mainPanel(
sund2bOutput("sunburst"),
sund2bOutput("sunburst2"),
textOutput("selection")
)
)
)
shinyApp(ui = ui, server = server)
The name of the click event input depends on the output name (input$sunburst2_click for output$sunburst2).
Please check the following:
library(shiny)
library(sunburstR)
sequences <- read.csv(
system.file("examples/visit-sequences.csv", package = "sunburstR"),
header = FALSE,
stringsAsFactors = FALSE
)[1:200,]
sequences2 <- read.csv(
system.file("examples/visit-sequences.csv", package = "sunburstR"),
header = FALSE,
stringsAsFactors = FALSE
)[201:400,]
ui <- fluidPage(sidebarLayout(
sidebarPanel(),
# plot sunburst
mainPanel(
sund2bOutput("sunburst"),
sund2bOutput("sunburst2"),
textOutput("selection"),
textOutput("selection2"),
textOutput("selectionLatest")
)
))
server <- function(input, output, session) {
#sunburst1
output$sunburst <- renderSund2b({
add_shiny(sund2b(sequences))
})
#sunburst2
output$sunburst2 <- renderSund2b({
add_shiny(sund2b(sequences2))
})
#sunburst click event
selection <- reactive({
input$sunburst_click
})
selection2 <- reactive({
input$sunburst2_click
})
latestClick <- reactiveVal()
observeEvent(input$sunburst_click, {
latestClick(input$sunburst_click)
})
observeEvent(input$sunburst2_click, {
latestClick(input$sunburst2_click)
})
output$selection <- renderText(paste("plot 1:", paste(selection(), collapse = " - ")))
output$selection2 <- renderText(paste("plot 2:", paste(selection2(), collapse = " - ")))
output$selectionLatest <- renderText(paste("plot 1 or 2:", paste(latestClick(), collapse = " - ")))
}
shinyApp(ui = ui, server = server)
Here the "official" example can be found.
Here is an example. The progress bar just jumps from 0% to 100% due a single function getres(). How to indicate the progress smoothly?
library("shiny")
library("shinyWidgets")
library("DESeq2")
library("airway")
data(airway)
getres <- function(eset){
dds<-DESeqDataSet(eset, design = ~cell + dex)
keep <- rowSums(counts(dds)) >= 10
dds <- dds[keep,]
dds <- DESeq(dds)
res <- results(dds)
return(res)
}
ui <- fluidPage(
tags$h1("Progress bar in Sweet Alert"),
useSweetAlert(), # /!\ needed with 'progressSweetAlert'
actionButton(
inputId = "go",
label = "Launch long calculation !"
)
)
server <- function(input, output, session) {
observeEvent(input$go, {
progressSweetAlert(
session = session, id = "myprogress",
title = "Work in progress",
display_pct = TRUE, value = 0
)
for (i in seq_len(1)) {
Sys.sleep(0.1)
updateProgressBar(
session = session,
id = "myprogress",
res<-getres(airway),
value = i
)
}
closeSweetAlert(session = session)
sendSweetAlert(
session = session,
title =" Calculation completed !",
type = "success"
)
})
}
shinyApp(ui = ui, server = server)
I was unable to run your example as airway and DESeq2 are not available for R 3.6+. BUT there is an interesting package that I have been meaning to try out called waiter.
Within waiter there is waitress which will "let you display loading bars on the entire screen or specific elements only."
There is a great demo app where you play with the different functions.
Here is an example from the docs!
library(shiny)
library(waiter)
ui <- navbarPage(
"Waitress on nav",
tabPanel(
"home",
use_waitress(),
plotOutput("plot")
)
)
server <- function(input, output){
# now waitress ranges from 0 to 100
waitress <- Waitress$new("nav", theme = "overlay", min = 0, max = 10)
output$plot <- renderPlot({
for(i in 1:10){
waitress$inc(1) # increase by 10%
Sys.sleep(.5)
}
hist(runif(100))
waitress$close() # hide when done
})
}
shinyApp(ui, server)
Hope this helps or gives you other ideas!
I am developing a shiny app to be used locally. I am trying to develop a system for the user to be able to restore a former session.
For that, I took the code from this entrance: Saving state of Shiny app to be restored later , and it did work, however I wanted to be able to restore the inputs within a different session, so that I added a fileInput (Restore Session) and a downloadButton (Save Session) to the code, but unfortunately I could not make it work.
My code is as follows:
library(shiny)
ui <- fluidPage(
textInput("control_label",
"This controls some of the labels:",
"LABEL TEXT"),
numericInput("inNumber", "Number input:", min = 1, max = 20, value = 5, step = 0.5),
radioButtons("inRadio", "Radio buttons:",
c("label 1" = "option1",
"label 2" = "option2",
"label 3" = "option3")),
fileInput("load_inputs", "Restore Session", multiple = FALSE),
downloadButton("save_inputs", 'Save Session')
)
server <- function(input, output,session) {
# SAVE SESSION
output$save_inputs <- downloadHandler(
filename = function() {
paste("session", ".RDS", sep = "")
},
content = function(file) {
saveRDS( reactiveValuesToList(input), file)
})
# LOAD SESSION
load_sesion <- reactive({
req(input$load_inputs)
load_session <- readRDS( input$load_inputs$datapath )
})
observeEvent(input$load_inputs,{
if(is.null(input$load_inputs)) {return(NULL)}
savedInputs <- load_sesion()
inputIDs <- names(savedInputs)
inputvalues <- unlist(savedInputs)
for (i in 1:length(inputvalues)) {
session$sendInputMessage(inputIDs[i], list(value=inputvalues[[i]]) )
}
})}
shinyApp(ui, server)
With this code I can save the inputs of the session and I can read them in the following session, however I am not able to use those values stored on the RDS as inputs in another session.
Thanks a lot,
Rachael
As suggested in my above comments the following app uses shiny's built-in capabilities to create bookmarks instead of using a custom function to save the current state of the inputs.
After the download button is clicked a bookmark is stored on the server side, renamed and copied to the downloadHandler.
If the user uploads a bookmark file, the needed path is created based on the filename and the user gets redirected to the earlier session. (Also see the commented out alternative, which requires the user to actively switch sessions).
Of course you could implement a modal to have the user input a name for the session to avoid using the rather cryptic bookmark hash as the filename.
Edit:
Implemented a modal to let the user provide a custom session name (limited to alphanumeric characters)
library(shiny)
library(shinyjs)
library(utils)
library(tools)
library(stringi)
ui <- function(request) {
fluidPage(
useShinyjs(),
textInput("control_label", "This controls some of the labels:", "LABEL TEXT"),
numericInput("inNumber", "Number input:", min = 1, max = 20, value = 5, step = 0.5 ),
radioButtons("inRadio", "Radio buttons:", c("label 1" = "option1", "label 2" = "option2", "label 3" = "option3")),
fileInput("restore_bookmark", "Restore Session", multiple = FALSE, accept = ".rds"),
actionButton("save_inputs", 'Save Session', icon = icon("download"))
)
}
server <- function(input, output, session) {
latestBookmarkURL <- reactiveVal()
onBookmarked(
fun = function(url) {
latestBookmarkURL(parseQueryString(url))
}
)
onRestored(function(state) {
showNotification(paste("Restored session:", basename(state$dir)), duration = 10, type = "message")
})
observeEvent(input$save_inputs, {
showModal(modalDialog(
title = "Session Name",
textInput("session_name", "Please enter a session name (optional):"),
footer = tagList(
modalButton("Cancel"),
downloadButton("download_inputs", "OK")
)
))
}, ignoreInit = TRUE)
# SAVE SESSION
output$download_inputs <- downloadHandler(
filename = function() {
removeModal()
session$doBookmark()
if (input$session_name != "") {
tmp_session_name <- sub("\\.rds$", "", input$session_name)
# "Error: Invalid state id" when using special characters - removing them:
tmp_session_name <- stri_replace_all(tmp_session_name, "", regex = "[^[:alnum:]]")
# TODO: check if a valid filename is provided (e.g. via library(shinyvalidate)) for better user feedback
tmp_session_name <- paste0(tmp_session_name, ".rds")
} else {
paste(req(latestBookmarkURL()), "rds", sep = ".")
}
},
content = function(file) {
file.copy(from = file.path(
".",
"shiny_bookmarks",
req(latestBookmarkURL()),
"input.rds"
),
to = file)
}
)
# LOAD SESSION
observeEvent(input$restore_bookmark, {
sessionName <- file_path_sans_ext(input$restore_bookmark$name)
targetPath <- file.path(".", "shiny_bookmarks", sessionName, "input.rds")
if (!dir.exists(dirname(targetPath))) {
dir.create(dirname(targetPath), recursive = TRUE)
}
file.copy(
from = input$restore_bookmark$datapath,
to = targetPath,
overwrite = TRUE
)
restoreURL <- paste0(session$clientData$url_protocol, "//", session$clientData$url_hostname, ":", session$clientData$url_port, session$clientData$url_pathname, "?_state_id_=", sessionName)
# redirect user to restoreURL
runjs(sprintf("window.location = '%s';", restoreURL))
# showModal instead of redirecting the user
# showModal(modalDialog(
# title = "Restore Session",
# "The session data was uploaded to the server. Please visit:",
# tags$a(restoreURL),
# "to restore the session"
# ))
})
}
shinyApp(ui, server, enableBookmarking = "server")
Just wanted to add a note here because I spent awhile figuring out how to make this work with saved values. My version is very much derived from #ismirsehregal. I also created bookmarking modules that might be helpful to others. This was needed because shinyFiles inputs caused an error and needed to be excluded from bookmarks so I saved the value in a reactive and then saved it in onBookmark. This was the error when they were not excluded:
Error in writeImpl: Text to be written must be a length-one character vector
library(shiny)
library(shinyFiles)
# source these functions
#Saving #=======================================================================
save_bookmark_ui <- function(id){
actionButton(NS(id, "start_save"), "Save")
}
save_bookmark_server <- function(id, latestBookmarkURL, volumes){
moduleServer(id, function(input, output, session) {
shinyDirChoose(input, "save_dir", root = volumes)
save_dir_pth <- reactive(parseDirPath(volumes, input$save_dir))
onRestored(function(state) {
showNotification(paste("Restored session:", basename(state$dir)),
duration = 10, type = "message")
})
setBookmarkExclude(c("save_dir", "start_save", "save_action", "new_dir_name"))
observeEvent(input$start_save, {
showModal(
modalDialog(
p("The app session is saved using two files, input.rds and values.rds",
"You will provide a location and name for a new folder that will",
" be created to store these files. Make sure you choose a name",
"and location that will be easy to find when you want to load the ",
"saved inputs."),
strong("Choose location to save progess"),
br(),
shinyDirButton(NS(id, "save_dir"), "Location to create folder",
"Location to create folder"),
br(),
textInput(NS(id, "new_dir_name"),
"Choose a name for the new folder that will be created"),
br(),
footer = tagList(
actionButton(NS(id, "save_action"), "Save"),
modalButton("Cancel")
),
title = "Save assessment progress"
)
)
})
iv <- shinyvalidate::InputValidator$new()
iv$add_rule("new_dir_name", shinyvalidate::sv_optional())
iv$add_rule("new_dir_name",
shinyvalidate::sv_regex("[^[:alnum:]]",
paste0("Please choose a name with only",
" letters or numbers and no spaces"),
invert = TRUE))
observeEvent(input$save_action, {
if (!iv$is_valid()) {
iv$enable()
} else {
removeModal()
session$doBookmark()
if (input$new_dir_name != "") {
# "Error: Invalid state id" when using special characters - removing them:
tmp_session_name <- stringr::str_replace_all(input$new_dir_name,
"[^[:alnum:]]", "")
} else {
tmp_session_name <- paste(req(latestBookmarkURL))
}
# create the new directory in the chosen location
new_dir <- fs::dir_create(fs::path(save_dir_pth(), tmp_session_name))
message("Saving session")
# move the files from where shiny saves them to where the user can find them
fs::dir_copy(path = fs::path(".", "shiny_bookmarks", req(latestBookmarkURL)),
new_path = new_dir,
overwrite = TRUE)
}
}, ignoreInit = TRUE)
})
}
# Load #=======================================================================
load_bookmark_ui <- function(id){
actionButton(NS(id, "start_load"), "Load")
}
load_bookmark_server <- function(id, volumes){
moduleServer(id, function(input, output, session){
shinyDirChoose(input, "load_dir", root = volumes)
load_dir_pth <- reactive(parseDirPath(volumes, input$load_dir))
setBookmarkExclude(c("load_dir", "load_action", "start_load"))
observeEvent(input$start_load, {
showModal(
modalDialog(
strong("Select the folder where the app was saved"),
br(),
shinyDirButton(NS(id, "load_dir"), "Select Folder",
"Location of folder with previous state"),
footer = tagList(
actionButton(NS(id, "load_action"), "Load"),
modalButton("Cancel")
),
title = "Load existing assessment"
)
)
})
# LOAD SESSION
observeEvent(input$load_action, {
sessionName <- fs::path_file(load_dir_pth())
targetPath <- file.path(".", "shiny_bookmarks", sessionName)
if (!dir.exists(dirname(targetPath))) {
dir.create(dirname(targetPath), recursive = TRUE)
}
# copy the bookmark to where shiny expects it to be
fs::dir_copy(path = load_dir_pth(),
new_path = targetPath,
overwrite = TRUE)
restoreURL <- paste0(session$clientData$url_protocol, "//",
session$clientData$url_hostname, ":",
session$clientData$url_port, "/?_state_id_=",
sessionName)
removeModal()
# redirect user to restoreURL
shinyjs::runjs(sprintf("window.location = '%s';", restoreURL))
# showModal instead of redirecting the user
# showModal(modalDialog(
# title = "Restore Session",
# "The session data was uploaded to the server. Please visit:",
# tags$a(restoreURL, href = restoreURL),
# "to restore the session"
# ))
})
})
}
# Input options
valueNms <- c("Greatly increase", "Increase", "Somewhat increase", "Neutral")
valueOpts <- c(3, 2, 1, 0)
ui <- function(request) {
fluidPage(
shinyjs::useShinyjs(),
textInput("control_label", "This controls some of the labels:", "LABEL TEXT"),
numericInput("inNumber", "Number input:", min = 1, max = 20, value = 5, step = 0.5 ),
radioButtons("inRadio", "Radio buttons:",
c("label 1" = "option1", "label 2" = "option2", "label 3" = "option3")),
checkboxGroupInput("inChk","Checkbox:", choiceNames = valueNms,
choiceValues = valueOpts),
shinyFilesButton("range_poly_pth", "Choose file",
"Range polygon shapefile", multiple = FALSE),
verbatimTextOutput("range_poly_pth_out", placeholder = TRUE),
save_bookmark_ui("save"),
load_bookmark_ui("load"),
tableOutput("table")
)
}
server <- function(input, output, session) {
shinyFileChoose("range_poly_pth", root = volumes, input = input)
file_pth <- reactive({
if(is.integer(input$range_poly_pth)){
if(!is.null(restored$yes)){
return(file_pth_restore())
}
return(NULL)
} else{
return(parseFilePaths(volumes, input$range_poly_pth)$datapath)
}
})
output$table <- renderTable({
req(file_pth())
read.csv(file_pth())
})
output$range_poly_pth_out <- renderText({
file_pth()
})
setBookmarkExclude(c("range_poly_pth",
"range_poly_pth_out"))
# this part is not allowed to be inside the module
latestBookmarkURL <- reactiveVal()
onBookmarked(
fun = function(url) {
latestBookmarkURL(parseQueryString(url))
showNotification("Session saved",
duration = 10, type = "message")
}
)
R.utils::withTimeout({
volumes <- c(wd = getShinyOption("file_dir"),
Home = fs::path_home(),
getVolumes()())
}, timeout = 10, onTimeout = "error")
save_bookmark_server("save", latestBookmarkURL(), volumes)
load_bookmark_server("load", volumes)
# Need to explicitly save and restore reactive values.
onBookmark(function(state) {
val2 <- Sys.Date()
state$values$date <- val2
state$values$file <- file_pth()
})
restored <- reactiveValues()
file_pth_restore <- reactiveVal()
onRestore(fun = function(state){
file_pth_restore(state$values$file)
print(file_pth_restore())
restored$yes <- TRUE
})
}
shinyApp(ui, server, enableBookmarking = "server")