Bi-directional synchronization without triggering a loop

Bookmarking is very limited and will not be able to deal with recovery from state saved on a file on the server or from a database.

enableBookmarking = "server" saves a file on the host to restore from.

To restore custom data use the state object:

To record vals$sum , we will tell Shiny to save extra values when bookmarking state, and restore those values when restoring state. This is done by adding callbacks, using onBookmark() and onRestore() in the application’s server function. The callback functions that you pass to onBookmark() and onRestore() must take one argument, typically named state . The state object has an environment object named values , to which you can write or read arbitrary values. For this app, it’s simple: we’ll just save vals$sum when we bookmark, and copy it back when we restore.

That's not what I need. I need to be able to reload state from arbitrary sources with data in arbitrary format. bookmarking is a canned storage system that dumps and recover current state to a predefined pickling storage format I have no control over.

Imagine I want to fetch information from a SQL database and set the UI state accordingly. I can't achieve that with bookmark.

You can use shiny's bookmarking along with a database to store arbitrary data just fine:

library(RSQLite)
library(shiny)

con <- dbConnect(RSQLite::SQLite(), "test.db")
onStop(function(){dbDisconnect(con)})

ui <- function(req) {
  fluidPage(
    textInput("txt1", "Input text"),
    textInput("txt2", "Input text"),
    bookmarkButton()
  )
}
server <- function(input, output, session) {
  
  setBookmarkExclude(c("txt1", "txt2"))
  
  onBookmark(function(state) {
    dbDF <- data.frame(dir = state$dir, txt1 = paste("We just got restored...", input$txt1), txt2 = paste("...from a database!", input$txt2), stringsAsFactors = FALSE)
    
    if(!"dbDF" %in% dbListTables(con)){
      dbWriteTable(con, "dbDF", dbDF)
    } else {
      dbAppendTable(con, "dbDF", dbDF)
    }
  })
  
  onRestored(function(state) {
    resDF <- dbGetQuery(con, sprintf("Select txt1, txt2 FROM dbDF WHERE dir = '%s'", state$dir))
    updateTextInput(session, "txt1", value = resDF$txt1)
    updateTextInput(session, "txt2", value = resDF$txt2)
  })
}

enableBookmarking("server")
shinyApp(ui, server)

screen

Please see this.

Regarding this:

an option would be to use disable from library(shinyjs) to avoid user input while calculating:

library(shiny)
library(shinyjs)

AppModel <- function() {
  self <- reactiveValues(
    name = NULL
  )
  
  return(self)
}

AppModel_checkCompleteness <- function(self) {
  incomplete <- list(
    name = NULL,
    completed = TRUE
  )
  
  if (is.null(self$name) || stringr::str_length(stringr::str_trim(self$name)) == 0) {
    incomplete$name <- "You have not specified your name"
    incomplete$completed <- FALSE;
  }
  
  return(incomplete)
}


ui <- function() {
  view <- wellPanel(
    useShinyjs(),
    tags$head(
      tags$script(
        "$(document).on('shiny:outputinvalidated', function(event) {
          if (event.name === 'checklistUi') {
            Shiny.setInputValue('checklistUiInvalidated', true, {priority: 'event'});
          }
        });"
      )
    ),
    style="width: 70%; margin-left: auto; margin-right: auto",
    textInput(
      "nameText",
      "Your name and surname",
      placeholder="Please write your name and surname"),
    uiOutput("checklistUi"),
  )
  return(view)
}

server <- function(input, output, session) {    
  app_model <- AppModel()                                                                 
  
  output$checklistUi <- renderUI({
    disable("nameText")
    Sys.sleep(2)                                                                          
    completion_widget <- "Hello"                                                          
    
    completed <- AppModel_checkCompleteness(app_model)                                    
    
    return(completion_widget)                                                             
  })
  
  observeEvent(input$checklistUiInvalidated, {
    enable("nameText")
  })
  
  observeEvent({input$nameText},                                                          
               {                                                                                       
                 app_model$name <- input$nameText                                                      
               })                                                                                      
  
  observeEvent({app_model$name},                                                          
               {
                 updateTextInput(session, "nameText", value = app_model$name)
               })                                                                                      
}

shinyApp(ui = ui, server = server)

I'd combine this with the debounce approach above.

For future readers: bidirectional synchronization triggers back and forth fight between transactions · Issue #2767 · rstudio/shiny · GitHub

While this is true to an extent, what you are doing there is to update the view directly. What you want is to update the model, then the View updates against the model. Your approach looks easy when the interface is easy, but it does not scale to more complex applications where you have complex models and don't know which views are observing these models.

More specifically, I have a similar situation with a number of sliders that are generated dynamically. I don't know how many I have until the user actually provides the file to open. The file is then opened, parsed, sliders are created and mapped to the value, and the synchronization has to be kept. Your solution just considers "restoring", but this is a limited use case, and in the case you are showing specific to the fact that from the restore function you know exactly which inputs you have and where they are (e.g. which ids they have).

Of course it is a limited usecase. The point was to proof your earlier statement wrong. I think this approach can easily be adapted to work with a dynamic number of sliders. But you seem to be quite focused on "what isn't possible" instead of finding solutions.

Please don't think that I don't appreciate your input. In fact, I must say thank you for teaching me some interesting tricks. I am trying to figure out a more general solution because we need to develop applications in the future that will need these quite general scenarios. If I can figure out a general solution I can extract it into some sort of utility function or best practice for me and the team.

I think, if we accept that user direct entry into input feels should be prioritised, and at a much lower frequence mass updates from model to refresh those inputs (disrespecting and overwriting them which is bad if it happens fast and all the time because it frustrates manual update but fine if its in a flash when nothing much is happening) then I think the general structure demo'd in my previous post does have some merit. I think effort might go into a way to dynamically create the observeEvent refresh structures when you create correspondence between your dynamic inputs and the underlying model, but I suspect this could be successful.

This topic was automatically closed 54 days after the last reply. New replies are no longer allowed.