1ShinySaveState <- R6Class("ShinySaveState", 2 public = list( 3 input = NULL, 4 exclude = NULL, 5 onSave = NULL, # A callback to invoke during the saving process. 6 7 # These are set not in initialize(), but by external functions that modify 8 # the ShinySaveState object. 9 dir = NULL, 10 11 12 initialize = function(input = NULL, exclude = NULL, onSave = NULL) { 13 self$input <- input 14 self$exclude <- exclude 15 self$onSave <- onSave 16 private$values_ <- new.env(parent = emptyenv()) 17 } 18 ), 19 20 active = list( 21 # `values` looks to the outside world like an environment for storing 22 # arbitrary values. Two things to note: (1) This is an environment (instead 23 # of, say, a list) because if the onSave function represents multiple 24 # callback functions (when onBookmark is called multiple times), each 25 # callback can change `values`, and if we used a list, one of the callbacks 26 # could easily obliterate values set by another. This can happen when using 27 # modules that have an onBookmark function. (2) The purpose of the active 28 # binding is to prevent replacing state$values with another arbitrary 29 # object. (Simply locking the binding would prevent all changes to 30 # state$values.) 31 values = function(value) { 32 if (missing(value)) 33 return(private$values_) 34 35 if (identical(value, private$values_)) { 36 return(value) 37 } else { 38 stop("Items in `values` can be changed, but `values` itself cannot be replaced.") 39 } 40 } 41 ), 42 43 private = list( 44 values_ = NULL 45 ) 46) 47 48 49# Save a state to disk. Returns a query string which can be used to restore the 50# session. 51saveShinySaveState <- function(state) { 52 id <- createUniqueId(8) 53 54 # A function for saving the state object to disk, given a directory to save 55 # to. 56 saveState <- function(stateDir) { 57 state$dir <- stateDir 58 59 # Allow user-supplied onSave function to do things like add state$values, or 60 # save data to state dir. 61 if (!is.null(state$onSave)) 62 isolate(state$onSave(state)) 63 64 # Serialize values, possibly saving some extra data to stateDir 65 exclude <- c(state$exclude, "._bookmark_") 66 inputValues <- serializeReactiveValues(state$input, exclude, state$dir) 67 saveRDS(inputValues, file.path(stateDir, "input.rds")) 68 69 # If values were added, save them also. 70 if (length(state$values) != 0) 71 saveRDS(state$values, file.path(stateDir, "values.rds")) 72 } 73 74 # Pass the saveState function to the save interface function, which will 75 # invoke saveState after preparing the directory. 76 77 # Look for a save.interface function. This will be defined by the hosting 78 # environment if it supports bookmarking. 79 saveInterface <- getShinyOption("save.interface", default = NULL) 80 81 if (is.null(saveInterface)) { 82 if (inShinyServer()) { 83 # We're in a version of Shiny Server/Connect that doesn't have 84 # bookmarking support. 85 saveInterface <- function(id, callback) { 86 stop("The hosting environment does not support saved-to-server bookmarking.") 87 } 88 89 } else { 90 # We're running Shiny locally. 91 saveInterface <- saveInterfaceLocal 92 } 93 } 94 95 saveInterface(id, saveState) 96 97 paste0("_state_id_=", encodeURIComponent(id)) 98} 99 100# Encode the state to a URL. This does not save to disk. 101encodeShinySaveState <- function(state) { 102 exclude <- c(state$exclude, "._bookmark_") 103 inputVals <- serializeReactiveValues(state$input, exclude, stateDir = NULL) 104 105 # Allow user-supplied onSave function to do things like add state$values. 106 if (!is.null(state$onSave)) 107 isolate(state$onSave(state)) 108 109 inputVals <- vapply(inputVals, 110 function(x) toJSON(x, strict_atomic = FALSE), 111 character(1), 112 USE.NAMES = TRUE 113 ) 114 115 res <- "" 116 117 # If any input values are present, add them. 118 if (length(inputVals) != 0) { 119 res <- paste0(res, "_inputs_&", 120 paste0( 121 encodeURIComponent(names(inputVals)), 122 "=", 123 encodeURIComponent(inputVals), 124 collapse = "&" 125 ) 126 ) 127 } 128 129 # If 'values' is present, add them as well. 130 if (length(state$values) != 0) { 131 values <- vapply(state$values, 132 function(x) toJSON(x, strict_atomic = FALSE), 133 character(1), 134 USE.NAMES = TRUE 135 ) 136 137 res <- paste0(res, 138 if (length(inputVals != 0)) "&", # Add separator if there were inputs 139 "_values_&", 140 paste0( 141 encodeURIComponent(names(values)), 142 "=", 143 encodeURIComponent(values), 144 collapse = "&" 145 ) 146 ) 147 } 148 149 res 150} 151 152RestoreContext <- R6Class("RestoreContext", 153 public = list( 154 # This will be set to TRUE if there's actually a state to restore 155 active = FALSE, 156 157 # This is set to an error message string in case there was an initialization 158 # error. Later, after the app has started on the client, the server can send 159 # this message as a notification on the client. 160 initErrorMessage = NULL, 161 162 # This is a RestoreInputSet for input values. This is a key-value store with 163 # some special handling. 164 input = NULL, 165 166 # Directory for extra files, if restoring from state that was saved to disk. 167 dir = NULL, 168 169 # For values other than input values. These values don't need the special 170 # phandling that's needed for input values, because they're only accessed 171 # from the onRestore function. 172 values = NULL, 173 174 initialize = function(queryString = NULL) { 175 self$reset() # Need this to initialize self$input 176 177 if (!is.null(queryString) && nzchar(queryString)) { 178 tryCatch( 179 withLogErrors({ 180 qsValues <- parseQueryString(queryString, nested = TRUE) 181 182 if (!is.null(qsValues[["__subapp__"]]) && qsValues[["__subapp__"]] == 1) { 183 # Ignore subapps in shiny docs 184 self$reset() 185 186 } else if (!is.null(qsValues[["_state_id_"]]) && nzchar(qsValues[["_state_id_"]])) { 187 # If we have a "_state_id_" key, restore from saved state and 188 # ignore other key/value pairs. If not, restore from key/value 189 # pairs in the query string. 190 self$active <- TRUE 191 private$loadStateQueryString(queryString) 192 193 } else { 194 # The query string contains the saved keys and values 195 self$active <- TRUE 196 private$decodeStateQueryString(queryString) 197 } 198 }), 199 error = function(e) { 200 # If there's an error in restoring problem, just reset these values 201 self$reset() 202 self$initErrorMessage <- e$message 203 warning(e$message) 204 } 205 ) 206 } 207 }, 208 209 reset = function() { 210 self$active <- FALSE 211 self$initErrorMessage <- NULL 212 self$input <- RestoreInputSet$new(list()) 213 self$values <- new.env(parent = emptyenv()) 214 self$dir <- NULL 215 }, 216 217 # Completely replace the state 218 set = function(active = FALSE, initErrorMessage = NULL, input = list(), values = list(), dir = NULL) { 219 # Validate all inputs 220 stopifnot(is.logical(active)) 221 stopifnot(is.null(initErrorMessage) || is.character(initErrorMessage)) 222 stopifnot(is.list(input)) 223 stopifnot(is.list(values)) 224 stopifnot(is.null(dir) || is.character(dir)) 225 226 self$active <- active 227 self$initErrorMessage <- initErrorMessage 228 self$input <- RestoreInputSet$new(input) 229 self$values <- list2env2(values, parent = emptyenv()) 230 self$dir <- dir 231 }, 232 233 # This should be called before a restore context is popped off the stack. 234 flushPending = function() { 235 self$input$flushPending() 236 }, 237 238 239 # Returns a list representation of the RestoreContext object. This is passed 240 # to the app author's onRestore function. An important difference between 241 # the RestoreContext object and the list is that the former's `input` field 242 # is a RestoreInputSet object, while the latter's `input` field is just a 243 # list. 244 asList = function() { 245 list( 246 input = self$input$asList(), 247 dir = self$dir, 248 values = self$values 249 ) 250 } 251 ), 252 253 private = list( 254 # Given a query string with a _state_id_, load saved state with that ID. 255 loadStateQueryString = function(queryString) { 256 values <- parseQueryString(queryString, nested = TRUE) 257 id <- values[["_state_id_"]] 258 259 # Check that id has only alphanumeric chars 260 if (grepl("[^a-zA-Z0-9]", id)) { 261 stop("Invalid state id: ", id) 262 } 263 264 # This function is passed to the loadInterface function; given a 265 # directory, it will load state from that directory 266 loadFun <- function(stateDir) { 267 self$dir <- stateDir 268 269 if (!dirExists(stateDir)) { 270 stop("Bookmarked state directory does not exist.") 271 } 272 273 tryCatch({ 274 inputValues <- readRDS(file.path(stateDir, "input.rds")) 275 self$input <- RestoreInputSet$new(inputValues) 276 }, 277 error = function(e) { 278 stop("Error reading input values file.") 279 } 280 ) 281 282 valuesFile <- file.path(stateDir, "values.rds") 283 if (file.exists(valuesFile)) { 284 tryCatch({ 285 self$values <- readRDS(valuesFile) 286 }, 287 error = function(e) { 288 stop("Error reading values file.") 289 } 290 ) 291 } 292 } 293 294 # Look for a load.interface function. This will be defined by the hosting 295 # environment if it supports bookmarking. 296 loadInterface <- getShinyOption("load.interface", default = NULL) 297 298 if (is.null(loadInterface)) { 299 if (inShinyServer()) { 300 # We're in a version of Shiny Server/Connect that doesn't have 301 # bookmarking support. 302 loadInterface <- function(id, callback) { 303 stop("The hosting environment does not support saved-to-server bookmarking.") 304 } 305 306 } else { 307 # We're running Shiny locally. 308 loadInterface <- loadInterfaceLocal 309 } 310 } 311 312 loadInterface(id, loadFun) 313 314 invisible() 315 }, 316 317 # Given a query string with values encoded in it, restore saved state 318 # from those values. 319 decodeStateQueryString = function(queryString) { 320 # Remove leading '?' 321 if (substr(queryString, 1, 1) == '?') 322 queryString <- substr(queryString, 2, nchar(queryString)) 323 324 325 # Error if multiple '_inputs_' or '_values_'. This is needed because 326 # strsplit won't add an entry if the search pattern is at the end of a 327 # string. 328 if (length(gregexpr("(^|&)_inputs_(&|$)", queryString)[[1]]) > 1) 329 stop("Invalid state string: more than one '_inputs_' found") 330 if (length(gregexpr("(^|&)_values_(&|$)", queryString)[[1]]) > 1) 331 stop("Invalid state string: more than one '_values_' found") 332 333 # Look for _inputs_ and store following content in inputStr 334 splitStr <- strsplit(queryString, "(^|&)_inputs_(&|$)")[[1]] 335 if (length(splitStr) == 2) { 336 inputStr <- splitStr[2] 337 # Remove any _values_ (and content after _values_) that may come after 338 # _inputs_ 339 inputStr <- strsplit(inputStr, "(^|&)_values_(&|$)")[[1]][1] 340 341 } else { 342 inputStr <- "" 343 } 344 345 # Look for _values_ and store following content in valueStr 346 splitStr <- strsplit(queryString, "(^|&)_values_(&|$)")[[1]] 347 if (length(splitStr) == 2) { 348 valueStr <- splitStr[2] 349 # Remove any _inputs_ (and content after _inputs_) that may come after 350 # _values_ 351 valueStr <- strsplit(valueStr, "(^|&)_inputs_(&|$)")[[1]][1] 352 353 } else { 354 valueStr <- "" 355 } 356 357 358 inputs <- parseQueryString(inputStr, nested = TRUE) 359 values <- parseQueryString(valueStr, nested = TRUE) 360 361 valuesFromJSON <- function(vals) { 362 mapply(names(vals), vals, SIMPLIFY = FALSE, 363 FUN = function(name, value) { 364 tryCatch( 365 safeFromJSON(value), 366 error = function(e) { 367 stop("Failed to parse URL parameter \"", name, "\"") 368 } 369 ) 370 } 371 ) 372 } 373 374 inputs <- valuesFromJSON(inputs) 375 self$input <- RestoreInputSet$new(inputs) 376 377 values <- valuesFromJSON(values) 378 self$values <- list2env2(values, self$values) 379 } 380 ) 381) 382 383 384# Restore input set. This is basically a key-value store, except for one 385# important difference: When the user `get()`s a value, the value is marked as 386# pending; when `flushPending()` is called, those pending values are marked as 387# used. When a value is marked as used, `get()` will not return it, unless 388# called with `force=TRUE`. This is to make sure that a particular value can be 389# restored only within a single call to `withRestoreContext()`. Without this, if 390# a value is restored in a dynamic UI, it could completely prevent any other 391# (non- restored) kvalue from being used. 392RestoreInputSet <- R6Class("RestoreInputSet", 393 private = list( 394 values = NULL, 395 pending = character(0), 396 used = character(0) # Names of values which have been used 397 ), 398 399 public = list( 400 initialize = function(values) { 401 private$values <- list2env2(values, parent = emptyenv()) 402 }, 403 404 exists = function(name) { 405 exists(name, envir = private$values) 406 }, 407 408 # Return TRUE if the value exists and has not been marked as used. 409 available = function(name) { 410 self$exists(name) && !self$isUsed(name) 411 }, 412 413 isPending = function(name) { 414 name %in% private$pending 415 }, 416 417 isUsed = function(name) { 418 name %in% private$used 419 }, 420 421 # Get a value. If `force` is TRUE, get the value without checking whether 422 # has been used, and without marking it as pending. 423 get = function(name, force = FALSE) { 424 if (force) 425 return(private$values[[name]]) 426 427 if (!self$available(name)) 428 return(NULL) 429 430 # Mark this name as pending. Use unique so that it's not added twice. 431 private$pending <- unique(c(private$pending, name)) 432 private$values[[name]] 433 }, 434 435 # Take pending names and mark them as used, then clear pending list. 436 flushPending = function() { 437 private$used <- unique(c(private$used, private$pending)) 438 private$pending <- character(0) 439 }, 440 441 asList = function() { 442 as.list.environment(private$values, all.names = TRUE) 443 } 444 ) 445) 446 447# This is a fastmap::faststack(); value is assigned in .onLoad(). 448restoreCtxStack <- NULL 449 450withRestoreContext <- function(ctx, expr) { 451 restoreCtxStack$push(ctx) 452 453 on.exit({ 454 # Mark pending names as used 455 restoreCtxStack$peek()$flushPending() 456 restoreCtxStack$pop() 457 }, add = TRUE) 458 459 force(expr) 460} 461 462# Is there a current restore context? 463hasCurrentRestoreContext <- function() { 464 if (restoreCtxStack$size() > 0) 465 return(TRUE) 466 domain <- getDefaultReactiveDomain() 467 if (!is.null(domain) && !is.null(domain$restoreContext)) 468 return(TRUE) 469 470 return(FALSE) 471} 472 473# Call to access the current restore context. First look on the restore 474# context stack, and if not found, then see if there's one on the current 475# reactive domain. In practice, the only time there will be a restore context 476# on the stack is when executing the UI function; when executing server code, 477# the restore context will be attached to the domain/session. 478getCurrentRestoreContext <- function() { 479 ctx <- restoreCtxStack$peek() 480 if (is.null(ctx)) { 481 domain <- getDefaultReactiveDomain() 482 483 if (is.null(domain) || is.null(domain$restoreContext)) { 484 stop("No restore context found") 485 } 486 487 ctx <- domain$restoreContext 488 } 489 ctx 490} 491 492#' Restore an input value 493#' 494#' This restores an input value from the current restore context. It should be 495#' called early on inside of input functions (like [textInput()]). 496#' 497#' @param id Name of the input value to restore. 498#' @param default A default value to use, if there's no value to restore. 499#' 500#' @export 501restoreInput <- function(id, default) { 502 # Need to evaluate `default` in case it contains reactives like input$x. If we 503 # don't, then the calling code won't take a reactive dependency on input$x 504 # when restoring a value. 505 force(default) 506 507 if (!hasCurrentRestoreContext()) { 508 return(default) 509 } 510 511 oldInputs <- getCurrentRestoreContext()$input 512 if (oldInputs$available(id)) { 513 oldInputs$get(id) 514 } else { 515 default 516 } 517} 518 519#' Update URL in browser's location bar 520#' 521#' This function updates the client browser's query string in the location bar. 522#' It typically is called from an observer. Note that this will not work in 523#' Internet Explorer 9 and below. 524#' 525#' For `mode = "push"`, only three updates are currently allowed: 526#' \enumerate{ 527#' \item the query string (format: `?param1=val1¶m2=val2`) 528#' \item the hash (format: `#hash`) 529#' \item both the query string and the hash 530#' (format: `?param1=val1¶m2=val2#hash`) 531#' } 532#' 533#' In other words, if `mode = "push"`, the `queryString` must start 534#' with either `?` or with `#`. 535#' 536#' A technical curiosity: under the hood, this function is calling the HTML5 537#' history API (which is where the names for the `mode` argument come from). 538#' When `mode = "replace"`, the function called is 539#' `window.history.replaceState(null, null, queryString)`. 540#' When `mode = "push"`, the function called is 541#' `window.history.pushState(null, null, queryString)`. 542#' 543#' @param queryString The new query string to show in the location bar. 544#' @param mode When the query string is updated, should the the current history 545#' entry be replaced (default), or should a new history entry be pushed onto 546#' the history stack? The former should only be used in a live bookmarking 547#' context. The latter is useful if you want to navigate between states using 548#' the browser's back and forward buttons. See Examples. 549#' @param session A Shiny session object. 550#' @seealso [enableBookmarking()], [getQueryString()] 551#' @examples 552#' ## Only run these examples in interactive sessions 553#' if (interactive()) { 554#' 555#' ## App 1: Doing "live" bookmarking 556#' ## Update the browser's location bar every time an input changes. 557#' ## This should not be used with enableBookmarking("server"), 558#' ## because that would create a new saved state on disk every time 559#' ## the user changes an input. 560#' enableBookmarking("url") 561#' shinyApp( 562#' ui = function(req) { 563#' fluidPage( 564#' textInput("txt", "Text"), 565#' checkboxInput("chk", "Checkbox") 566#' ) 567#' }, 568#' server = function(input, output, session) { 569#' observe({ 570#' # Trigger this observer every time an input changes 571#' reactiveValuesToList(input) 572#' session$doBookmark() 573#' }) 574#' onBookmarked(function(url) { 575#' updateQueryString(url) 576#' }) 577#' } 578#' ) 579#' 580#' ## App 2: Printing the value of the query string 581#' ## (Use the back and forward buttons to see how the browser 582#' ## keeps a record of each state) 583#' shinyApp( 584#' ui = fluidPage( 585#' textInput("txt", "Enter new query string"), 586#' helpText("Format: ?param1=val1¶m2=val2"), 587#' actionButton("go", "Update"), 588#' hr(), 589#' verbatimTextOutput("query") 590#' ), 591#' server = function(input, output, session) { 592#' observeEvent(input$go, { 593#' updateQueryString(input$txt, mode = "push") 594#' }) 595#' output$query <- renderText({ 596#' query <- getQueryString() 597#' queryText <- paste(names(query), query, 598#' sep = "=", collapse=", ") 599#' paste("Your query string is:\n", queryText) 600#' }) 601#' } 602#' ) 603#' } 604#' @export 605updateQueryString <- function(queryString, mode = c("replace", "push"), 606 session = getDefaultReactiveDomain()) { 607 mode <- match.arg(mode) 608 session$updateQueryString(queryString, mode) 609} 610 611#' Create a button for bookmarking/sharing 612#' 613#' A `bookmarkButton` is a [actionButton()] with a default label 614#' that consists of a link icon and the text "Bookmark...". It is meant to be 615#' used for bookmarking state. 616#' 617#' @inheritParams actionButton 618#' @param title A tooltip that is shown when the mouse cursor hovers over the 619#' button. 620#' @param id An ID for the bookmark button. The only time it is necessary to set 621#' the ID unless you have more than one bookmark button in your application. 622#' If you specify an input ID, it should be excluded from bookmarking with 623#' [setBookmarkExclude()], and you must create an observer that 624#' does the bookmarking when the button is pressed. See the examples below. 625#' 626#' @seealso [enableBookmarking()] for more examples. 627#' 628#' @examples 629#' ## Only run these examples in interactive sessions 630#' if (interactive()) { 631#' 632#' # This example shows how to use multiple bookmark buttons. If you only need 633#' # a single bookmark button, see examples in ?enableBookmarking. 634#' ui <- function(request) { 635#' fluidPage( 636#' tabsetPanel(id = "tabs", 637#' tabPanel("One", 638#' checkboxInput("chk1", "Checkbox 1"), 639#' bookmarkButton(id = "bookmark1") 640#' ), 641#' tabPanel("Two", 642#' checkboxInput("chk2", "Checkbox 2"), 643#' bookmarkButton(id = "bookmark2") 644#' ) 645#' ) 646#' ) 647#' } 648#' server <- function(input, output, session) { 649#' # Need to exclude the buttons from themselves being bookmarked 650#' setBookmarkExclude(c("bookmark1", "bookmark2")) 651#' 652#' # Trigger bookmarking with either button 653#' observeEvent(input$bookmark1, { 654#' session$doBookmark() 655#' }) 656#' observeEvent(input$bookmark2, { 657#' session$doBookmark() 658#' }) 659#' } 660#' enableBookmarking(store = "url") 661#' shinyApp(ui, server) 662#' } 663#' @export 664bookmarkButton <- function(label = "Bookmark...", 665 icon = shiny::icon("link", lib = "glyphicon"), 666 title = "Bookmark this application's state and get a URL for sharing.", 667 ..., 668 id = "._bookmark_") 669{ 670 actionButton(id, label, icon, title = title, ...) 671} 672 673 674#' Generate a modal dialog that displays a URL 675#' 676#' The modal dialog generated by `urlModal` will display the URL in a 677#' textarea input, and the URL text will be selected so that it can be easily 678#' copied. The result from `urlModal` should be passed to the 679#' [showModal()] function to display it in the browser. 680#' 681#' @param url A URL to display in the dialog box. 682#' @param title A title for the dialog box. 683#' @param subtitle Text to display underneath URL. 684#' @export 685urlModal <- function(url, title = "Bookmarked application link", subtitle = NULL) { 686 687 subtitleTag <- tagList( 688 br(), 689 span(class = "text-muted", subtitle), 690 span(id = "shiny-bookmark-copy-text", class = "text-muted") 691 ) 692 693 modalDialog( 694 title = title, 695 easyClose = TRUE, 696 tags$textarea(class = "form-control", rows = "1", style = "resize: none;", 697 readonly = "readonly", 698 url 699 ), 700 subtitleTag, 701 # Need separate show and shown listeners. The show listener sizes the 702 # textarea just as the modal starts to fade in. The 200ms delay is needed 703 # because if we try to resize earlier, it can't calculate the text height 704 # (scrollHeight will be reported as zero). The shown listener selects the 705 # text; it's needed because because selection has to be done after the fade- 706 # in is completed. 707 tags$script( 708 "$('#shiny-modal'). 709 one('show.bs.modal', function() { 710 setTimeout(function() { 711 var $textarea = $('#shiny-modal textarea'); 712 $textarea.innerHeight($textarea[0].scrollHeight); 713 }, 200); 714 }); 715 $('#shiny-modal') 716 .one('shown.bs.modal', function() { 717 $('#shiny-modal textarea').select().focus(); 718 }); 719 $('#shiny-bookmark-copy-text') 720 .text(function() { 721 if (/Mac/i.test(navigator.userAgent)) { 722 return 'Press \u2318-C to copy.'; 723 } else { 724 return 'Press Ctrl-C to copy.'; 725 } 726 }); 727 " 728 ) 729 ) 730} 731 732 733#' Display a modal dialog for bookmarking 734#' 735#' This is a wrapper function for [urlModal()] that is automatically 736#' called if an application is bookmarked but no other [onBookmark()] 737#' callback was set. It displays a modal dialog with the bookmark URL, along 738#' with a subtitle that is appropriate for the type of bookmarking used ("url" 739#' or "server"). 740#' 741#' @param url A URL to show in the modal dialog. 742#' @export 743showBookmarkUrlModal <- function(url) { 744 store <- getShinyOption("bookmarkStore", default = "") 745 if (store == "url") { 746 subtitle <- "This link stores the current state of this application." 747 } else if (store == "server") { 748 subtitle <- "The current state of this application has been stored on the server." 749 } else { 750 subtitle <- NULL 751 } 752 753 showModal(urlModal(url, subtitle = subtitle)) 754} 755 756#' Enable bookmarking for a Shiny application 757#' 758#' @description 759#' 760#' There are two types of bookmarking: saving an application's state to disk on 761#' the server, and encoding the application's state in a URL. For state that has 762#' been saved to disk, the state can be restored with the corresponding state 763#' ID. For URL-encoded state, the state of the application is encoded in the 764#' URL, and no server-side storage is needed. 765#' 766#' URL-encoded bookmarking is appropriate for applications where there not many 767#' input values that need to be recorded. Some browsers have a length limit for 768#' URLs of about 2000 characters, and if there are many inputs, the length of 769#' the URL can exceed that limit. 770#' 771#' Saved-on-server bookmarking is appropriate when there are many inputs, or 772#' when the bookmarked state requires storing files. 773#' 774#' @details 775#' 776#' For restoring state to work properly, the UI must be a function that takes 777#' one argument, `request`. In most Shiny applications, the UI is not a 778#' function; it might have the form `fluidPage(....)`. Converting it to a 779#' function is as simple as wrapping it in a function, as in 780#' \code{function(request) \{ fluidPage(....) \}}. 781#' 782#' By default, all input values will be bookmarked, except for the values of 783#' passwordInputs. fileInputs will be saved if the state is saved on a server, 784#' but not if the state is encoded in a URL. 785#' 786#' When bookmarking state, arbitrary values can be stored, by passing a function 787#' as the `onBookmark` argument. That function will be passed a 788#' `ShinySaveState` object. The `values` field of the object is a list 789#' which can be manipulated to save extra information. Additionally, if the 790#' state is being saved on the server, and the `dir` field of that object 791#' can be used to save extra information to files in that directory. 792#' 793#' For saved-to-server state, this is how the state directory is chosen: 794#' \itemize{ 795#' \item If running in a hosting environment such as Shiny Server or 796#' Connect, the hosting environment will choose the directory. 797#' \item If running an app in a directory with [runApp()], the 798#' saved states will be saved in a subdirectory of the app called 799#' shiny_bookmarks. 800#' \item If running a Shiny app object that is generated from code (not run 801#' from a directory), the saved states will be saved in a subdirectory of 802#' the current working directory called shiny_bookmarks. 803#' } 804#' 805#' When used with [shinyApp()], this function must be called before 806#' `shinyApp()`, or in the `shinyApp()`'s `onStart` function. An 807#' alternative to calling the `enableBookmarking()` function is to use the 808#' `enableBookmarking` *argument* for `shinyApp()`. See examples 809#' below. 810#' 811#' @param store Either `"url"`, which encodes all of the relevant values in 812#' a URL, `"server"`, which saves to disk on the server, or 813#' `"disable"`, which disables any previously-enabled bookmarking. 814#' 815#' @seealso [onBookmark()], [onBookmarked()], 816#' [onRestore()], and [onRestored()] for registering 817#' callback functions that are invoked when the state is bookmarked or 818#' restored. 819#' 820#' Also see [updateQueryString()]. 821#' 822#' @export 823#' @examples 824#' ## Only run these examples in interactive R sessions 825#' if (interactive()) { 826#' 827#' # Basic example with state encoded in URL 828#' ui <- function(request) { 829#' fluidPage( 830#' textInput("txt", "Text"), 831#' checkboxInput("chk", "Checkbox"), 832#' bookmarkButton() 833#' ) 834#' } 835#' server <- function(input, output, session) { } 836#' enableBookmarking("url") 837#' shinyApp(ui, server) 838#' 839#' 840#' # An alternative to calling enableBookmarking(): use shinyApp's 841#' # enableBookmarking argument 842#' shinyApp(ui, server, enableBookmarking = "url") 843#' 844#' 845#' # Same basic example with state saved to disk 846#' enableBookmarking("server") 847#' shinyApp(ui, server) 848#' 849#' 850#' # Save/restore arbitrary values 851#' ui <- function(req) { 852#' fluidPage( 853#' textInput("txt", "Text"), 854#' checkboxInput("chk", "Checkbox"), 855#' bookmarkButton(), 856#' br(), 857#' textOutput("lastSaved") 858#' ) 859#' } 860#' server <- function(input, output, session) { 861#' vals <- reactiveValues(savedTime = NULL) 862#' output$lastSaved <- renderText({ 863#' if (!is.null(vals$savedTime)) 864#' paste("Last saved at", vals$savedTime) 865#' else 866#' "" 867#' }) 868#' 869#' onBookmark(function(state) { 870#' vals$savedTime <- Sys.time() 871#' # state is a mutable reference object, and we can add arbitrary values 872#' # to it. 873#' state$values$time <- vals$savedTime 874#' }) 875#' onRestore(function(state) { 876#' vals$savedTime <- state$values$time 877#' }) 878#' } 879#' enableBookmarking(store = "url") 880#' shinyApp(ui, server) 881#' 882#' 883#' # Usable with dynamic UI (set the slider, then change the text input, 884#' # click the bookmark button) 885#' ui <- function(request) { 886#' fluidPage( 887#' sliderInput("slider", "Slider", 1, 100, 50), 888#' uiOutput("ui"), 889#' bookmarkButton() 890#' ) 891#' } 892#' server <- function(input, output, session) { 893#' output$ui <- renderUI({ 894#' textInput("txt", "Text", input$slider) 895#' }) 896#' } 897#' enableBookmarking("url") 898#' shinyApp(ui, server) 899#' 900#' 901#' # Exclude specific inputs (The only input that will be saved in this 902#' # example is chk) 903#' ui <- function(request) { 904#' fluidPage( 905#' passwordInput("pw", "Password"), # Passwords are never saved 906#' sliderInput("slider", "Slider", 1, 100, 50), # Manually excluded below 907#' checkboxInput("chk", "Checkbox"), 908#' bookmarkButton() 909#' ) 910#' } 911#' server <- function(input, output, session) { 912#' setBookmarkExclude("slider") 913#' } 914#' enableBookmarking("url") 915#' shinyApp(ui, server) 916#' 917#' 918#' # Update the browser's location bar every time an input changes. This should 919#' # not be used with enableBookmarking("server"), because that would create a 920#' # new saved state on disk every time the user changes an input. 921#' ui <- function(req) { 922#' fluidPage( 923#' textInput("txt", "Text"), 924#' checkboxInput("chk", "Checkbox") 925#' ) 926#' } 927#' server <- function(input, output, session) { 928#' observe({ 929#' # Trigger this observer every time an input changes 930#' reactiveValuesToList(input) 931#' session$doBookmark() 932#' }) 933#' onBookmarked(function(url) { 934#' updateQueryString(url) 935#' }) 936#' } 937#' enableBookmarking("url") 938#' shinyApp(ui, server) 939#' 940#' 941#' # Save/restore uploaded files 942#' ui <- function(request) { 943#' fluidPage( 944#' sidebarLayout( 945#' sidebarPanel( 946#' fileInput("file1", "Choose CSV File", multiple = TRUE, 947#' accept = c( 948#' "text/csv", 949#' "text/comma-separated-values,text/plain", 950#' ".csv" 951#' ) 952#' ), 953#' tags$hr(), 954#' checkboxInput("header", "Header", TRUE), 955#' bookmarkButton() 956#' ), 957#' mainPanel( 958#' tableOutput("contents") 959#' ) 960#' ) 961#' ) 962#' } 963#' server <- function(input, output) { 964#' output$contents <- renderTable({ 965#' inFile <- input$file1 966#' if (is.null(inFile)) 967#' return(NULL) 968#' 969#' if (nrow(inFile) == 1) { 970#' read.csv(inFile$datapath, header = input$header) 971#' } else { 972#' data.frame(x = "multiple files") 973#' } 974#' }) 975#' } 976#' enableBookmarking("server") 977#' shinyApp(ui, server) 978#' 979#' } 980enableBookmarking <- function(store = c("url", "server", "disable")) { 981 store <- match.arg(store) 982 shinyOptions(bookmarkStore = store) 983} 984 985 986#' Exclude inputs from bookmarking 987#' 988#' This function tells Shiny which inputs should be excluded from bookmarking. 989#' It should be called from inside the application's server function. 990#' 991#' This function can also be called from a module's server function, in which 992#' case it will exclude inputs with the specified names, from that module. It 993#' will not affect inputs from other modules or from the top level of the Shiny 994#' application. 995#' 996#' @param names A character vector containing names of inputs to exclude from 997#' bookmarking. 998#' @param session A shiny session object. 999#' @seealso [enableBookmarking()] for examples. 1000#' @export 1001setBookmarkExclude <- function(names = character(0), session = getDefaultReactiveDomain()) { 1002 session$setBookmarkExclude(names) 1003} 1004 1005 1006#' Add callbacks for Shiny session bookmarking events 1007#' 1008#' @description 1009#' 1010#' These functions are for registering callbacks on Shiny session events. They 1011#' should be called within an application's server function. 1012#' 1013#' \itemize{ 1014#' \item `onBookmark` registers a function that will be called just 1015#' before Shiny bookmarks state. 1016#' \item `onBookmarked` registers a function that will be called just 1017#' after Shiny bookmarks state. 1018#' \item `onRestore` registers a function that will be called when a 1019#' session is restored, after the server function executes, but before all 1020#' other reactives, observers and render functions are run. 1021#' \item `onRestored` registers a function that will be called after a 1022#' session is restored. This is similar to `onRestore`, but it will be 1023#' called after all reactives, observers, and render functions run, and 1024#' after results are sent to the client browser. `onRestored` 1025#' callbacks can be useful for sending update messages to the client 1026#' browser. 1027#' } 1028#' 1029#' @details 1030#' 1031#' All of these functions return a function which can be called with no 1032#' arguments to cancel the registration. 1033#' 1034#' The callback function that is passed to these functions should take one 1035#' argument, typically named "state" (for `onBookmark`, `onRestore`, 1036#' and `onRestored`) or "url" (for `onBookmarked`). 1037#' 1038#' For `onBookmark`, the state object has three relevant fields. The 1039#' `values` field is an environment which can be used to save arbitrary 1040#' values (see examples). If the state is being saved to disk (as opposed to 1041#' being encoded in a URL), the `dir` field contains the name of a 1042#' directory which can be used to store extra files. Finally, the state object 1043#' has an `input` field, which is simply the application's `input` 1044#' object. It can be read, but not modified. 1045#' 1046#' For `onRestore` and `onRestored`, the state object is a list. This 1047#' list contains `input`, which is a named list of input values to restore, 1048#' `values`, which is an environment containing arbitrary values that were 1049#' saved in `onBookmark`, and `dir`, the name of the directory that 1050#' the state is being restored from, and which could have been used to save 1051#' extra files. 1052#' 1053#' For `onBookmarked`, the callback function receives a string with the 1054#' bookmark URL. This callback function should be used to display UI in the 1055#' client browser with the bookmark URL. If no callback function is registered, 1056#' then Shiny will by default display a modal dialog with the bookmark URL. 1057#' 1058#' @section Modules: 1059#' 1060#' These callbacks may also be used in Shiny modules. When used this way, the 1061#' inputs and values will automatically be namespaced for the module, and the 1062#' callback functions registered for the module will only be able to see the 1063#' module's inputs and values. 1064#' 1065#' @param fun A callback function which takes one argument. 1066#' @param session A shiny session object. 1067#' @seealso enableBookmarking for general information on bookmarking. 1068#' 1069#' @examples 1070#' ## Only run these examples in interactive sessions 1071#' if (interactive()) { 1072#' 1073#' # Basic use of onBookmark and onRestore: This app saves the time in its 1074#' # arbitrary values, and restores that time when the app is restored. 1075#' ui <- function(req) { 1076#' fluidPage( 1077#' textInput("txt", "Input text"), 1078#' bookmarkButton() 1079#' ) 1080#' } 1081#' server <- function(input, output) { 1082#' onBookmark(function(state) { 1083#' savedTime <- as.character(Sys.time()) 1084#' cat("Last saved at", savedTime, "\n") 1085#' # state is a mutable reference object, and we can add arbitrary values to 1086#' # it. 1087#' state$values$time <- savedTime 1088#' }) 1089#' 1090#' onRestore(function(state) { 1091#' cat("Restoring from state bookmarked at", state$values$time, "\n") 1092#' }) 1093#' } 1094#' enableBookmarking("url") 1095#' shinyApp(ui, server) 1096#' 1097#' 1098#' 1099# This app illustrates two things: saving values in a file using state$dir, and 1100# using an onRestored callback to call an input updater function. (In real use 1101# cases, it probably makes sense to save content to a file only if it's much 1102# larger.) 1103#' ui <- function(req) { 1104#' fluidPage( 1105#' textInput("txt", "Input text"), 1106#' bookmarkButton() 1107#' ) 1108#' } 1109#' server <- function(input, output, session) { 1110#' lastUpdateTime <- NULL 1111#' 1112#' observeEvent(input$txt, { 1113#' updateTextInput(session, "txt", 1114#' label = paste0("Input text (Changed ", as.character(Sys.time()), ")") 1115#' ) 1116#' }) 1117#' 1118#' onBookmark(function(state) { 1119#' # Save content to a file 1120#' messageFile <- file.path(state$dir, "message.txt") 1121#' cat(as.character(Sys.time()), file = messageFile) 1122#' }) 1123#' 1124#' onRestored(function(state) { 1125#' # Read the file 1126#' messageFile <- file.path(state$dir, "message.txt") 1127#' timeText <- readChar(messageFile, 1000) 1128#' 1129#' # updateTextInput must be called in onRestored, as opposed to onRestore, 1130#' # because onRestored happens after the client browser is ready. 1131#' updateTextInput(session, "txt", 1132#' label = paste0("Input text (Changed ", timeText, ")") 1133#' ) 1134#' }) 1135#' } 1136#' # "server" bookmarking is needed for writing to disk. 1137#' enableBookmarking("server") 1138#' shinyApp(ui, server) 1139#' 1140#' 1141#' # This app has a module, and both the module and the main app code have 1142#' # onBookmark and onRestore functions which write and read state$values$hash. The 1143#' # module's version of state$values$hash does not conflict with the app's version 1144#' # of state$values$hash. 1145#' # 1146#' # A basic module that captializes text. 1147#' capitalizerUI <- function(id) { 1148#' ns <- NS(id) 1149#' wellPanel( 1150#' h4("Text captializer module"), 1151#' textInput(ns("text"), "Enter text:"), 1152#' verbatimTextOutput(ns("out")) 1153#' ) 1154#' } 1155#' capitalizerServer <- function(input, output, session) { 1156#' output$out <- renderText({ 1157#' toupper(input$text) 1158#' }) 1159#' onBookmark(function(state) { 1160#' state$values$hash <- rlang::hash(input$text) 1161#' }) 1162#' onRestore(function(state) { 1163#' if (identical(rlang::hash(input$text), state$values$hash)) { 1164#' message("Module's input text matches hash ", state$values$hash) 1165#' } else { 1166#' message("Module's input text does not match hash ", state$values$hash) 1167#' } 1168#' }) 1169#' } 1170#' # Main app code 1171#' ui <- function(request) { 1172#' fluidPage( 1173#' sidebarLayout( 1174#' sidebarPanel( 1175#' capitalizerUI("tc"), 1176#' textInput("text", "Enter text (not in module):"), 1177#' bookmarkButton() 1178#' ), 1179#' mainPanel() 1180#' ) 1181#' ) 1182#' } 1183#' server <- function(input, output, session) { 1184#' callModule(capitalizerServer, "tc") 1185#' onBookmark(function(state) { 1186#' state$values$hash <- rlang::hash(input$text) 1187#' }) 1188#' onRestore(function(state) { 1189#' if (identical(rlang::hash(input$text), state$values$hash)) { 1190#' message("App's input text matches hash ", state$values$hash) 1191#' } else { 1192#' message("App's input text does not match hash ", state$values$hash) 1193#' } 1194#' }) 1195#' } 1196#' enableBookmarking(store = "url") 1197#' shinyApp(ui, server) 1198#' } 1199#' @export 1200onBookmark <- function(fun, session = getDefaultReactiveDomain()) { 1201 session$onBookmark(fun) 1202} 1203 1204#' @rdname onBookmark 1205#' @export 1206onBookmarked <- function(fun, session = getDefaultReactiveDomain()) { 1207 session$onBookmarked(fun) 1208} 1209 1210#' @rdname onBookmark 1211#' @export 1212onRestore <- function(fun, session = getDefaultReactiveDomain()) { 1213 session$onRestore(fun) 1214} 1215 1216#' @rdname onBookmark 1217#' @export 1218onRestored <- function(fun, session = getDefaultReactiveDomain()) { 1219 session$onRestored(fun) 1220} 1221