Modularizing an app with dynamic inputs (renderUI)

I have been struggling to wrap my mind around the pattern for modules that interact with dynamic UI.

Just as a preface, I have read and explored the following relevant resources:

All of those have been helpful but when I actually try to apply this information to the design of my app I find myself overwhelmed.


The app that I want to build gives the user a set of radio button controls which influence an output - here's a screenshot:

I want only one radio button input to be active at a time, so I keep track of which input was clicked last using shinyjs::onclick() and clear the selection of all other radio button inputs using renderUI() - this is what makes the UI dynamic.

I would like to modularize this pattern so that I could easily add more datasets to the app, but I'm finding it conceptually challenging to hold a mental model of the modules in my head.

App specific recommendations or general advice on modules are both welcome.

App code:

# SETUP ----

library(shiny)
library(shinyjs)

# MODULES ----

# none yet

# UI ----
ui <- fluidPage(
  useShinyjs(), 
  
  # Application title
  titlePanel("Example: linked radio buttons"),
  
  sidebarLayout(

    # Sidebar with a slider input
    sidebarPanel(
      fluidRow(
        column(4,
               uiOutput("cars")
               ),
        column(4,
               uiOutput("pressure")
               ),
        column(4,
               uiOutput("faithful")
               )
        )

      ),

    # Show a plot of the generated distribution
    mainPanel(
      fluidRow(
        column(4,
               strong("Current dataset: "),textOutput("current",inline = TRUE),
               br(),
               strong("Selected row:"), textOutput("current_row", inline = TRUE),
               verbatimTextOutput("row"),
               strong("Summary:"),
               verbatimTextOutput("summary")
        )

      )

    )
  )
)

# SERVER ----

server <- shinyServer(function(input, output, session) {
  
  rx <- reactiveValues(reactInd = 0)
  
  # Observers (which input was most recently changed)
  
  onclick("cars",{{
    rx$reactInd <- 1

  }})

  onclick("pressure",{{
    rx$reactInd <- 2

  }})
  
  onclick("faithful",{{
    rx$reactInd <- 3

  }})
  

  observe({ 
    
    if(rx$reactInd != 1){
      
      output$cars <- renderUI({
        radioButtons("cars",
                 label = 'cars:',
                 choices = 1:6,
                 selected = character(0))
      })
    }
    
    if(rx$reactInd != 2){
      
      output$pressure <- renderUI({
        radioButtons("pressure",
                 label = 'pressure:',
                 choices = 1:6,
                 selected = character(0))
      })
    }
     
    if(rx$reactInd !=3){
      
      output$faithful <- renderUI({
        radioButtons("faithful",
                 label = 'faithful:',
                 choices = 1:6,
                 selected = character(0))
      })
    }
    
    
    })

  
  output$current <- renderText({
    
    req(rx$reactInd > 0)
    
    switch(rx$reactInd,
           "1" = "cars",
           "2" = "pressure",
           "3" = "faithful")
  })
  
  output$current_row <- renderText({
    
    req(rx$reactInd > 0)
    
    switch(rx$reactInd,
           "1" = input$cars,
           "2" = input$pressure,
           "3" = input$faithful)
  })
  
  output$row <- renderPrint({
    req(rx$reactInd > 0)
    switch(rx$reactInd,
           "1" = cars[input$cars,],
           "2" = pressure[input$pressure,],
           "3" = faithful[input$faithful,])
    
  })
  
  output$summary <- renderPrint({
    req(rx$reactInd > 0)
    df <- switch(rx$reactInd,
           "1" = cars,
           "2" = pressure,
           "3" = faithful)
    summary(df)
  })
  
})

# APP ----

shinyApp(ui, server)


When modularizing an app, I like to start by mentally breaking it up into standalone components. Maybe draw some boxes on the UI and figure out which boxes could function as independent, reusable units.

For this example, I don't see the individual radio button groups as modules, but all of them composing a single module that serves as the input control for the selected dataset/row. Individual radio groups wouldn't be great fits for modules as they don't make much sense in isolation. And maybe that's why you're finding it difficult to turn them into modules.

Instead, I would start with extracting the repetitive logic into functions. It's like the RStudio article says - functions alone are good enough to generate UI, define outputs, and create reactive expressions. You could make a function that adds an arbitrary dataset to the app, and then consider what could be further extracted into a module.


For some app specific advice -

The whole thing is essentially just one group of radio buttons, but laid out and labeled into subgroups. It could be done in HTML alone by giving every radio the same name, and changing the values underneath to something like cars-1 or cars-2 so they don't clash. This wouldn't even need a custom input binding since it'd only differ from the built-in radioButtons in HTML.

Here's an example where I've piggybacked off the existing radioButtons to generate the HTML. I separated radioButtons' inner content from its binding container and pulled them into functions that could be used to build radio buttons with subgroups.

library(shiny)

radioSubgroup <- function(inputId, id, label, choices, inline = FALSE) {
  values <- paste0(id, "-", choices)
  choices <- setNames(values, choices)

  rb <- radioButtons(inputId, label, choices, selected = character(0), inline = inline)
  rb$children
}

radioGroupContainer <- function(inputId, ...) {
  class <- "form-group shiny-input-radiogroup shiny-input-container"
  div(id = inputId, class = class, ...)
}

ui <- fluidPage(
  titlePanel("Example: linked radio buttons"),

  sidebarLayout(
    sidebarPanel(
      radioGroupContainer("selectedRow",
        fluidRow(
          column(4, radioSubgroup("selectedRow", "cars", label = "cars:", choices = 1:6)),
          column(4, radioSubgroup("selectedRow", "pressure", label = "pressure:", choices = 1:6)),
          column(4, radioSubgroup("selectedRow", "faithful", label = "faithful:", choices = 1:6))
        )
      )
    ),

    mainPanel(
      fluidRow(
        column(4,
               strong("Current dataset: "), textOutput("current", inline = TRUE),
               br(),
               strong("Selected row:"), textOutput("selectedRow", inline = TRUE),
               verbatimTextOutput("row"),
               strong("Summary:"),
               verbatimTextOutput("summary")
        )
      )
    )
  )
)

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

  selectedRow <- reactive({
    req(input$selectedRow)
    parts <- unlist(strsplit(input$selectedRow, "-"))
    list(id = parts[1], value = parts[2])
  })

  currentDataset <- reactive({
    getExportedValue("datasets", selectedRow()$id)
  })

  output$current <- renderText({
    selectedRow()$id
  })

  output$selectedRow <- renderText({
    selectedRow()$value
  })

  output$row <- renderPrint({
    currentDataset()[selectedRow()$value, ]
  })

  output$summary <- renderPrint({
    summary(currentDataset())
  })
}

shinyApp(ui, server)
4 Likes

What an elegant approach!

It did occur to me that the set of radio buttons were functioning as a single input, but I didn't know how to go about splitting the UI apart. Not only does your solution resolve my issue, but now I have a useful template for solving similar problems in the future.

Thanks @greg!

1 Like