3
votes

Shiny provides a selectizeInput that wraps selectize.js, which produces a text input / combo box widget. There are several reasons that I'd like to delay loading of the options/values for the selectizeInput: there may be a long list of values or the possible values are dependent on other parameters. Also, I'd like the user to be able to create new values (e.g. choose an existing value or create your own).

But in all of these cases, the value of the selectizeInput cannot be bookmarked using Shiny's server-side bookmarking. When the selectizeInput object is initialized, the bookmarked value stored in the input are not yet valid option values.

Does anybody have any clever workarounds to bookmark selectizeInput values?

Here's the simplest example dealing with just the create option:

library(shiny)

ui <- function(request) {
  fluidPage(

    selectizeInput("foo", "Created Values Cannot Be Bookmarked", choices=c("Choose From List or Type New Value"="", "bar", "baz"),
                   options=list(create=TRUE)),
    bookmarkButton("Update URL")

  )}

server <- function(input, output) {
  onBookmarked(function(url) {
    updateQueryString(url)
  })
}

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

If you type a new value in the selection box, hit the bookmark button to update the URL in the location bar in the browser, and hit reload, then that created value will be lost. Try the same with an existing option and it works, of course.

It gets more complicated if option loading is delayed, but the problem is the same, which is that the user's selection is not valid when the selectizeInput is rendering.

2

2 Answers

4
votes

Here's a better solution using the server side support for selectize. The important step to support create=TRUE options is to use onRestored to update the choices with the new values that were created by the user.

library(shiny)

init.choices <- c("Choose From List or Type New Value"="")

ui <- function(request) {
  fluidPage(
    titlePanel("Bookmarking Created Values"),
    selectizeInput("foo", NULL, 
                   choices=init.choices,
                   multiple=TRUE,
                   options=list(create=TRUE)),
    bookmarkButton("Update URL")

  )}

server <- function(input, output, session) {
  updateSelectizeInput(session, "foo", choices=c(init.choices, rownames(mtcars)), server=TRUE)
  onBookmarked(function(url) {
    updateQueryString(url)
  })
  onRestored(function(state) {
    updateSelectizeInput(session, "foo", selected=state$input$foo, choices=c(init.choices, rownames(mtcars), state$input$foo), server=TRUE)
  })
}


shinyApp(ui = ui, server = server, enableBookmarking = "server")
2
votes

Here's one workaround hack, expanding on the example from the original question. In this example I also show delayed loading.

A duplicate value of the selectizeInput is stored in a hidden text input. When the bookmarked values are loaded, the hidden backup value is used to restore the real one. To set a value that's not among the current options, the selectize.js API is called using shinyjs to addOption and then addItem. Note that ignoreInit=TRUE on the input$foo observer so that the hidden backup is not overwritten on load. On the other hand, once=TRUE is set on the input$foo.bak observer so that the hidden backup is only used to update the real value once at startup. See documentation for observeEvent.

The choices for the selectizeInput are loaded asynchronously only when focus is on the input to speed initial page load. A JSON file with choices is stored locally in the www/ directory.

This example also allows multiple selection. Selected items are stored comma-delimited in the hidden text field and then expanded during initialization.

library(shiny)
library(shinyjs)

# create JSON file with selectizeInput choices to be retrieved via ajax during load
# normally this would be run once outside of app.R
dir.create("www")
foo.choices.json <- paste('{ "cars": [\n', paste0('{ "value": "', rownames(mtcars), '", "label": "',rownames(mtcars), '" }', collapse=",\n" ), "\n]}")
writeChar(foo.choices.json, "www/foo.choices.json", eos=NULL)

# javascript to set one or more values
jsCode <- "shinyjs.setfoo = function(params){ 
  x=$('#foo')[0].selectize; 
  $.each(params.items, function(i,v) { x.addOption({value:v,label:v}); x.addItem(v) })
}"

# javascript to retrieve the choices asynchronously after focus
foo.load <- "function(query, callback) {
  if (query.length) return callback(); // not dynamic by query, just delayed load
  $.ajax({
    url: 'foo.choices.json',
    type: 'GET',
    error: function() { callback() },
    success: function(res) { callback(res.cars) }
  })
}"

ui <- function(request) {
  fluidPage(
    useShinyjs(), 
    extendShinyjs(text=jsCode),
    titlePanel("Bookmarking Created Values"),
    selectizeInput("foo", NULL, 
                   choices=c("Choose From List or Type New Value"=""),
                   multiple=TRUE,
                   options=list(create=TRUE, preload="focus", load=I(foo.load))),
    bookmarkButton("Update URL"),
    div(style="display:none", textInput("foo.bak", NULL))

  )}

server <- function(input, output, session) {
  onBookmarked(function(url) {
    updateQueryString(url)
  })
  observeEvent(input$foo, { 
    if (is.null(input$foo)) {
      updateTextInput(session, "foo.bak", value="")
    } else {
      updateTextInput(session, "foo.bak", value=input$foo)
    }
  }, ignoreInit=TRUE, ignoreNULL = FALSE)
  observeEvent(input$foo.bak, {
    js$setfoo(items=as.list(strsplit(input$foo.bak,",")[[1]]))
  }, once=TRUE)
}


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