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.
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.
Share Improve this question edited 9 hours ago Jan 9,8256 gold badges20 silver badges33 bronze badges asked 9 hours ago Eliot DixonEliot Dixon 1886 bronze badges 1 |2 Answers
Reset to default 0Instead of module servers which send over observable events to the main server which iterates over the panels and hides them, you can do all that by adding a click.event function to each expand_box
which does all the hiding using JavaScript. Set the id's inside the JS to fit your module namespace e.g. ns("expand_box")
. To your second point: I acknowledge that you probably want to reduce runtime, because only one (the last clicked) panel needs to be hidden. I would say, unless you add a large number of these expandable boxs, you should be fine.
library(shiny)
library(bslib)
# Module UI
cont_box_ui <- function(id) {
ns <- NS(id)
div(id = ns("expand_box"),
value_box(
title = "Click me",
value = "10",
theme = value_box_theme(bg = "white"),
div(id = ns("expanded_content"), style = "display: none;",
tags$p("This is additional information."),
actionButton(ns("btn"), "Click me")
)
)
)
}
ui <- page_sidebar(
sidebar = sidebar(cont_box_ui(1),cont_box_ui(2),cont_box_ui(3)), # Add value box UIs
mainPanel(),
tags$script(HTML("
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('[id$=\"expand_box\"]').forEach(function(box) {
box.addEventListener('click', function() {
let panelId = box.id.replace('expand_box', 'expanded_content');
document.querySelectorAll('[id$=\"expanded_content\"]').forEach(function(panel) {
if (panel.id !== panelId) panel.style.display = 'none';
});
document.getElementById(panelId).style.display = 'block'; // show clicked panel
});
});
});
"))
)
shinyApp(ui, \(...) {})
Directly in JS:
- If there is an input value for
last_clicked
, change the class of the corresonding expanded content toshinyjs-hide
- Set the new value for
last_clicked
- 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)
ns("btn")
insideactionButton()
because otherwise you have duplicatedid
s. – Jan Commented 9 hours ago