Why and how to use JS in your Shiny app
The snake biting its own tail
Disclaimer: I’m a beginner at JS, so don’t ask me about the many intricacies of JS.
I’ve been working on a Shiny app for work these past few weeks, and had to use Javascript to solve a very specific issue I encountered. Something for which, as far as I know, there is no other solution than using Javascript. The problem had to do with dynamically changing the UI of an app. The way to usually achieve this is using renderUI()/uiOutput()
. For example, consider the following little app (if you don’t want to run it, watch the video below):
library(shiny)
library(ggplot2)
data(mtcars)
ui <- fluidPage(
selectInput("var", "Select variable:", choices = colnames(mtcars)),
uiOutput("welcome"),
plotOutput("my_plot")
)
server <- function(input, output) {
output$welcome <- renderUI({
tags$div(paste0("Welcome to my award-winning app! Currently showing variable: ", input$var))
})
output$my_plot <- renderPlot({
ggplot(data = mtcars) +
geom_bar(aes_string(y = input$var))
})
}
shinyApp(ui, server)
As you can see, when the user chooses a new variable, the plot gets updated of course, but the welcome message changes as well. Normally, the UI of a Shiny app gets rendered once, at startup, and stays fixed. But thanks to renderUI()/uiOutput()
, it is possible to change UI elements on the fly, and anything can go inside of renderUI()/uiOutput()
, it can be something much more complex than a simple message like in my example above.
So, why did I need to use Javascript to basically achieve the same thing? The reason is that I am currently using {bs4Dash}
, an amazing package to build Shiny dashboard using Bootstrap 4. {bs4Dash}
comes with many neat features, one of them being improved box()
es (improved when compared to the box()
es from {shinydashboard}
). These improved boxes allow you to do something like this (if you don’t want to run it, watch the video below):
library(shiny)
library(ggplot2)
library(bs4Dash)
data(mtcars)
shinyApp(
ui = dashboardPage(
header = dashboardHeader(
title = dashboardBrand(
title = "Welcome to my award-winning dashboard!",
color = "primary"
)
),
sidebar = dashboardSidebar(),
body = dashboardBody(
box(
plotOutput("my_plot"),
title = "This is where I will put the title, but bear with me.",
width = 12,
sidebar = boxSidebar(
id = "sidebarid",
startOpen = TRUE,
selectInput("var", "Select variable:", choices = colnames(mtcars))
))
),
controlbar = dashboardControlbar(),
title = "DashboardPage"
),
server = function(input, output, session) {
output$my_plot <- renderPlot({
ggplot(data = mtcars) +
geom_bar(aes_string(y = input$var))
})
}
)
Each box can have a side bar, and these side bars can contain toggles specific to the graph. If you click outside the side bar, the side bar closes; to show the side bar, click on the little gears in the top right corner of the side bar. Ok we’re almost done with the setup: see how the box can have a title? Let’s make it change like before; for this, because the title is part of the box()
function, I need to re-render the whole box (if you don’t want to run it, watch the video below):
library(shiny)
library(ggplot2)
library(bs4Dash)
data(mtcars)
shinyApp(
ui = dashboardPage(
header = dashboardHeader(
title = dashboardBrand(
title = "Welcome to my award-winning dashboard!",
color = "primary"
)
),
sidebar = dashboardSidebar(),
body = dashboardBody(
uiOutput("my_dynamic_box")
),
controlbar = dashboardControlbar(),
title = "DashboardPage"
),
server = function(input, output, session) {
output$my_plot <- renderPlot({
ggplot(data = mtcars) +
geom_bar(aes_string(y = input$var))
})
output$my_dynamic_box <- renderUI({
box(
plotOutput("my_plot"),
title = paste0("Currently showing variable:", input$var),
width = 12,
sidebar = boxSidebar(
id = "sidebarid",
startOpen = TRUE,
selectInput("var", "Select variable:", choices = colnames(mtcars))
))
})
}
)
Now try changing variables and see what happens… as soon as you change the value in the selectInput()
, it goes back to selecting mpg
! The reason is because the whole box gets re-rendered, including the selectInput()
, and its starting, default, value (even if we did not specify one, this value is simply the first element of colnames(mtcars)
which happens to be mpg
). So now you see the problem; I have to re-render part of the UI, but doing so puts the selectInput()
on its default value… so I need to be able to only to re-render the title, not the whole box (or move the selectInput()
outside the boxes, but that was not an acceptable solution in my case).
So there we have it, we’re done with the problem statement. Now on to the solution.
UPDATE
It turns out that it’s not needed to use JS for this special use case! {bs4Dash}
comes with a function, called updateBox()
which updates a targeted box. You can read about it here. Thanks to {bs4Dash}
’s author, David Granjon for the heads-up!
Well, even though my specific use case does not actually need Javascript, you can continue reading, because in case your use case does not have an happy ending like mine, the blog post is still relevant!