Dependent selectInput objects in Shiny apps that you can bypass
4 February, 2020 (updated 2 November, 2022)
I see this question pop up on Stack Overflow all the time: How do you tie together multiple selectInput
(or selectizeInput
) selectors, making the child input dependent on the parent input, but still allowing the user to bypass the first input? In other words, how do we make the options held in one dropdown menu dependent on the choice of the first dropdown menu, while still allowing the first input to have an option that is essentially "everything"? As far as I'm aware, there isn't an out-of-the-box solution from Shiny, but there's a fairly simple work-around.
Background
Firstly, what do I mean by dependent selectInput
selectors? Let's use mtcars as an example. Say we wanted two selectInput
dropdown menus. In the first, we want the user to be able to select the number of cylinders (the cyl
variable). In the second dropdown menu, we want the user to be able to select the number of carburetors (the carb
variable). Here, we want carb
to be dependent on cyl
; in the mtcars dataset, it's easy to see that there are only two possible carb
values when cyl == 4
; there are three possible carb
options when cyl == 6
; and there are four possible carb
options when cyl == 8:
library(dplyr)
mtcars %>%
group_by(cyl, carb) %>%
summarise(.groups = "drop)
# A tibble: 9 × 2
# cyl carb
#
# 1 4 1
# 2 4 2
# 3 6 1
# 4 6 4
# 5 6 6
# 6 8 2
# 7 8 3
# 8 8 4
# 9 8 8
So, if we select cyl == 4
, we want the second dropdown menu to only show 1
and 2
as valid carb
options.
Solution
We're going to solve this problem by having the parent input as a normal selectInput
, but making the child (dependent) input rendered as a uiOutput
. uiOutput
is an important function in Shiny, as it allows you to construct UI elements that are dependent on other server-side code. The idea in this case is that we're going to pass a vector to the choices
argument of the parent input that contains all the valid cyl
choices, in addition to a character string "All cylinders". This will allow us to choose whether we want to filter our dataset by cylinder or not. The UI for this is really simple:
library(shiny)
library(dplyr)
ui <- fluidPage(
fluidRow(
selectizeInput("select_cyl", "Cyl",
choices = c('All cylinders', sort(unique(mtcars$cyl))),
multiple = TRUE,
selected = 'All cylinders'
),
uiOutput("select_carb")
)
)
Sweet. The server code is also really simple! We firstly want to check whether "All cylinders" has been selected; if so, then we don't need to do any filtering (and so we can just provide the user with all possible carb
options). If it hasn't been selected, i.e., if specific cylinder values have been selected, then we need to filter accordingly, and only provide the user with carb
options where there is a matching observation for the selected value.
server <- function(input, output, session) {
# render the child dropdown menu
output$select_carb <- renderUI({
# check whether user wants to filter by cyl;
# if not, then filter by selection
if ("All cylinders" %in% input$select_cyl) {
dat <- mtcars
} else {
dat <- mtcars %>%
filter(cyl %in% input$select_cyl)
}
# get available carb values
carbs <- sort(unique(dat$carb))
# render selectizeInput
selectizeInput("select_carb", "Carb",
choices = c("All carburetors", carbs),
multiple = TRUE,
selected = "All carburetors")
})
}
shinyApp(ui, server)
And that's it. When we run the server, we have two dropdown menus, where the choices for the child input (carb) are dependent on what the user selected for the parent input (cyl), and we still have the option to bypass the first input :)
The full code for this tutorial is here.
Update based on Christopher's question: If you only want "All carburetors", and don't want "All cylinders", then something like the following should suffice:
library(shiny)
library(dplyr)
cyls <- sort(unique(mtcars$cyl))
ui <- fluidPage(
selectInput("select_cyl", "Cyl",
choices = cyls,
multiple = TRUE,
selected = cyls[1]
),
uiOutput("select_carb")
)
server <- function(input, output, session) {
output$select_carb <- renderUI({
carbs <- mtcars %>%
filter(cyl %in% input$select_cyl) %>%
pull(carb) %>%
unique() %>%
sort()
selectInput("select_carb", "Carb",
choices = c("All carburetors", carbs),
multiple = TRUE,
selected = carbs[1]
)
})
}
shinyApp(ui, server)
2 comments
Christopher Chitemerere 24 October, 2022
Thank you very much, this helped me a lot. I have a situation where I do not want "All cylinders" in the first selectizeInput, but "All carburetors" in the second selectizeInput. How can the code be adapted to suit this.
Thanking you
Roland 25 February, 2024
This saved me a ton of time, thank you so much.