Dependent selectInput objects in Shiny apps that you can bypass

Shiny R

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

Roland 25 February, 2024

This saved me a ton of time, thank you so much.

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

Leave a comment