Search code examples
rshinyreactive-programming

Combine event binding and dynamic outputs in shiny


My Shiny app consists of selecting a set of parameters to generate outputs based on the corresponding data, where each parameter is dependent on the previous one to narrow down which data needs to be used. To achieve this I use dynamic inputs with uiOutput and renderUi.

An example of the logic would be:

  1. Static selection of parameter 1 (choices are known)
  2. Render dynamic selection of parameter 2 based on param1's value
  3. Render dynamic selection of parameter 3 based on param1 and param2's values
  4. Fetch unique report corresponding to parameters 1,2,3 selections, process data and store in reactive variable
  5. Output renders from reactive data

On initialization, a valid set of parameters and the output reports are rendered. However I'd like to add a "Run" button so that the outputs are only rendered when the user completed their selections and manually trigger a recalculation - but the outputs still need to be rendered on initialization.

Here is a minimal reprex I put together with one static selection and one dynamic selection:

library(tidyverse)
library(shiny)

(homes <- tibble(
  zone = rep(c("A", "B"), each = 3),
  owner = paste0("person", c(1,2,3,4,3,1)),
  price = 111000*(1:6)
))

ui <- shiny::fluidPage(
  selectInput("sel1", "zone", choices = unique(homes$zone)),
  uiOutput("ui_sel2"),
  actionButton("confirm", "confirm"),
  verbatimTextOutput("txt")
)

server <- function(input,output,session) {
  # render dynamic input
  output$ui_sel2 <- renderUI({
    choices <- dplyr::filter(homes, zone == input$sel1)$owner %>% unique()
    selectInput("sel2", "owner", choices = choices)
  })
  
  sel_price <- reactive({
    # need dynamic input to render before calc
    req(input$sel2)
    # get price
    dplyr::filter(homes, zone == input$sel1, owner == input$sel2)$price[1]
  })
  
  # render price from selected zone+owner
  output$txt <- renderText({paste("Price of selected home is", sel_price())}) %>%
    bindEvent(input$confirm, ignoreNULL=FALSE)
}
shinyApp(ui = ui, server = server)

I tried multiple variations of bindEvent(), both on the render function and the reactive expression, trying to include both input$sel2 and input$confirm but wasn't able to achive the render on initialization. The render only works after first clicking the button, after which I get the expected behavior.

I believe it has to do with the req(input$sel2) I use in my reactive expression to ignore the dynamic until it is rendered, but I would have guessed that adding it in bindEvent() would have solved the issue - not the case.


Solution

  • I would add a reactive which returns the filtered choices for the dynamic selectInput and in your sel_prices use coalesce to set it to the first element of this list in case it is NULL (so basiclaly on startup):

    library(tidyverse)
    library(shiny)
    
    homes <- tibble(
      zone = rep(c("A", "B"), each = 3),
      owner = paste0("person", c(1,2,3,4,3,1)),
      price = 111000*(1:6)
    )
    
    ui <- fluidPage(
      selectInput("sel1", "zone", choices = unique(homes$zone)),
      uiOutput("ui_sel2"),
      actionButton("confirm", "confirm"),
      verbatimTextOutput("txt")
    )
    
    server <- function(input,output,session) {
      get_choices <- reactive({
        dplyr::filter(homes, zone == input$sel1) %>% 
          pull(owner) %>% 
          unique()
      })
      
      output$ui_sel2 <- renderUI({
        selectInput("sel2", "owner", choices = get_choices())
      })
      
      sel_price <- reactive({
        filter(homes, zone == input$sel1, 
                      owner == coalesce(input$sel2, get_choices()[[1L]])) %>% 
          pull(price)
      })
    
      output$txt <- renderText({
        paste("Price of selected home is", sel_price())
      }) %>%
        bindEvent(input$confirm, ignoreNULL = FALSE)
    }
    shinyApp(ui = ui, server = server)