Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding a state list to avoid update...Input #1

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
2 changes: 2 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
^.*\.Rproj$
^\.Rproj\.user$
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.Rproj.user
.Rhistory
.RData
shinyapps
16 changes: 16 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Package: resume
Title: Resume a shiny-app
Version: 0.0.1
Date: 2015-4-1
Authors@R: person("Vincent", "Nijs", , "radiant@rady.ucsd.edu", c("aut", "cre"))
Description:
Resume a shiny app (this is actually Joe Cheng's package - I am just testing this out)
Depends:
R (>= 3.1.0),
dplyr (>= 0.4.1),
magrittr (>= 1.5),
shiny (>= 0.11.1)
URL: https://github.com/vnijs/shiny-resume
BugReports: https://github.com/vnijs/shiny-resume/issues
License: AGPL-3
LazyData: true
6 changes: 6 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Generated by roxygen2 (4.1.0): do not edit by hand

export(resume)
export(state_init)
export(state_multiple)
export(state_single)
9 changes: 9 additions & 0 deletions R/resume.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#' Launch Resume in the default browser
#'
#' @details ...
#'
#' @param app Choose one
#'
#' @export
resume <- function(app = "")
runApp(system.file("test", package="resume"), launch.browser = TRUE)
78 changes: 78 additions & 0 deletions R/state.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#' Set initial value for shiny input
#'
#' @details Useful for radio button or checkbox
#'
#' @param inputvar Name shiny input
#' @param init Initial value to use if state value for input not set
#'
#' @return value for inputvar
#'
#' @examples
#'
#' r_state <- list()
#' state_init("test")
#' state_init("test",0)
#' r_state$test <- c("a","b")
#' state_init("test",0)
#'
#' @seealso \code{\link{state_single}}
#' @seealso \code{\link{state_multiple}}
#'
#' @export
state_init <- function(inputvar, init = "")
r_state %>% { if(is.null(.[[inputvar]])) init else .[[inputvar]] }

#' Set initial value for shiny input from a list of values
#'
#' @details Useful for select input with multiple = FALSE
#'
#' @param inputvar Name shiny input
#' @param vals Possible values for inputvar
#' @param init Initial value to use if state value for input not set
#'
#' @return value for inputvar
#'
#' @examples
#'
#' r_state <- list()
#' state_single("test",1:10,1)
#' r_state$test <- 8
#' state_single("test",1:10,1)
#'
#' @seealso \code{\link{state_init}}
#' @seealso \code{\link{state_multiple}}
#'
#' @export
state_single <- function(inputvar, vals, init = character(0))
r_state %>% { if(is.null(.[[inputvar]])) init else vals[vals == .[[inputvar]]] }

#' Set initial values for shiny input from a list of values
#'
#' @details Useful for select input with multiple = TRUE and when you want to use inputs selected for another tool
#'
#' @param inputvar Name shiny input
#' @param vals Possible values for inputvar
#' @param init Initial value to use if state value for input not set
#'
#' @return value for inputvar
#'
#' @examples
#'
#' r_state <- list()
#' state_multiple("test",1:10,1:3)
#' r_state$test <- 8:10
#' state_multiple("test",1:10,1:3)
#'
#' @seealso \code{\link{state_init}}
#' @seealso \code{\link{state_single}}
#'
#' @export
state_multiple <- function(inputvar, vals, init = character(0)) {
# "a" %in% character(0) --> FALSE, letters[FALSE] --> character(0)
r_state %>%
{ if(is.null(.[[inputvar]]))
vals[vals %in% init]
else
vals[vals %in% .[[inputvar]]]
}
}
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
This is a proof of concept of saving/restoring session data for a Shiny app, allowing browser refreshes not to lose all of the state that an app has set up.
This is fork from [https://github.com/jcheng5/shiny-resume](shiny-resume). This is a proof of concept of saving/restoring session data for a Shiny app to avoid losing state on browser refresh. If can also be used to save and share state. See [vnijs/radiant](https://github.com/vnijs/radiant) for a related approach.

The logic for save and restore must be provided by the app author, using a call to `manageSession(save, restore)` during session start. The `save` parameter must be a reactive expression, while `restore` is a single-arg function that will be invoked with a value returned by `save`.

Serious limitations at the moment, that make this not well suited for production use:

1. All session data is stored in an in-memory environment, so when the R process ends or the app is restarted, all sessions are wiped.
2. Sessions are only created or overwritten, never destroyed, so memory usage will climb with each session.
Functionality to be added:
1. Save session information to file when the R-process ends.
2. Remove old sessions

To run the app:

devtools::install_github("vnijs/shiny-resume")
library(resume)
resume()

Binary file added inst/.DS_Store
Binary file not shown.
101 changes: 101 additions & 0 deletions inst/test/server.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
source("session.R")
library(magrittr)

# a few utility funcs, might as well take them from `import`
import::here(symbol_list, symbol_as_character, .from = import)

copy_from <- function(.from, ...) {
symbols <- symbol_list(...)
parent <- parent.frame()
from <- symbol_as_character(substitute(.from))

for (s in seq_along(symbols)) {
fn <- get(symbols[s], envir = asNamespace(from), inherits = TRUE)
assign(names(symbols)[s],
eval.parent(call("function", formals(fn), body(fn))),
parent)
}

invisible(NULL)
}

function(input, output, session) {

# initialize state containers
r_data <- reactiveValues()
r_data$clicks <- 0
r_state <- list()

# the default approach in shiny apps
# source("../../R/state.R", local = TRUE)

# trying import
# import::here(resume, state_init, state_single, state_multiple)
# import::here(.from = resume, state_init, state_single, state_multiple)
# import::here(state_init, state_single, state_multiple, .from = resume)

copy_from(resume, state_init, state_single, state_multiple)

# this would be nice
# import::here(resume, state*)

# The call to manageSession starts a new session, but if we are continuing a
# previous session it first calls restore() with the saved data.
manageSession(
save_state = reactive({
reactiveValuesToList(input)
}),
save_data = reactive({
reactiveValuesToList(r_data)
}),
restore = function(data) {
r_data <<- do.call(reactiveValues, data$r_data)
r_state <<- data$r_state

r_data$clicks <- r_data[["clicks"]]
updateNumericInput(session, "remember", value = r_state[["remember"]])
}
)
output$ui_state <- renderUI({
vals <- c("a","b","c")
# select for single value
tagList(
tags$textarea(id="message", rows="2", class="form-control",
state_init("message","[empty]")),
radioButtons("radio", label = "Option:", choices = vals,
selected = state_init("radio", "a"), inline = TRUE),
checkboxGroupInput("check", label = "Options:", choices = vals,
selected = state_init("check", "a"), inline = TRUE),
selectInput("select_one", label = "Select one:", choices = vals,
selected = state_single("select_one", vals), multiple = FALSE),
selectInput("select_two", label = "Select two (or more):", choices = vals,
selected = state_multiple("select_two", vals), multiple = TRUE)
)
})

observeEvent(input$click, {
r_data$clicks <- r_data$clicks + 1
})

output$values_selected <- renderPrint({
cat("Click count: ")
cat(r_data$clicks)
cat("\nSelected one: ")
cat(input$select_one)
cat("\nSelected two: ")
cat(input$select_two)
cat("\nRemember: ")
cat(input$remember)
})

output$state_values <- renderPrint({
cat("r_data:\n")
print(reactiveValuesToList(r_data))
cat("\nr_state:\n")
if(length(r_state) == 0)
cat("[empty]")
else
str(r_state[sort(names(r_state))])

})
}
15 changes: 10 additions & 5 deletions session.R → inst/test/session.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,29 @@ library(shiny)

sessionStore <- new.env(parent = emptyenv())

manageSession <- function(save, restore, session = getDefaultReactiveDomain()) {
manageSession <- function(save_state, save_data, restore, session = getDefaultReactiveDomain()) {

isolate({
params <- parseQueryString(session$clientData$url_search)
prevSSUID = params[["SSUID"]]
prevSSUID <- params[["SSUID"]]
if (!is.null(prevSSUID)) {
if (!is.null(sessionStore[[prevSSUID]])) {
restore(sessionStore[[prevSSUID]]$data)
restore(sessionStore[[prevSSUID]])
}
}
})

ssuid <- shiny:::createUniqueId(16)
if(is.null(prevSSUID))
ssuid <- shiny:::createUniqueId(16)
else
ssuid <- prevSSUID

session$sendCustomMessage("session_start", ssuid)
observe({
tryCatch(
sessionStore[[ssuid]] <- list(
data = save(),
r_data = save_data(),
r_state = save_state(),
timestamp = Sys.time()
),
error = function(e) {
Expand Down
31 changes: 31 additions & 0 deletions inst/test/test.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
library(magrittr)

# a few utility funcs, might as well take them from `import`
import::here(symbol_list, symbol_as_character, .from = import)

copy_from <- function(.from, ...) {
symbols <- symbol_list(...)
parent <- parent.frame()
from <- symbol_as_character(substitute(.from))

for (s in seq_along(symbols)) {
fn <- get(symbols[s], envir = asNamespace(from), inherits = TRUE)
assign(names(symbols)[s],
eval.parent(call("function", formals(fn), body(fn))),
parent)
}

invisible(NULL)
}

test <- function() {

# source("../../R/state.R", local = TRUE)
# import::here(state_init, state_single, state_multiple, .from = resume)
copy_from(resume, state_init, state_single, state_multiple)

r_state <- list()
state_init("test") %>% print
}

test()
12 changes: 12 additions & 0 deletions inst/test/ui.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
fluidPage(
# This line is required for the session ID to appear in the URL
tags$script(src="session.js"),

h3("Saving state demo"),
hr(),
uiOutput("ui_state"),
actionButton("click", "Click me"),
numericInput("remember", "Remember this value:", 0),
verbatimTextOutput("values_selected"),
verbatimTextOutput("state_values")
)
File renamed without changes.
18 changes: 18 additions & 0 deletions man/resume.Rd
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
% Generated by roxygen2 (4.1.0): do not edit by hand
% Please edit documentation in R/resume.R
\name{resume}
\alias{resume}
\title{Launch Resume in the default browser}
\usage{
resume(app = "")
}
\arguments{
\item{app}{Choose one}
}
\description{
Launch Resume in the default browser
}
\details{
...
}

35 changes: 35 additions & 0 deletions man/state_init.Rd
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
% Generated by roxygen2 (4.1.0): do not edit by hand
% Please edit documentation in R/state.R
\name{state_init}
\alias{state_init}
\title{Set initial value for shiny input}
\usage{
state_init(inputvar, init = "")
}
\arguments{
\item{inputvar}{Name shiny input}

\item{init}{Initial value to use if state value for input not set}
}
\value{
value for inputvar
}
\description{
Set initial value for shiny input
}
\details{
Useful for radio button or checkbox
}
\examples{
r_state <- list()
state_init("test")
state_init("test",0)
r_state$test <- c("a","b")
state_init("test",0)
}
\seealso{
\code{\link{state_single}}

\code{\link{state_multiple}}
}

Loading