rshinyshinyjsshinymodules

Advice to improve an implementation of shinyjs show/hide which uses modules and a 'click' event listener


I'm working on a UI element which contains a number of value boxes. When one of these value boxes is clicked, it shows hidden content. Additionally, when a value box is clicked, the other previously selected value box goes back to its initial state. As seen here:

enter image description here

I have made a working implementation of this. However, it's a bit messy and I'm concerned that it will become difficult to manage if I have several UI elements like these within a large application.

My implementation:

library(shiny)
library(bslib)
library(shinyjs)


# Module UI
cont_box_ui <- function(id) {
  ns <- NS(id)
  # Value box wrapped in a div for JavaScript tracking
  div(id = ns("expand_box"),
      value_box(
        title = "Click me",
        value = "10",
        theme = value_box_theme(bg = "white"),
        # Hidden UI content
        hidden(div(id = ns("expanded_content"),
                   tags$p("This is additional information."),
                   actionButton("btn", "Click me")
        ))
      )
  )
}



cont_box_server <- function(id) {
  moduleServer(id, function(input, output, session) {
      # Add on.click function which 
      runjs(sprintf("
      document.getElementById('%s-expand_box').addEventListener('click', function() {
        Shiny.setInputValue('last_clicked', %s);

      });
    ", id, id))
  })
  
  
}

ui <- page_sidebar(
  sidebar = sidebar(
    useShinyjs(), 
    # Add value box UIs
    cont_box_ui(1),
    cont_box_ui(2),
    cont_box_ui(3)
  ),
  mainPanel()
)

server <- function(input, output, session) {
  # Add value box servers
  cont_box_server(1)
  cont_box_server(2)
  cont_box_server(3)

  # Observe for when a value box is clicked
  observeEvent(input$last_clicked, {
    # Store ids of all show/hide panels
    panels <- c("1-expanded_content", "2-expanded_content", "3-expanded_content")
    
    # Store ids of panels which should be collapsed (even though it's just one)
    collapse <- panels[panels != sprintf("%s-expanded_content", input$last_clicked)]
    
    # Store id of panel to expand
    expand <- sprintf("%s-expanded_content", input$last_clicked)

    # Hide all panels except the one which was clicked
    for(this_panel in collapse){
      shinyjs::hide(this_panel)
    }
    
    # Show hidden content of clicked panel
    shinyjs::show(expand)
  })
  
}

shinyApp(ui, server)

My only goal here is to simplify this implementation to reduce mental load when it is used in a large Shiny application.


Solution

  • Directly in JS:

    1. If there is an input value for last_clicked, change the class of the corresonding expanded content to shinyjs-hide
    2. Set the new value for last_clicked
    3. Set the class of the expanded content of the current card to shinyjs-show
    library(shiny)
    library(bslib)
    library(shinyjs)
    
    # Module UI
    cont_box_ui <- function(id) {
      ns <- NS(id)
      # Value box wrapped in a div for JavaScript tracking
      div(id = ns("expand_box"),
          value_box(
            title = "Click me",
            value = "10",
            theme = value_box_theme(bg = "white"),
            # Hidden UI content
            hidden(div(id = ns("expanded_content"),
                       tags$p("This is additional information."),
                       actionButton(ns("btn"), "Click me")
            ))
          )
      )
    }
    
    cont_box_server <- function(id) {
      moduleServer(id, function(input, output, session) {
        
        # Add on.click function which 
        runjs(sprintf("
          document.getElementById('%s-expand_box').addEventListener('click', function() {
          
            if (Shiny.shinyapp.$inputValues['last_clicked']) {
              document.getElementById(
                Shiny.shinyapp.$inputValues['last_clicked'] + '-expanded_content'
              ).className = 'shinyjs-hide';
            }
          
            Shiny.setInputValue('last_clicked', %s);
            
            document.getElementById('%s-expanded_content').className = 'shinyjs-show';
          });
        ", id, id, id))
      })
      
      
    }
    
    ui <- page_sidebar(
      sidebar = sidebar(
        useShinyjs(), 
        # Add value box UIs
        cont_box_ui(1),
        cont_box_ui(2),
        cont_box_ui(3)
      ),
      mainPanel()
    )
    
    server <- function(input, output, session) {
      # Add value box servers
      cont_box_server(1)
      cont_box_server(2)
      cont_box_server(3)
    }
    
    shinyApp(ui, server)