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:
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)
I dislike that, in my implementation, I iterate over each value box other than the one that was clicked to hide them, even though I only need to hide the previously clicked value box. Might there be a way to avoid this?
I would like to contain all the code required for this show/hide functionality within the module server/UI, without the need for an observeEvent()
within the main server to show/hide elements. Is this possible?
My only goal here is to simplify this implementation to reduce mental load when it is used in a large Shiny application.
Directly in JS:
last_clicked
, change the class of the corresonding expanded content to shinyjs-hide
last_clicked
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)