Dependent selectInput objects in Shiny apps that you can bypass

Shiny R

4 February, 2020 (updated 12 July, 2020)



I see this question pop up on Stack Overflow all the time. How do you tie together multiple selectInputs (or selectizeInputs), 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 I've come up with a really simple work-around.

Background

Firstly, what do I mean by dependent selectizeInputs? Let's use mtcars as an example. Say we wanted two selectizeInputs. In the first dropdown menu, 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()

Solution

We're going to solve this problem by having the parent input as a normal selectizeInput, but making the child (dependent) input rendered as a uiOutput. The catch 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 "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:


ui <- {
    fluidPage(
        fluidRow(
            selectizeInput(
                inputId = 'select_cyl',
                label = 'Cyl',
                choices = c('All cylinders', sort(unique(mtcars$cyl))),
                multiple = TRUE,
                selected = 'All cylinders'),
            uiOutput(
                outputId = '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, 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) {
            df <- mtcars
        } else {
            df <- mtcars %>%
                filter(
                    cyl %in% input$select_cyl)
        }

        # get available carb values
        carbs <- sort(unique(df$carb))

        # render selectizeInput
        selectizeInput(
            inputId = 'select_carb',
            label = '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 :)

Check out the full code for this tutorial here.



0 comments

Leave a comment