5

I want to design a shiny app that allow the users to save their inputs in the local storage, which means when the users reopen the tool with their web browsers, the tool reload the values last time the users provide. This is mainly achieved by the shinyStore package.

Below is an example. So far I can use the shinyStore to restore any shiny input widget, such as textInput. However, I now want to also restore the edited values in a datatable from the DT package.

I know the information of the edited values are in the input$DT_out_cell_edit, but it is not a single value, so the updateStore function would not work. I thought about using dataTableProxy and replaceData from the DT package, but they cannot keep values from the last time when the app runs. Finally, I tried to set stateSave = TRUE as in this example, but it cannot document the edited values.

If possible, please let me know if you have any ideas. If it is not possible, please also let me know.

library(shiny)
library(DT)
library(shinyStore)

ui <- fluidPage(
  headerPanel("shinyStore Example"),
  sidebarLayout(
    sidebarPanel = sidebarPanel(
      initStore("store", "shinyStore-ex1"),
      # A button to save current input to local storage
      actionButton("save", "Save", icon("save")),
      # A button to clear the input values and local storage
      actionButton("clear", "Clear", icon("stop"))
    ),
    mainPanel = mainPanel(
      fluidRow(
        textInput(inputId = "text1", label = "A text input", value = ""),
        DTOutput(outputId = "DT_out")
      )
    )
  )
)

server <- function(input, output, session) {
  
  output$DT_out <- renderDT(
    datatable(
      mtcars,
      selection = "none", editable = TRUE,
      options = list(
        stateSave = TRUE
      )
    )
  )
  
  # Update the input with local storage when the app runs
  observe({
    if (input$save <= 0){
      updateTextInput(session, inputId = "text1", value = isolate(input$store)[["text1"]])
    }
    updateStore(session, name = "text1", isolate(input$text1))
  })
  
  # Clear the local storage
  observe({
    if (input$clear > 0){
      updateTextInput(session, inputId = "text1", value = "")
      
      updateStore(session, name = "text1", value = "")
    }
  })
}

shinyApp(ui, server)
www
  • 38,575
  • 12
  • 48
  • 84

1 Answers1

6

Please check the following:

Im using a reactiveValue uiTable to track the changes made to the datatable. Once the save button is clicked updateStore is used to save the data.frame.

When a new session starts input$store$uiTable is monitored for changes. If the table was changed it is updated via replaceData.

For now this doesn't work for the rownames of a data.frame, as it needs some extra code, which in my eyes isn't necessary to illustrate the principle.


Edit: I added the mtcars rownames as a column via data.table and disabled editing for the DT rownames to provide a more intuitive example for future readers.

library(shiny)
library(DT)
library(shinyStore)
library(data.table)

mtcarsDT <- data.table(mtcars, keep.rownames = TRUE)
cols <- names(mtcarsDT)
mtcarsDT[, (cols) := lapply(.SD, as.character), .SDcols = cols]

ui <- fluidPage(
  headerPanel("shinyStore Example"),
  sidebarLayout(
    sidebarPanel = sidebarPanel(
      initStore("store", "shinyStore-ex1"),
      actionButton("save", "Save", icon("save")),
      actionButton("clear", "Clear", icon("stop"))
    ),
    mainPanel = mainPanel(
      fluidRow(
        textInput(inputId = "text1", label = "A text input", value = ""),
        DTOutput(outputId = "DT_out")
      )
    )
  )
)

server <- function(input, output, session) {
  
  rv <- reactiveValues(uiTable = mtcarsDT)
  
  mydataTableProxy <- dataTableProxy(outputId = "DT_out")
  
  output$DT_out <- renderDT({
    datatable(mtcarsDT, selection = "none", editable = list(target = 'cell', disable = list(columns = c(0)))
    )})
  
  observeEvent(input$DT_out_cell_edit, {
    # data.frame rownames would need extra handling...
    if(input$DT_out_cell_edit$col > 0){
      rv$uiTable[input$DT_out_cell_edit$row, input$DT_out_cell_edit$col] <- input$DT_out_cell_edit$value
    }
  })
  
  observeEvent(input$save, {
    updateStore(session, name = "text1", input$text1)
    updateStore(session, name = "uiTable", rv$uiTable)
  }, ignoreInit = TRUE)
  
  observeEvent(input$clear, {
    # clear current user inputs:
    updateTextInput(session, inputId = "text1", value = "")
    replaceData(mydataTableProxy, data = mtcarsDT)
    
    # clear tracking table:
    rv$uiTable <- mtcarsDT
    
    # clear shinyStore:
    updateStore(session, name = "text1", value = "")
    updateStore(session, name = "uiTable", mtcarsDT)
  }, ignoreInit = TRUE)
  
  observeEvent(input$store$uiTable, {
    updateTextInput(session, inputId = "text1", value = input$store[["text1"]])
    replaceData(mydataTableProxy, data = as.data.frame(input$store$uiTable))
  })
  
}

shinyApp(ui, server)
ismirsehregal
  • 30,045
  • 5
  • 31
  • 78
  • Thank you. This is great. No need to include the row names. I will need some time to verify the results. – www Jul 16 '21 at 16:58
  • 1
    Sure, let me know when a more detailed explanation is needed. Cheers – ismirsehregal Jul 16 '21 at 20:10
  • 1
    Thank you so much. I tested your code and made some edits. First, I converted all columns in `mtcarsDT` to be character. This step is to prevent if a text was entered by the users into those numeric columns. If this happens, this will lead to some error when Shiny reload the stored values. – www Jul 20 '21 at 19:10
  • I also changed some of the `observeEvent` to `observe` with an `if` statement to decided if the `save` and `clear` button has been clicked. In your original code, it seems like if I keep clicking `save`, the texts for the the `textInput` would be keep updating, which is probably related to the `observeEvent`. – www Jul 20 '21 at 19:12
  • Now the app works like this. When the app opened, if the local storage contains values, the app will load those values. When users click the `save` button, the values in the input and the table would be store to the local storage. When users click the `clear` button, both the inputs, the table, and the local storage would be cleared. – www Jul 20 '21 at 19:16
  • I think this works very well for me. Please let me know what you think and if you have any concerns or comments on my edits. – www Jul 20 '21 at 19:17
  • 2
    Thanks for sharing your progress! I focused on the general concept and didn't extensively test the app. I hope I can review those changes in some spare time - but so far they seem reasonable. – ismirsehregal Jul 20 '21 at 19:24
  • 2
    Thank you! If you finished your test, please let me know. I will mark this answer as completed. I like your answer a lot and I think it will help others in the future. – www Jul 20 '21 at 19:34
  • 2
    @www I checked the code once again. Your additions were necessary. Still I fixed the `observeEvent` solution. I prefer `observeEvent` to make sure that reactions are only triggered on button press. Using `observe` you might run into problems when you change your code later on and accidentally add another trigger etc. but thats more or less personal preference. I think all over this is a sensible solution. Cheers – ismirsehregal Jul 21 '21 at 10:45