Why and how to use JS in your Shiny app

[This article was first published on Econometrics and Free Software, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

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:

  • I have defined a JS script outside the app, called box_title_js;
  • Read the title of the box;
  • In the server, there is now an observeEvent(),
  • In the UI you’ll see the following line (inside the box’s definition): tags$script(box_title_js), which executes the JS script box_title_js.

The script knows which element to change thanks to $("#box_plot h3"). That’s a bit of jQuery, which comes bundled with Shiny. jQuery allows you to query elements of the DOM. If you know nothing about it, like me, you should read this. This should give you the basic knowledge such that you’ll eventually somehow manage to select the element you actually want to change.

library(shiny)
library(ggplot2)
library(bs4Dash)

# This is the bit of JS that will update the title
# From what I could gather, $(bla bla) references the object,
# here the title, and `.html()` is a getter/setter.
# So $("#box_plot h3").html() means "take whatever is called #box_plot h3
# (h3 is the class of the title, meaning, it’s a header3 bit of text)
# and set its html to whatever string is inside `html()`"
box_title_js <- '
  Shiny.addCustomMessageHandler("box_title", function(title) {
    $("#box_plot h3").html(title)
  });
'

data(mtcars)

shinyApp(
  ui = dashboardPage(
    header = dashboardHeader(
      title = dashboardBrand(
        title = "Welcome to my award-winning dashboard!",
        color = "primary"
      )
    ),
    sidebar = dashboardSidebar(),
    body = dashboardBody(
      box(id = "box_plot", #We need to give the box an ID now, to help query it
        plotOutput("my_plot"),
        tags$script(box_title_js), #Integration of the JS script into the app
        title = "This title will change dynamically. You won’t even see this sentence!",
        width = 12,
        sidebar = boxSidebar(
          id = "sidebarid",
          startOpen = TRUE,
          selectInput("var", "Select variable:", choices = colnames(mtcars))
        ))
    ),
    controlbar = dashboardControlbar(),
    title = "DashboardPage"
  ),
  server = function(input, output, session) {

    # The following lines put the title together, and send them to the JS script
    observe({
      session$sendCustomMessage(
                "box_title",
                paste0("Currently showing variable:", input$var)
              )
    })

    output$my_plot <- renderPlot({
      ggplot(data = mtcars) +
        geom_bar(aes_string(y = input$var))
    })

  }
)

The video below shows how the app works:

The idea is as follows: a bit of code puts the title together in the server part of your app. This title gets sent to a JS script that you define somewhere where the UI and the server part know about it (for example, in your global.R file). In the UI you can now integrate the JS script using tags$script(). And you’re done!

Just for fun, let’s have a more complex example; I’ll change the background color of the box using JS as well, but depending on the selected column, the color will be different. For this, I only need to change the JS script. Using a simple if-then-else statement, I set the background color of the box to red if the selected column is mpg, else I set it to blue. The way I do this, is by using jQuery again to target the element I want to change, in this case, the object with the id “box_plot” and of class “.card-body”. Take a look at the script:

library(shiny)
library(ggplot2)
library(bs4Dash)

# This is the bit of JS that will update the title
# From what I could gather, $(bla bla) references the object,
# here the title, and `.html()` is a getter/setter.
# So $("#box_plot h3").html() means "take whatever is called #box_plot h3
# (h3 is the class of the title, meaning, it’s a header3 bit of text)
# and set its html to whatever string is inside `html()`"
box_title_js <- '
  Shiny.addCustomMessageHandler("box_title", function(title) {
  if(title.includes("mpg")){
    colour = "red"
  } else {
    colour = "blue"
  }
    $("#box_plot h3").html(title)
    $("#box_plot .card-body").css("background-color", colour)
  });
'

data(mtcars)

shinyApp(
  ui = dashboardPage(
    header = dashboardHeader(
      title = dashboardBrand(
        title = "Welcome to my award-winning dashboard!",
        color = "primary"
      )
    ),
    sidebar = dashboardSidebar(),
    body = dashboardBody(
      box(id = "box_plot", #We need to give the box an ID now, to help query it
        plotOutput("my_plot"),
        tags$script(box_title_js), #Integration of the JS script into the app
        title = "This title will change dynamically. You won’t even see this sentence!",
        width = 12,
        sidebar = boxSidebar(
          id = "sidebarid",
          startOpen = TRUE,
          selectInput("var", "Select variable:", choices = colnames(mtcars))
        ))
    ),
    controlbar = dashboardControlbar(),
    title = "DashboardPage"
  ),
  server = function(input, output, session) {

    # The following lines put the title together, and send them to the JS script
    observe({
      session$sendCustomMessage(
                "box_title",
                paste0("Currently showing variable:", input$var)
              )
    })

    output$my_plot <- renderPlot({
      ggplot(data = mtcars) +
        geom_bar(aes_string(y = input$var))
    })

  }
)

How did I know that I needed to target card-body? To find out, go to your browser, right click on the box and select Inspect (sometimes inspect element). Navigating through the source of your app in this way allows you to find the classes and ids of things you need to target, which you then can use as a query. You can even try changing stuff in real time, as the video below shows:

It’s actually scary what you can achieve with only some cursory knowledge of JS. I’m sure nothing bad ever happens because clueless beginners like me start playing around with JS.

Hope you enjoyed! If you found this blog post useful, you might want to follow me on twitter for blog post updates and buy me an espresso or paypal.me, or buy my ebook on Leanpub. You can also watch my videos on youtube. So much content for you to consoom!

Buy me an EspressoBuy me an Espresso

To leave a comment for the author, please follow the link and comment on their blog: Econometrics and Free Software.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)