How to use the localStorage option for DT in R Shiny?
Asked Answered
L

1

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)
Luncheon answered 14/7, 2021 at 3:40 Comment(0)
G
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)
Glidden answered 16/7, 2021 at 13:2 Comment(9)
Thank you. This is great. No need to include the row names. I will need some time to verify the results.Luncheon
Sure, let me know when a more detailed explanation is needed. CheersGlidden
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.Luncheon
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.Luncheon
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.Luncheon
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.Luncheon
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.Glidden
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.Luncheon
@Luncheon 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. CheersGlidden

© 2022 - 2024 — McMap. All rights reserved.