7

My Shiny app has several inputs which are used to define several parameters of a generated plot. It's very likely that the user will spend some minutes going through all possible options until he's satisfied with the output. Obviously the plot can be exported in different formats, but it's possible that the user will want to recreate the same plot with different data later, or maybe just change one small detail.

Because of this, I need to offer the user a way to export all his settings and keep that file for later use. I've developed an approach, but it isn't working well. I'm using reactiveValuesToList to get the names of all input elements and save as a simple text file with the format inputname=inputvalue. This is the downloadHandler on server.R:

output$bt_export <- downloadHandler(
  filename = function() {
    "export.txt"
  },
  content = function(file) {
    inputsList <- names(reactiveValuesToList(input))
    exportVars <- paste0(inputsList, "=", sapply(inputsList, function(inpt) input[[inpt]]))
    write(exportVars, file)
  })

This works fine, but loading isn't going very smoothly. Since I don't (and couldn't figure out how) save the input type, I have to update the values blindly. This is how I do it:

importFile <- reactive({      
  inFile <- input$fileImport      
  if (is.null(inFile))
    return(NULL)      
  lines <- readLines(inFile$datapath)
  out <- lapply(lines, function(l) unlist(strsplit(l, "=")))      
  return(out)
})

observe({
    imp <- importFile()            
    for (inpt in imp) {
      if (substr(inpt[2], 0, 1) == "#") {
        shinyjs::updateColourInput(session, inputId = inpt[1], value = inpt[2])
      } else {
        try({
          updateTextInput(session, inputId = inpt[1], value = inpt[2])
          updateNumericInput(session, inputId = inpt[1], value = inpt[2])
          updateSelectInput(session, inputId = inpt[1], selected = inpt[2])              
        })
      }
    }       
  })

Apart from the shinyjs::colorInput, which can be recognized by the # start, I have to use try() for the others. This works, partially, but some inputs are not being updated. Inspecting the exported file manually shows that inputs which weren't updated are there, so I suppose that updating 100+ inputs at once isn't a good idea. Also the try() part doesn't look good and is probably not a good idea.

The app is close to finished, but will probably be updated in the future, having some inputs added/changed. It's acceptable if this even make some "old" exported inputs invalid, since I'll try keep the backwards compatibility. But I'm looking for an approach that isn't just writing hundreds of lines to update the inputs one-by-one.

I've thought about using save.image() but simply using load() does not restore the app inputs. I also considered a way to somehow update all inputs at once, instead of one-by-one, but didn't come up with anything. Is there any better way to export all user inputs to a file and then load them all? It doesn't matter if it's a tweak to this one that works better or a completely different approach.

Molx
  • 6,816
  • 2
  • 31
  • 47
  • Does your app feature highly dependent inputs? Is it a lot of `renderUI` blocks or is it a lot of `numericInput` -esque inputs? – Mark Sep 08 '15 at 14:36
  • @Mark They are all "static" inputs, none of them are created using `renderUI`. – Molx Sep 08 '15 at 14:55
  • @Molx If you don't find a solution that you're happy with, please contact me. If one of the solutions here work, then great :) (nice to see my colourInput being used!) – DeanAttali Sep 16 '15 at 23:06
  • @daattali You have no idea how happy I was when I found `colourInput`, my app wouldn't be as useful without it. `shinyjs::reset` is also very neat. I have to say though that I just found out that the main issue here was that `shinyjs-resettable-settings` was also being exported and loaded, which was making a mess on the input values, since it's a list full of `"input = value"`. But that's solved now. :) – Molx Sep 17 '15 at 01:31
  • @Molx I don't fully understand the problem that was happening, do you mind explaining that in more detail? If there's anything my code does that is problematic I'd like to know so people won't run into issues in the future – DeanAttali Sep 17 '15 at 02:59
  • @daattali Not your code. I was exporting all values from `reactiveValuesToList(input)`, and it seems that shinyjs creates one reactive value when you use reset(). This value when exported has the format `list = (input1 = value1, input2 = value2)`, etc. I also used `=` as a separator on my export file, so reading that in caused some inputs to be updated twice. Once correctly and once back again to the default value. This bugged only part of them because of the order that the values were exported. – Molx Sep 17 '15 at 03:04
  • Ah yes, I suppose it's not perfect that the input variable is getting "dirtied" by things that the user didn't define, but unfortunately I think it's the only way to have shinyjs JavaScript communicate to R. Glad you were able to work around that :) – DeanAttali Sep 17 '15 at 04:36
  • I was redirected by a new question to this old one: Anyway, this might be of interest: https://shiny.rstudio.com/gallery/bookmarking-url.html – Tonio Liebrand Jan 24 '17 at 18:40
  • @TonioLiebrand This looks promising, thanks for bringing it up here. I think it could lead to a huge URL on my case, but it's still worth looking into. – Molx Jan 24 '17 at 22:21

3 Answers3

12

If you look at the code of the shiny input update functions, they end by session$sendInputMessage(inputId, message). message is a list of attributes that need to be changed in the input, for ex, for a checkbox input: message <- dropNulls(list(label = label, value = value))

Since most of the input have the value attribute, you can just use the session$sendInputMessage function directly on all of them without the try.

Here's an example, I created dummy_data to update all the inputs when you click on the button, the structure should be similar to what you export:

ui.R

library(shiny)
shinyUI(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")),
  actionButton("update_data", "Update")

  ))

server.R

library(shiny)

dummy_data <- c("inRadio=option2","inNumber=10","control_label=Updated TEXT" )

shinyServer(function(input, output,session) {
  observeEvent(input$update_data,{    
    out <- lapply(dummy_data, function(l) unlist(strsplit(l, "="))) 
   for (inpt in out) {
     session$sendInputMessage(inpt[1], list(value=inpt[2]))
    }
   })

})

All the update functions also preformat the value before calling session$sendInputMessage. I haven't tried all possible inputs but at least for these 3 you can pass a string to the function to change the numericInput and it still works fine.

If this is an issue for some of your inputs, you might want to save reactiveValuesToList(input) using save, and when you want to update your inputs, use load and run the list in the for loop (you'll have to adapt it to a named list).

NicE
  • 21,165
  • 3
  • 51
  • 68
  • Although this was a rather simple change, it was very useful to remove the ugly `try()` and good knowledge. Eventually it helped me find the biggest problem of the approach, which was some undesired and unexpected reactive values being exported and doing something like a injection-attack on the loading function. – Molx Sep 17 '15 at 01:27
4

This is a bit old but I think is usefull to post a complete example, saving and loading user inputs.

library(shiny)  

ui <- shinyUI(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")),

  actionButton("load_inputs", "Load inputs"), 
  actionButton('save_inputs', 'Save inputs')

)) 

server <-  shinyServer(function(input, output,session) { 

  observeEvent(input$load_inputs,{   

    if(!file.exists('inputs.RDS')) {return(NULL)}

    savedInputs <- readRDS('inputs.RDS')

    inputIDs      <- names(savedInputs) 
    inputvalues   <- unlist(savedInputs) 
    for (i in 1:length(savedInputs)) { 
      session$sendInputMessage(inputIDs[i],  list(value=inputvalues[[i]]) )
    }
  })

  observeEvent(input$save_inputs,{ 
    saveRDS( reactiveValuesToList(input) , file = 'inputs.RDS')
  })  
})
Eduardo Bergel
  • 2,685
  • 1
  • 16
  • 21
  • This works well until there are null values, such as when using checkboxes. When loading the inputs, the `unlist` command fails to make an entry for anything with a null value, which causes a subscript error, because the length of `inputvalues` is less than the length of `savesInputs`. – S Novogoratz Aug 07 '20 at 13:45
1

Unless you're doing a lot of highly flexible type inputs (renderUI blocks which could be any sort of input) then you could create a list storing all current values, use dput to save them to a file with a corresponding dget to read it in.

In one app I have, I allow users to download a file storing all their uploaded data plus all their options.

output$saveData <- downloadHandler(
  filename = function() { 
    paste0('Export_',Sys.Date(),'.sprout')
  },
  content = function(file) {
    dataToExport = list()
    #User specified options
    dataToExport$sproutData$transformations=sproutData$transformations  #user specified transformations
    dataToExport$sproutData$processing=sproutData$processing  #user specified text processing rules
    dataToExport$sproutData$sc=sproutData$sc  #user specified option to spell check
    dataToExport$sproutData$scOptions=sproutData$scOptions  #user specified spell check options (only used if spell check is turned on)
    dataToExport$sproutData$scLength=sproutData$scLength  #user specified min word lenght for spell check (only used if spell check is turned on)
    dataToExport$sproutData$stopwords=sproutData$stopwords  #user specified stopwords
    dataToExport$sproutData$stopwordsLastChoice=sproutData$stopwordsLastChoice #last pre-built list selected
    dput(dataToExport,file=file)
  }
)

Here I make an empty list, then I stick in the values I use in my app. The reason for the dTE$sD$name structure is that I have a reactiveValues called sproutData which stores all user selected options and data. So, I preserve the structure in the output.

Then, I have a load data page which does the following:

output$loadStatusIndicator = renderUI({
  worked = T
  a = tryCatch(dget(input$loadSavedData$datapath),error=function(x){worked<<-F})
  if(worked){
    #User specified options
    a$sproutData$transformations->sproutData$transformations  #user specified transformations
    a$sproutData$processing->sproutData$processing  #user specified text processing rules
    updateCheckboxGroupInput(session,"processingOptions",selected=sproutData$processing)
    a$sproutData$sc->sproutData$sc  #user specified option to spell check
    updateCheckboxInput(session,"spellCheck",value = sproutData$sc)
    a$sproutData$scOptions->sproutData$scOptions  #user specified spell check options (only used if spell check is turned on)
    updateCheckboxGroupInput(session,"spellCheckOptions",selected=sproutData$scOptions)
    a$sproutData$scLength->sproutData$scLength  #user specified min word lenght for spell check (only used if spell check is turned on)
    updateNumericInput(session,"spellCheckMinLength",value=sproutData$scLength)
    a$sproutData$stopwords->sproutData$stopwords  #user specified stopwords
    a$sproutData$stopwordsLastChoice->sproutData$stopwordsLastChoice
    if(sproutData$stopwordsLastChoice[1] == ""){
      updateSelectInput(session,"stopwordsChoice",selected="none")
    } else if(all(sproutData$stopwordsLastChoice == stopwords('en'))){
      updateSelectInput(session,"stopwordsChoice",selected="en")
    } else if(all(sproutData$stopwordsLastChoice == stopwords('SMART'))){
      updateSelectInput(session,"stopwordsChoice",selected="SMART")
    }
    HTML("<strong>Loaded data!</strong>")
  } else if (!is.null(input$loadSavedData$datapath)) {
    HTML(paste("<strong>Not a valid save file</strong>"))
  }
})

The actual output is a table which details what it found and what it set. But, because I know all the inputs and they don't change, I can explicitly store them (default or changed value) and then explicitly update them when the save file is uploaded.

Mark
  • 4,387
  • 2
  • 28
  • 48
  • I've also thought about "manually" updating each one of them, but this would add too many lines (currently, 117), which would be very hard to maitain. The app is almost done, but I'd say it's in alpha and will probably be modified, potentially receiving more inputs, maybe changing some of them. I'll add some of that to the question. – Molx Sep 08 '15 at 14:58