Search code examples
ruser-interfaceshinyshinyjs

RShiny enable/disable UI modules


I would like to set up my Shiny app to dynamically enable/disable UI modules based on user input. I am accustomed to using ShinyJS to do this in a non-modular app by passing the ID of the UI element into the enable() or disable() functions. However, with the UI now being generated inside of a module, I no longer have access to the same ID.

Here is an example app which increments by 1 each time the "counter1" button is clicked. The "counterButton" function is contained in an external module called "counterModule.R", and I would like the "toggleButton" to toggle the state of the "counterButton" between enabled and disabled. The call to toggleState() currently does nothing I assume because the "counter1" ID is not found. What would be the best way of going about this?

app.R

library(shiny)
library(shinyjs)


ui <- fluidPage(
  shinyjs::useShinyjs(), 
  mainPanel(actionButton(inputId = "toggleButton", label = "Toggle counter button"), 
  sidebarPanel(counterButton("counter1", "+1"))) 
)

server <- function(input, output, session) {
  observeEvent(input$toggleButton, {
    print("clicked toggle button")
    shinyjs::toggleState("counter1")
  })
  counterServer("counter1")
}

shinyApp(ui, server)

R/counterModule.R

counterButton <- function(id, label = "Counter") {
  ns <- NS(id)
  tagList(
      actionButton(ns("button"), label = label), 
      verbatimTextOutput(ns("out"))
  )
}

counterServer <- function(id) {
  moduleServer(
    id,
    function(input, output, session) {
      count <- reactiveVal(0)
      observeEvent(input$button, {
        count(count() + 1)
      })
      output$out <- renderText({
        count()
      })
      count
    }
  )
}

Solution

  • You have 2 possibilities. The namespacing of the shiny modules works in the following format: namespace-elementid. This means that your button in the module has the id counter1-button which is globally unique (and within in the module, you can just use button).

    Therefore, you can use the namespaced id in your main server function:

    observeEvent(input$toggleButton, {
        print("clicked toggle button")
        shinyjs::toggleState("counter1-button")
      })
    

    However, this somehow breaks the separation of ui/logic defined in the module and in the main server function. Therefore, the second option is to define the toggle button in the main app, but have the toggle logic in the module:

    library(shiny)
    library(shinyjs)
    
    ##########################
    # code of the module
    counterButton <- function(id, label = "Counter") {
      ns <- NS(id)
      tagList(
        actionButton(ns("button"), label = label), 
        verbatimTextOutput(ns("out"))
      )
    }
    
    counterServer <- function(id, toggle_action) {
      moduleServer(
        id,
        function(input, output, session) {
          count <- reactiveVal(0)
          observeEvent(input$button, {
            count(count() + 1)
          })
          output$out <- renderText({
            count()
          })
          
          observeEvent(toggle_action(), {
            print("clicked toggle button")
            shinyjs::toggleState("button")
          })
          
          count
        }
      )
    }
    
    
    ##########################
    # code of the main app
    ui <- fluidPage(
      shinyjs::useShinyjs(), 
      mainPanel(actionButton(inputId = "toggleButton", label = "Toggle counter button"), 
                sidebarPanel(counterButton("counter1", "+1"))) 
    )
    
    server <- function(input, output, session) {
    
      counterServer("counter1", toggle_action = reactive({input$toggleButton}))
    }
    
    shinyApp(ui, server)