0
votes

Problem: an updateSelectizeInput call within observeEvent changes the value displayed in the browser but does not change the value I can access from code with input$

Background: In my Shiny app, I want to have an input with the following properties:

  1. the user can type in and select from server-side list of options
  2. a URL query string can also control it
  3. blank means all options
  4. an actionButton allows the user to delay expensive computation until all desired choices have been made

I currently have an observeEvent watching for the query string, and when it sees one, it calls updateSelectizeInput, but, after it does so, the input is unchanged.

Example:

library(shiny)

possibleLetters = LETTERS[1:10]

ui <- fluidPage(
   
   sidebarLayout(
      sidebarPanel(
        selectizeInput(inputId='letters', 
                       label='Your letters:', 
                       choices=NULL,
                       selected=NULL, 
                       multiple=T, 
                       width='100%'),
        actionButton("recompute",
                     "Recompute now")
      ),
      
      mainPanel(
        h3("Letters: "),
        textOutput('lettersDisplay'),
        h3("Indices of letters: "),
        textOutput('lettersIndicesDisplay')
      )
   )
)

server <- function(input, output, session) {
   
  updateSelectizeInput(inputId='letters',
                       choices=c('',possibleLetters),
                       server=T)

  userLetters = eventReactive(input$recompute, ignoreNULL=FALSE, {
    if (length(input$letters) == 0) {
      return (possibleLetters)
    } else (
      return (input$letters)
    )
  })
  
  userLetterIndices = reactive({
    return(match(userLetters(),LETTERS))
  })
  
  queryStringObserver = observeEvent(session$clientData$url_search, {
    query <- parseQueryString(session$clientData$url_search)
    if (!is.null(query$letters)) {
      cat(file=stderr(), paste0('observeEvent running - user specified param is: ',query$letters,'\n'))
      updateSelectizeInput(session, 
                           inputId="letters",
                           choices = possibleLetters,
                           selected = query$letters,
                           server=T)
      cat(file=stderr(), paste0('observeEvent running - ran updateSelectizeInput, input$letters is now: ',input$letters,'\n'))
    }
  })
  
  
  output$lettersDisplay = renderText({
    return(paste(userLetters(),collapse=' '))
  })
  
  output$lettersIndicesDisplay = renderText({
    return(paste(userLetterIndices(), collapse=' '))
  })

}

shinyApp(ui = ui, server = server, options = list(port=1111))

Steps to reproduce problem: Run the app and then navigate to http://localhost:1111/?letters=A

You will find that "A" has indeed been filled into the selectize field in the browser window, however, the input value has not been updated. In your console you will see:

observeEvent running - user specified param is: A
observeEvent running - ran updateSelectizeInput, input$letters is now: 

Thus, the query string has been correctly parsed, updateSelectizeInput has been called, and yet when input$letters is accessed afterwards, its value has not changed.

I suspect this relates to some fundamental shortcoming in my understanding of reactive graphs or something, but after poring over Mastering Shiny, I still can't figure out why this call does nothing.

2

2 Answers

1
votes

The value of input$letters updates, it just hasn't by the time you try to print it. I'm not sure how or if Shiny batches messages, but your observeEvent eventually triggers a message to be sent to the client to update the input, which then has to inform the server is been updated. At a minimum, I assume it would finish executing the current observer code, but through some tinkering it appears Shiny may execute all necessary reactive code before sending messages to the client.

While the value of input$letters prints nothing with your given code, if I click recompute it does update the text as expected. Basically, here's more or less the conversation I believe that happens with your code as is:

Client: Yo, server. The user added a query parameter: letters=A.

Server: Hmmm, ok I will just run this observeEvent code. Oh, the developer wants to know the current value of `input$letters`. Client, can you help a server out with that input value.

Client: No problem, friend-o. The current selected value is NULL.

Server: Well, let me just cat this NULL to the stderr. 

Server (~1 ms later): Yo, client. I finished running the observeEvent code and you should really update that selectize input. It would make everyone happy.

Client: Can do.

Client (~2 ms later): Whoa, the select input updated. I gots to tell server about this jawn. Hey server, input$letters just changed to `A`. Just FYI.

Server: Hmm, good to know. Nothing for me to do about that.

When the server prints inputletters, the value is still NULL because it hasn't told the client to update it yet. Actually, I'm not sure if the server polls the client for the value or if looks it up from it's own list of current values, but either way, it still hasn't been updated when it goes to cat the value.

Move your cat statement to a separate observe statement and the conversation above changes to

Client (~2 ms later): Whoa, the select input updated. I gots to tell server about this jawn. Hey server, input$letters just changed to `A`. Just FYI.

Server: WHAT?!!! OMG! I MUST TELL STDERR ABOUT THIS!!

This all a long way to say I don't think there is actually anything wrong with your code per se. Sorry, for all the personality changes with the server and the client, but hopefully this helps.

0
votes

To achieve the desired behavior of the app immediately showing the results with the given query string (without waiting for user to press Recompute now), the following changes were necessary:

  1. Delete the original observer, and instead only call updateSelectizeInput from inside the userLetters eventReactive
  2. Only fill in values from the query string on startup (if(!input$recompute))
  3. To keep the query string up to date with what the user then changes in the app, add a new observer update_qs

The code:


library(shiny)

possibleLetters = LETTERS[1:10]

ui <- fluidPage(
   
   sidebarLayout(
      sidebarPanel(
        selectizeInput(inputId='letters', 
                       label='Your letters:', 
                       choices=NULL,
                       selected=NULL, 
                       multiple=T, 
                       width='100%'),
        actionButton("recompute",
                     "Recompute now")
      ),
      
      mainPanel(
        h3("Letters: "),
        textOutput('lettersDisplay'),
        h3("Indices of letters: "),
        textOutput('lettersIndicesDisplay')
      )
   )
)

server <- function(input, output, session) {

  userLetters = eventReactive(input$recompute, ignoreNULL=FALSE, {
    query <- parseQueryString(session$clientData$url_search)
    if (!input$recompute & !is.null(query$letters)) {
      selectedLetters = strsplit(query$letters,';')[[1]]
    } else if (is.null(input$letters)) {
      selectedLetters = character(0)
    } else (
      selectedLetters = input$letters
    )
    updateSelectizeInput(session, 
                         inputId="letters",
                         choices = c('',possibleLetters),
                         selected = selectedLetters,
                         server=T)
    if (length(selectedLetters)==0) {
      return (possibleLetters)
    } else {
      return (selectedLetters)
    }
  })
  
  update_qs = observeEvent(input$recompute, {
    if (!identical(userLetters(),possibleLetters)) {
      new_qs = paste0('?letters=',paste0(userLetters(),collapse=';'))
    } else {
      new_qs = '?'
    }
    updateQueryString(new_qs, mode='push')
  })
  
  userLetterIndices = reactive({
    return(match(userLetters(),LETTERS))
  })

  
  output$lettersDisplay = renderText({
    return(paste(userLetters(),collapse=' '))
  })
  
  output$lettersIndicesDisplay = renderText({
    return(paste(userLetterIndices(), collapse=' '))
  })

}

# Run the application 
shinyApp(ui = ui, server = server, options = list(port=1111))