Context
Letâs talk about injections⌠Code & data injections, to be specific.
If you have built any sizeable shiny app, then youâve probably had to handle many hidden tab panels.
You end up with code which looks like this:
global.R
library(shiny)
ui.R
# btns to switch from one tab to the other:
<- lapply(letters[1:10], \(letter) {
btns actionButton(
inputId = paste0("btn_", letter),
label = paste0(letter, letter),
class = "btn-primary btn-lg"
)
})
<- tabsetPanel(
tabs id = "tabs",
type = "hidden",
selected = "a",
tabPanelBody(value = "a", tags$h1("Tab A")),
tabPanelBody(value = "b", tags$h1("Tab B")),
tabPanelBody(value = "c", tags$h1("Tab C")),
tabPanelBody(value = "d", tags$h1("Tab D")),
tabPanelBody(value = "e", tags$h1("Tab E")),
tabPanelBody(value = "f", tags$h1("Tab F")),
tabPanelBody(value = "g", tags$h1("Tab G")),
tabPanelBody(value = "h", tags$h1("Tab H")),
tabPanelBody(value = "i", tags$h1("Tab I")),
tabPanelBody(value = "j", tags$h1("Tab J"))
)
<- fluidPage(
ui $div(
tagsclass = "container text-center",
$div(class = "page-header", btns),
tags
tabs
) )
server.R
<- \(input, output, session) {
server # switch to selected tab:
<- \(selected) {
switch_tabs freezeReactiveValue(x = input, name = "tabs")
updateTabsetPanel(
session = session,
inputId = "tabs",
selected = selected
)
}# add btn observers:
lapply(letters[1:10], \(letter) {
<- paste0("btn_", letter)
btn_id observeEvent(input[[btn_id]], switch_tabs(letter))
}) }
Problem isolation
server.R
looks good.
Letâs focus on ui.R
, specifically on the tabs.
<- tabsetPanel(
tabs id = "tabs",
type = "hidden",
selected = "a",
tabPanelBody(value = "a", tags$h1("Tab A")),
tabPanelBody(value = "b", tags$h1("Tab B")),
tabPanelBody(value = "c", tags$h1("Tab C")),
tabPanelBody(value = "d", tags$h1("Tab D")),
tabPanelBody(value = "e", tags$h1("Tab E")),
tabPanelBody(value = "f", tags$h1("Tab F")),
tabPanelBody(value = "g", tags$h1("Tab G")),
tabPanelBody(value = "h", tags$h1("Tab H")),
tabPanelBody(value = "i", tags$h1("Tab I")),
tabPanelBody(value = "j", tags$h1("Tab J"))
)
One thing is clear: the tabPanelBody()
s are wet, not DRY. (Yes, Iâm actually smiling right now).
Realistically, the content of each tabPanelBody()
is usually a call to a module which I give the same id as the value of the tabPanelBody()
.
I have used h1
tags here for simplification.
To avoid repetition, letâs use lapply()
:
<- lapply(letters[1:10], \(value) {
panel_bodies tabPanelBody(
value = value,
$h1(
tagspaste("tab", value) |> stringr::str_to_title()
)
) })
If you have modules youâd have to use Map()
so that you iterate over the values/ids and modules.
For example:
<- Map(
panel_bodies f = \(value, mod_ui) {
tabPanelBody(value = value, mod_ui(id = value))
},list(
"home", "generate_shifts", "leave_application",
"manage_employees", "download_shifts"
),list(
mod_home_ui, mod_generate_shifts_ui, mod_leave_application_ui,
mod_manage_employees_ui, mod_download_shifts_ui
) )
I digress. Back to the lapply()
.
We now no longer repeat ourselves. Yeeey!
But our joy is not meant to last long: how do we pass this list of tabPanelBody()
s to tabsetPanel()
?
This will not work:
tabsetPanel(
id = "tabs",
type = "hidden",
selected = "a",
panel_bodies )
Error: Navigation containers expect a collection of `bslib::nav_panel()`/`shiny::tabPanel()`s and/or `bslib::nav_menu()`/`shiny::navbarMenu()`s. Consider using `header` or `footer` if you wish to place content above (or below) every panel's contents.
tabsetPanel()
expects the bare tabPanelBody()
s, without wrappers (in this case a list).
Good-old do.call()
My first thought when I encountered this was to use do.call()
.
Since we already have some default arguments passed to tabsetPanel()
, we have to modify the approach to do.call()
a little bit:
# a wrapper function with default args to `tabsetPanel()`
<- \(...) {
tp tabsetPanel(
id = "tabs",
type = "hidden",
selected = "a",
...
)
}
do.call(what = tp, args = panel_bodies)
Enter rlang::inject()
.
The older I grow, the more I prefer reading the docs, so:
::`!!!` ?rlang
The splice operator
â !!!
â implemented in dynamic dots injects a list of arguments into a function call. It belongs to the family of injection operators and provides the same functionality asdo.call()
.The two main cases for splice injection are:
- Turning a list of inputs into distinct arguments. This is especially useful with functions that take data in
...
, such asbase::rbind()
.<- list(mtcars, mtcars) dfs inject(rbind(!!!dfs))
âŚ
This is exactly what we need. Letâs now inject the tabPanelBody()
s into the tabsetPanel()
:
<- rlang::inject(
tabs tabsetPanel(
id = "tabs",
type = "hidden",
selected = "a",
!!!panel_bodies
) )
Shiny comes with injection support out of the box. Therefore, thereâs no need to wrap the code in rlang::inject()
:
<- tabsetPanel(
tabs id = "tabs",
type = "hidden",
selected = "a",
!!!panel_bodies
)
This is way cleaner and also visually appealing.
There are numerous applications of code & data injection. Check out more in the docs.