Suspending event scheduling in Shiny - r

I have an app with two observeEvent() handlers reacting to input A and input B and doing some stuff. Among the stuff for event A, is updating input B.
shinyApp(
ui = fluidPage(
selectInput("A", "Input A", c(1:5)),
selectInput("B", "Input B", c(6:10))
),
server = function(input, output, session) {
observeEvent(input$A, ignoreInit = TRUE, {
message("Doing A stuff")
updateSelectInput(session, "B", selected = 10)
})
observeEvent(input$B, ignoreInit = TRUE, {
message("Doing B stuff")
})
}
)
So changing input A obviously triggers event B as well. I would like event B to be triggered only when the user is changing the value of the input and not when it is done by updateInput. Is there a way to suspend scheduling events when a expression is evaluated? I would like something like this:
shinyApp(
ui = fluidPage(
selectInput("A", "Input A", c(1:5)),
selectInput("B", "Input B", c(6:10))
),
server = function(input, output, session) {
observeEvent(input$A, ignoreInit = TRUE, {
message("Doing A stuff")
suspendEventScheduling()
updateSelectInput(session, "B", selected = 10)
resumeEventScheduling()
})
observeEvent(input$B, ignoreInit = TRUE, {
message("Doing B stuff")
})
}
)
Documentation for observers mentions "suspended state" but I cannot find any examples as to how to actually use it.

In the past I have used a sentinel value pattern to work around these types of situations (see below). But it always feels very fragile. Hopefully this feature request leads to better options.
library(shiny)
shinyApp(
ui = fluidPage(
selectInput("A", "Input A", c(1:5)),
selectInput("B", "Input B", c(6:10))
),
server = function(input, output, session) {
is_server_update <- FALSE
observeEvent(input$A, {
message("Doing A stuff")
updateSelectInput(session, "B", selected = 10)
# Unchanged value doesn't trigger an invalidation
if (input$B != 10) {
is_server_update <<- TRUE
}
}, ignoreInit = TRUE)
observeEvent(input$B, {
if (is_server_update) {
is_server_update <<- FALSE
} else {
message("Doing B stuff")
}
}, ignoreInit = TRUE)
}
)

After some playing around, I put together a bit of JavaScript that should do
the trick.
The idea is to keep track of suspended inputs whose values should not
change. Using an event hook, we can then check if an input event targets
one of the suspended inputs. If so, prevent it from making changes. Crucially
though, the UI still gets updated – just not the Shiny input values.
We then also need a couple of helper functions to manage the list of
suspended inputs. Here’s the JavaScript, and the R helpers:
js <-
"
// Don't actually modify the Shiny object in 'real' code!
Shiny.suspendedInputs = new Set();
$(document).on('shiny:inputchanged', function(event) {
Shiny.suspendedInputs.has(event.target.id) && event.preventDefault();
})
Shiny.addCustomMessageHandler('suspendinput', function(message) {
Shiny.suspendedInputs.add(message.id);
});
Shiny.addCustomMessageHandler('resumeinput', function(message) {
Shiny.suspendedInputs.delete(message.id);
// Last value that Shiny got is probably out of sync with the UI
Shiny.forgetLastInputValue(message.id);
})
"
suspendInput <- function(inputId, session = getDefaultReactiveDomain()) {
session$sendCustomMessage("suspendinput", list(id = inputId))
}
resumeInput <- function(inputId, session = getDefaultReactiveDomain()) {
session$sendCustomMessage("resumeinput", list(id = inputId))
}
Almost always the suspend and resume messages should be sent on different
flush cycles. Otherwise the resume is executed before the input events from
any updates have triggered, resulting in nothing happening. Another helper
to ensure “correct” usage would be in order:
suspendForNextFlush <- function(inputId, session = getDefaultReactiveDomain()) {
session$onFlush(function() suspendInput(inputId, session = session))
session$onFlushed(function() resumeInput(inputId, session = session))
}
And now we are ready to put everything together for a working app:
library(shiny)
shinyApp(
ui = fluidPage(
tags$script(HTML(js)),
selectInput("A", "Input A", c(1:5)),
selectInput("B", "Input B", c(6:10))
),
server = function(input, output, session) {
observeEvent(input$A, {
message("Doing A stuff")
suspendForNextFlush("B")
updateSelectInput(session, "B", selected = 10)
}, ignoreInit = TRUE)
observeEvent(input$B, {
message("Doing B stuff")
}, ignoreInit = TRUE)
}
)

Related

Mutually update material switch in shiny app

I have to swtiches. I want each switch to force the other one to FALSE should they become TRUE. (e.g. if you 'toggle' switch1 (= TRUE), and switch2 = TRUE, then switch2 should be changed to FALSE).
This is a simplified version of what I'm trying to achieve:
library(shiny)
library(shinydashboard)
library(shinydashboardPlus)
ui <- fluidPage(
materialSwitch(inputId = "switch1", label = "Switch 1", status = "danger"),
materialSwitch(inputId = "switch2", label = "Switch 2", status = "danger")
)
server <- function(input, output, session) {
observeEvent(input$switch1, {
#if (is.null(input$switch1)) return(NULL)
if (input$switch1 == TRUE && input$switch2 == TRUE) {
updateMaterialSwitch(session = session, "switch2", status = "danger", value = FALSE)
}
},
ignoreInit = TRUE)
}
shinyApp(ui, server)
I tried passing the input to a reactive event, but everytime I trigger switch1 after switch2 has been triggered, the app enters an endless loop. Any suggestions?
The end game would be for condition to work both ways, but for now since it doens't even work in one direction I would appreciate some help there.
Please check the following:
library(shiny)
library(shinyWidgets)
ui <- fluidPage(
materialSwitch(inputId = "switch1", label = "Switch 1", status = "danger"),
materialSwitch(inputId = "switch2", label = "Switch 2", status = "danger")
)
server <- function(input, output, session) {
observeEvent(input$switch1, {
if (input$switch1 == TRUE && input$switch2 == TRUE) {
updateMaterialSwitch(session = session, "switch2", value = FALSE)
}
})
observeEvent(input$switch2, {
if (input$switch1 == TRUE && input$switch2 == TRUE) {
updateMaterialSwitch(session = session, "switch1", value = FALSE)
}
})
}
shinyApp(ui, server)

How to fire an event on edit only, not on update in Shiny

I want to enable a Save button when a control is edited by the user, but not when its state is loaded by update.... I know of js functions shiny:updateinput and shiny:inputchanged: the latter fires on all changes, the first on updatexxx only`. There seem to be no function "shiny:onuseredit".
library(shiny)
library(shinyjs)
ui <- fluidPage(
useShinyjs(),
tags$script(HTML("$(document).on('shiny:updateinput', function(event) {
{console.log(event.target.id);}});")),
tags$script(HTML("$(document).on('shiny:inputchanged', function(event) {
{console.log(event.name);}});")),
# No choices here, these will be loaded from db
selectInput("group", "Group", choices = NULL),
numericInput("nruns", "Number of runs", value = NULL),
actionButton("load", "Load", class = "btn-info"),
actionButton("save", "Save", class = "btn-success")
)
server <- function(input, output) {
disable("save")
observeEvent(input$load,{
# In a real app, this will be loaded from a database
updateSelectInput(inputId = 'group', choices = c("first", "second", "third"),
selected = "second")
updateNumericInput(inputId = 'nruns', value = 10)
})
## Missing code
# How to enable the button on edits only, not on load from database
}
shinyApp(ui = ui, server = server)
It's a bit of a hack, but you can create a serverside flag if the change was due to loading the state or a user edit and listen for changes in the inputs:
server <- function(input, output) {
disable("save")
update_flag <- reactiveValues(updated = "no")
observeEvent(input$load,{
# In a real app, this will be loaded from a database
updateSelectInput(inputId = 'group', choices = c("first", "second", "third"),
selected = "second")
updateNumericInput(inputId = 'nruns', value = 10)
update_flag$updated <- "yes"
})
observeEvent(c(input$group, input$nruns), {
if (update_flag$updated == "no") {
enable("save")
} else {
update_flag$updated <- "no"
}
}, ignoreInit = TRUE)
}

selectizeInput: allowing one element per group

I have a selectizeInput with some grouped elements with multiple selection. Is there an elegant way (e.g. using the options argument) of allowing just one element per group, so that a whole group will discarded (or disabled) when an element of this specific group is selected?
So far I tried it programmatically, but than the dropdown menu of the selectizeInput will be closed when updating the selectizeInput.
Minimal example:
library(shiny)
ui <- fluidPage(
selectizeInput("selInput", "Default",
choices=list(g1 = c(A="A",B="B"),
g2 = c(C="C",D="D")),
multiple=T),
selectizeInput("oneElementPerGroup", "One element per group",
choices=list(g1 = c(A="A",B="B"),
g2 = c(C="C",D="D")),
multiple=T)
)
server <- function(session, input, output) {
#Removes the corresponding groups of selected items
observeEvent(input$oneElementPerGroup, ignoreNULL = F, {
plusChoice <- input$oneElementPerGroup
names(plusChoice) <- input$oneElementPerGroup
choices <- list(g1 = c(A="A",B="B"),
g2 = c(C="C",D="D"))
if(any(input$oneElementPerGroup %in% c("A", "B"))){
choices[["g1"]] <- NULL
}
if(any(input$oneElementPerGroup %in% c("C", "D"))){
choices[["g2"]] <- NULL
}
choices$we <- plusChoice
updateSelectizeInput(session,"oneElementPerGroup",
choices = choices,
selected=input$oneElementPerGroup)
})
}
shinyApp(ui = ui, server = server)
You can use pickerInput from {shinyWidgets}. Then we can add a little javascript to do what you want. No server code is needed, very simple. Read more about the data-max-options option: https://developer.snapappointments.com/bootstrap-select/options/.
We need to add the limit to each group, not an overall limit, so we can't add it through the options argument in pickerInput, have to do it in raw HTML or use some js code to inject like what I do.
Be sure your inputId="pick" matches the id in the script #pick. Rename pick to whatever you want.
ui <- fluidPage(
shinyWidgets::pickerInput(
inputId = "pick", label = "Selected",
choices =list(g1 = c(A="A",B="B"), g2 = c(C="C",D="D")),
multiple = TRUE
),
tags$script(
'
$(function(){
$("#pick optgroup").attr("data-max-options", "1");
})
'
)
)
server <- function(input, output, session){}
shinyApp(ui, server)
updates:
If you need to update, we need to run the script again but from server. We can send js by using {shinyjs}. Imagine an observer triggers the update event.
library(shinyjs)
ui <- fluidPage(
useShinyjs(),
shinyWidgets::pickerInput(
inputId = "pick", label = "Selected",
choices =NULL,
multiple = TRUE
)
)
server <- function(input, output, session){
observe({
shinyWidgets::updatePickerInput(session, "pick", choices = list(g1 = c(A="A",B="B"), g2 = c(C="C",D="D")))
observeEvent(once = TRUE, reactiveValuesToList(session$input), {
runjs('$("#pick optgroup").attr("data-max-options", "1");')
}, ignoreInit = TRUE)
})
}
shinyApp(ui, server)

It is possible to restore a session, locally, in a Shiny app if the inputs have been previously written in a RDS file?

I am developing a shiny app to be used locally. I am trying to develop a system for the user to be able to restore a former session.
For that, I took the code from this entrance: Saving state of Shiny app to be restored later , and it did work, however I wanted to be able to restore the inputs within a different session, so that I added a fileInput (Restore Session) and a downloadButton (Save Session) to the code, but unfortunately I could not make it work.
My code is as follows:
library(shiny)
ui <- fluidPage(
textInput("control_label",
"This controls some of the labels:",
"LABEL TEXT"),
numericInput("inNumber", "Number input:", min = 1, max = 20, value = 5, step = 0.5),
radioButtons("inRadio", "Radio buttons:",
c("label 1" = "option1",
"label 2" = "option2",
"label 3" = "option3")),
fileInput("load_inputs", "Restore Session", multiple = FALSE),
downloadButton("save_inputs", 'Save Session')
)
server <- function(input, output,session) {
# SAVE SESSION
output$save_inputs <- downloadHandler(
filename = function() {
paste("session", ".RDS", sep = "")
},
content = function(file) {
saveRDS( reactiveValuesToList(input), file)
})
# LOAD SESSION
load_sesion <- reactive({
req(input$load_inputs)
load_session <- readRDS( input$load_inputs$datapath )
})
observeEvent(input$load_inputs,{
if(is.null(input$load_inputs)) {return(NULL)}
savedInputs <- load_sesion()
inputIDs <- names(savedInputs)
inputvalues <- unlist(savedInputs)
for (i in 1:length(inputvalues)) {
session$sendInputMessage(inputIDs[i], list(value=inputvalues[[i]]) )
}
})}
shinyApp(ui, server)
With this code I can save the inputs of the session and I can read them in the following session, however I am not able to use those values stored on the RDS as inputs in another session.
Thanks a lot,
Rachael
As suggested in my above comments the following app uses shiny's built-in capabilities to create bookmarks instead of using a custom function to save the current state of the inputs.
After the download button is clicked a bookmark is stored on the server side, renamed and copied to the downloadHandler.
If the user uploads a bookmark file, the needed path is created based on the filename and the user gets redirected to the earlier session. (Also see the commented out alternative, which requires the user to actively switch sessions).
Of course you could implement a modal to have the user input a name for the session to avoid using the rather cryptic bookmark hash as the filename.
Edit:
Implemented a modal to let the user provide a custom session name (limited to alphanumeric characters)
library(shiny)
library(shinyjs)
library(utils)
library(tools)
library(stringi)
ui <- function(request) {
fluidPage(
useShinyjs(),
textInput("control_label", "This controls some of the labels:", "LABEL TEXT"),
numericInput("inNumber", "Number input:", min = 1, max = 20, value = 5, step = 0.5 ),
radioButtons("inRadio", "Radio buttons:", c("label 1" = "option1", "label 2" = "option2", "label 3" = "option3")),
fileInput("restore_bookmark", "Restore Session", multiple = FALSE, accept = ".rds"),
actionButton("save_inputs", 'Save Session', icon = icon("download"))
)
}
server <- function(input, output, session) {
latestBookmarkURL <- reactiveVal()
onBookmarked(
fun = function(url) {
latestBookmarkURL(parseQueryString(url))
}
)
onRestored(function(state) {
showNotification(paste("Restored session:", basename(state$dir)), duration = 10, type = "message")
})
observeEvent(input$save_inputs, {
showModal(modalDialog(
title = "Session Name",
textInput("session_name", "Please enter a session name (optional):"),
footer = tagList(
modalButton("Cancel"),
downloadButton("download_inputs", "OK")
)
))
}, ignoreInit = TRUE)
# SAVE SESSION
output$download_inputs <- downloadHandler(
filename = function() {
removeModal()
session$doBookmark()
if (input$session_name != "") {
tmp_session_name <- sub("\\.rds$", "", input$session_name)
# "Error: Invalid state id" when using special characters - removing them:
tmp_session_name <- stri_replace_all(tmp_session_name, "", regex = "[^[:alnum:]]")
# TODO: check if a valid filename is provided (e.g. via library(shinyvalidate)) for better user feedback
tmp_session_name <- paste0(tmp_session_name, ".rds")
} else {
paste(req(latestBookmarkURL()), "rds", sep = ".")
}
},
content = function(file) {
file.copy(from = file.path(
".",
"shiny_bookmarks",
req(latestBookmarkURL()),
"input.rds"
),
to = file)
}
)
# LOAD SESSION
observeEvent(input$restore_bookmark, {
sessionName <- file_path_sans_ext(input$restore_bookmark$name)
targetPath <- file.path(".", "shiny_bookmarks", sessionName, "input.rds")
if (!dir.exists(dirname(targetPath))) {
dir.create(dirname(targetPath), recursive = TRUE)
}
file.copy(
from = input$restore_bookmark$datapath,
to = targetPath,
overwrite = TRUE
)
restoreURL <- paste0(session$clientData$url_protocol, "//", session$clientData$url_hostname, ":", session$clientData$url_port, session$clientData$url_pathname, "?_state_id_=", sessionName)
# redirect user to restoreURL
runjs(sprintf("window.location = '%s';", restoreURL))
# showModal instead of redirecting the user
# showModal(modalDialog(
# title = "Restore Session",
# "The session data was uploaded to the server. Please visit:",
# tags$a(restoreURL),
# "to restore the session"
# ))
})
}
shinyApp(ui, server, enableBookmarking = "server")
Just wanted to add a note here because I spent awhile figuring out how to make this work with saved values. My version is very much derived from #ismirsehregal. I also created bookmarking modules that might be helpful to others. This was needed because shinyFiles inputs caused an error and needed to be excluded from bookmarks so I saved the value in a reactive and then saved it in onBookmark. This was the error when they were not excluded:
Error in writeImpl: Text to be written must be a length-one character vector
library(shiny)
library(shinyFiles)
# source these functions
#Saving #=======================================================================
save_bookmark_ui <- function(id){
actionButton(NS(id, "start_save"), "Save")
}
save_bookmark_server <- function(id, latestBookmarkURL, volumes){
moduleServer(id, function(input, output, session) {
shinyDirChoose(input, "save_dir", root = volumes)
save_dir_pth <- reactive(parseDirPath(volumes, input$save_dir))
onRestored(function(state) {
showNotification(paste("Restored session:", basename(state$dir)),
duration = 10, type = "message")
})
setBookmarkExclude(c("save_dir", "start_save", "save_action", "new_dir_name"))
observeEvent(input$start_save, {
showModal(
modalDialog(
p("The app session is saved using two files, input.rds and values.rds",
"You will provide a location and name for a new folder that will",
" be created to store these files. Make sure you choose a name",
"and location that will be easy to find when you want to load the ",
"saved inputs."),
strong("Choose location to save progess"),
br(),
shinyDirButton(NS(id, "save_dir"), "Location to create folder",
"Location to create folder"),
br(),
textInput(NS(id, "new_dir_name"),
"Choose a name for the new folder that will be created"),
br(),
footer = tagList(
actionButton(NS(id, "save_action"), "Save"),
modalButton("Cancel")
),
title = "Save assessment progress"
)
)
})
iv <- shinyvalidate::InputValidator$new()
iv$add_rule("new_dir_name", shinyvalidate::sv_optional())
iv$add_rule("new_dir_name",
shinyvalidate::sv_regex("[^[:alnum:]]",
paste0("Please choose a name with only",
" letters or numbers and no spaces"),
invert = TRUE))
observeEvent(input$save_action, {
if (!iv$is_valid()) {
iv$enable()
} else {
removeModal()
session$doBookmark()
if (input$new_dir_name != "") {
# "Error: Invalid state id" when using special characters - removing them:
tmp_session_name <- stringr::str_replace_all(input$new_dir_name,
"[^[:alnum:]]", "")
} else {
tmp_session_name <- paste(req(latestBookmarkURL))
}
# create the new directory in the chosen location
new_dir <- fs::dir_create(fs::path(save_dir_pth(), tmp_session_name))
message("Saving session")
# move the files from where shiny saves them to where the user can find them
fs::dir_copy(path = fs::path(".", "shiny_bookmarks", req(latestBookmarkURL)),
new_path = new_dir,
overwrite = TRUE)
}
}, ignoreInit = TRUE)
})
}
# Load #=======================================================================
load_bookmark_ui <- function(id){
actionButton(NS(id, "start_load"), "Load")
}
load_bookmark_server <- function(id, volumes){
moduleServer(id, function(input, output, session){
shinyDirChoose(input, "load_dir", root = volumes)
load_dir_pth <- reactive(parseDirPath(volumes, input$load_dir))
setBookmarkExclude(c("load_dir", "load_action", "start_load"))
observeEvent(input$start_load, {
showModal(
modalDialog(
strong("Select the folder where the app was saved"),
br(),
shinyDirButton(NS(id, "load_dir"), "Select Folder",
"Location of folder with previous state"),
footer = tagList(
actionButton(NS(id, "load_action"), "Load"),
modalButton("Cancel")
),
title = "Load existing assessment"
)
)
})
# LOAD SESSION
observeEvent(input$load_action, {
sessionName <- fs::path_file(load_dir_pth())
targetPath <- file.path(".", "shiny_bookmarks", sessionName)
if (!dir.exists(dirname(targetPath))) {
dir.create(dirname(targetPath), recursive = TRUE)
}
# copy the bookmark to where shiny expects it to be
fs::dir_copy(path = load_dir_pth(),
new_path = targetPath,
overwrite = TRUE)
restoreURL <- paste0(session$clientData$url_protocol, "//",
session$clientData$url_hostname, ":",
session$clientData$url_port, "/?_state_id_=",
sessionName)
removeModal()
# redirect user to restoreURL
shinyjs::runjs(sprintf("window.location = '%s';", restoreURL))
# showModal instead of redirecting the user
# showModal(modalDialog(
# title = "Restore Session",
# "The session data was uploaded to the server. Please visit:",
# tags$a(restoreURL, href = restoreURL),
# "to restore the session"
# ))
})
})
}
# Input options
valueNms <- c("Greatly increase", "Increase", "Somewhat increase", "Neutral")
valueOpts <- c(3, 2, 1, 0)
ui <- function(request) {
fluidPage(
shinyjs::useShinyjs(),
textInput("control_label", "This controls some of the labels:", "LABEL TEXT"),
numericInput("inNumber", "Number input:", min = 1, max = 20, value = 5, step = 0.5 ),
radioButtons("inRadio", "Radio buttons:",
c("label 1" = "option1", "label 2" = "option2", "label 3" = "option3")),
checkboxGroupInput("inChk","Checkbox:", choiceNames = valueNms,
choiceValues = valueOpts),
shinyFilesButton("range_poly_pth", "Choose file",
"Range polygon shapefile", multiple = FALSE),
verbatimTextOutput("range_poly_pth_out", placeholder = TRUE),
save_bookmark_ui("save"),
load_bookmark_ui("load"),
tableOutput("table")
)
}
server <- function(input, output, session) {
shinyFileChoose("range_poly_pth", root = volumes, input = input)
file_pth <- reactive({
if(is.integer(input$range_poly_pth)){
if(!is.null(restored$yes)){
return(file_pth_restore())
}
return(NULL)
} else{
return(parseFilePaths(volumes, input$range_poly_pth)$datapath)
}
})
output$table <- renderTable({
req(file_pth())
read.csv(file_pth())
})
output$range_poly_pth_out <- renderText({
file_pth()
})
setBookmarkExclude(c("range_poly_pth",
"range_poly_pth_out"))
# this part is not allowed to be inside the module
latestBookmarkURL <- reactiveVal()
onBookmarked(
fun = function(url) {
latestBookmarkURL(parseQueryString(url))
showNotification("Session saved",
duration = 10, type = "message")
}
)
R.utils::withTimeout({
volumes <- c(wd = getShinyOption("file_dir"),
Home = fs::path_home(),
getVolumes()())
}, timeout = 10, onTimeout = "error")
save_bookmark_server("save", latestBookmarkURL(), volumes)
load_bookmark_server("load", volumes)
# Need to explicitly save and restore reactive values.
onBookmark(function(state) {
val2 <- Sys.Date()
state$values$date <- val2
state$values$file <- file_pth()
})
restored <- reactiveValues()
file_pth_restore <- reactiveVal()
onRestore(fun = function(state){
file_pth_restore(state$values$file)
print(file_pth_restore())
restored$yes <- TRUE
})
}
shinyApp(ui, server, enableBookmarking = "server")

Shiny: update selectizeInput choices based on selected

I am trying to update the choices of a selectizeInput based on the current selected choices. Here is my attempt (causes loop):
library(shiny)
run_ui <- function() {
ui <- selectizeInput('words', 'Search words:', choices = NULL, selected = NULL, multiple = TRUE, options = NULL)
server <- function(input, output, session) {
# change 'Search words' ----
observeEvent(input$words, {
# handle no words (reset everything)
if (is.null(input$words)) {
cowords <- letters
} else {
# update cowords (choices for selectizeInput)
cowords <- unique(c(input$words, sample(letters, 5)))
}
# update UI
print('updating')
updateSelectizeInput(session, 'words', choices = cowords, selected = input$words, server = TRUE)
}, ignoreNULL = FALSE)
}
runGadget(shinyApp(ui, server), viewer = browserViewer())
}
run_ui()
How can I achieve this?
If you want to stick to server = TRUE, it's maybe not a trivial problem.
One possible work-around could be to debounce the input that you are observing, and then check and only update in case there is a change. This could look as follows - I added some print statements such that you can better follow what's happening.
library(shiny)
run_ui <- function() {
ui <- selectizeInput('words', 'Search words:', choices = NULL, selected = NULL, multiple = TRUE, options = NULL)
server <- function(input, output, session) {
val <- "a"
pasteCollPlus <- function(...) {
paste(..., collapse = "+")
}
wordSelect <- debounce(reactive({input$words}), millis = 50)
# change 'Search words' ----
observeEvent(wordSelect(), {
# handle no words (reset everything)
if (is.null(input$words)) {
cowords <- letters
} else {
# update cowords (choices for selectizeInput)
cowords <- unique(c(input$words, sample(letters, 5)))
}
if (isTRUE(pasteCollPlus(val) == pasteCollPlus(input$words))) {
print(paste("No update - val is", pasteCollPlus(val)))
} else {
# update UI
print(paste("updating selection to", pasteCollPlus(input$words)))
print(paste("val is", pasteCollPlus(val)))
val <<- input$words
updateSelectizeInput(session, 'words', choices = cowords, selected = input$words, server = TRUE)
}
}, ignoreNULL = FALSE)
}
runGadget(shinyApp(ui, server), viewer = browserViewer())
}
run_ui()
Edit
Another work-around would be to handle the bouncing pattern explicitly, in order to block it. This is maybe even less elegant, but could be more robust for more involved / complex cases (apps). An example for this follows:
library(shiny)
run_ui <- function() {
ui <- selectizeInput('words', 'Search words:', choices = NULL, selected = NULL, multiple = TRUE, options = NULL)
server <- function(input, output, session) {
val <- "a"
newVal <- NULL
pasteCollPlus <- function(...) {
paste(..., collapse = "+")
}
# change 'Search words' ----
observeEvent(input$words, {
# handle no words (reset everything)
if (is.null(input$words)) {
cowords <- letters
} else {
# update cowords (choices for selectizeInput)
cowords <- unique(c(input$words, sample(letters, 5)))
}
if (isTRUE(pasteCollPlus(val) == pasteCollPlus(input$words))) {
print(paste("No update - val is", pasteCollPlus(val)))
val <<- newVal
} else {
# update UI
print(paste("updating selection to", pasteCollPlus(input$words)))
print(paste("val is", pasteCollPlus(val)))
print(paste("newVal is", pasteCollPlus(newVal)))
val <<- NULL
newVal <<- input$words
updateSelectizeInput(session, 'words', choices = cowords, selected = input$words, server = TRUE)
}
}, ignoreNULL = FALSE)
}
runGadget(shinyApp(ui, server), viewer = browserViewer())
}
run_ui()
Do you need to use server-side selectize? If not, then your code would work fine as-is by simply removing that part.
library(shiny)
run_ui <- function() {
ui <- selectizeInput('words', 'Search words:', choices = NULL, selected = NULL, multiple = TRUE, options = NULL)
server <- function(input, output, session) {
# change 'Search words' ----
observeEvent(input$words, {
# handle no words (reset everything)
if (is.null(input$words)) {
cowords <- letters
} else {
# update cowords (choices for selectizeInput)
cowords <- unique(c(input$words, sample(letters, 5)))
}
# update UI
print('updating')
updateSelectizeInput(session, 'words', choices = cowords, selected = input$words)
}, ignoreNULL = FALSE)
}
runGadget(shinyApp(ui, server), viewer = browserViewer())
}
run_ui()
The following solution simply updates the entire object through renderUI and re-draws it, rather than passing back an update via updateSelectizeInput(). This does allow choices to be fully managed on the server-side. A downside is that it fires with each change event, which means that the multiple=TRUE is moot since the object redraws with each change. If multiples are critical, I think the updateSelectizeInput() approach or any other solution that updates onChange, would run into the same issue. To allow multiple choices, the event would need to move to onBlur or a mouseout event. Otherwise, the event trigger doesn't know if a user intends to select only one choice and fire; or wait for the user to make multiple choices before firing. However, blur or mouseout might make it behave strangely from the user's perspective. A button forcing the update action would resolve this. Keeping the update based on the first select, solution as follows:
library(shiny)
run_ui <- function() {
ui <- uiOutput(outputId="select_words")
server <- function(input, output, session) {
# change 'Search words' ----
output$select_words <- renderUI({
cowords <- letters
if (!is.null(input$words)) cowords <- unique(c(input$words, sample(letters, 5)))
print(paste("Updating words: ",paste0(cowords,collapse=",")))
return (tagList(selectizeInput('words', 'Search words:', choices = cowords, selected = input$words, multiple = TRUE, options = NULL)))
})
}
runGadget(shinyApp(ui, server), viewer = browserViewer())
}
run_ui()

Resources