Website - Youtube - About - Talks - Books - Packages - RSS

Why and how to use JS in your Shiny app

R
proramming
Published

October 1, 2022

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!

Javascript to the rescue

Let me be very clear: I know almost nothing about Javascript. I just knew a couple of things: Javascript can be used for exactly what I needed to do (change part of the UI), and it does so by making use of the DOM (which I also knew a little bit about). The DOM is a tree-like representation of a webpage. So you have your webpage’s header, body, footer, and inside of the body, for example, in my case here, we have a box with a title. That title has an address, if you will, represented by one of the branches of the DOM. At least, that’s the way I understand it.

In any case, it is possible to integrate JS scripts inside any Shiny app. So here’s what I thought I would do: I would create the title of my box as a reactive value inside the server part of my app, and would then pass this title to a JS script which would then, using the DOM, knock at the door of the box and give it its new title. Easier written in plain English than in R/JS though. But surprisingly enough, it didn’t turn out to be that complicated, and even someone (me) with only a very, very, shallow knowledge of JS could do it in less than an hour. First thing’s first, we need to read this documentation: Communicating with Shiny via JavaScript, especially the second part, From R to JavaScript.

Because we won’t re-render the whole box, let’s simply reuse the app from before, in which the box is static. The script is below, but first read the following lines, then take a look at the script: