Chapter 20 Using JavaScript

At its core, building a Shiny app is building a JavaScript app that can talk with an R session. This process is invisible to most Shiny developers, who usually do everything in R. And in the end, this is the case: most of the Shiny apps out there are written with R.

In fact, when you are writing UI elements in Shiny, what you are actually doing is building a series of HTML tags, which are then linked to JavaScript events. Later on, when the app is running, these JavaScript events will communicate with R, in the sense that they will send data to R, and receive data from R. Most of the time, when these JavaScript events are receiving data, they modify the page the user sees. What happens under the hood is a little bit complex and out of scope for this book, but the general idea is: R talks to your browser through a web socket (that you can imagine as a small “phone line” with both software modules listening at each end23), and this browser talks to R through the same web socket.

// TODO: create here a simple Flowchart

// R -> (Web Socket) -> JS

// R <- (Web Socket) <- JS

It’s important to note here that the communication happens in both ways: from R to JavaScript, and from JavaScript to R. In fact, when we write a piece of code like sliderInput("plop", "this", 1, 10, 5), what we are doing is creating a binding between JavaScript and R, where JavaScript listens to any event happening in the browser on the slider with the id "plop", and whenever JavaScript detects that something happens to this element, something (most of the time its value) is sent back to R. With output$bla <- renderPlot({}), what we are doing is making the two communicate the other way around: we are telling JavaScript to listen to any incoming data from R for the id "bla", and whenever JavaScript sees incoming data from R, it puts it into the proper HTML tag (here, JavaScript inserts in the page the image received from R).

So even if everything is written with R, we are writing a web application, i.e. HTML, CSS and JavaScript elements. Once you’ve realized that, the possibilities are endless: in fact almost anything doable in a “classic” web app can be done in Shiny. What this also implies is that getting (even a little bit) better at writing HTML, CSS, and especially JavaScript will make your app better, lighter, and more user-friendly, as JavaScript is a language that has been designed to interact with a web page: change element appearances, hide and show things, click somewhere, show alerts and prompts… Knowing just enough JavaScript can improve the quality of your app: especially when you’ve been using R to render some complex UIs: think conditional panels, simulating a button click from the server, hide and show elements… All these things are good examples of where you should be using JavaScript instead of building more or less complex renderUI or insertUI patterns in your server.

Moreover, the number of JavaScript libraries available on the web is tremendous ; and the good news is that Shiny has everything it needs to bundle external JavaScript libraries inside your application24.

This is what this section of the book aims at: giving you just enough JavaScript knowledge to lighten your Shiny App, in order to improve the global user and developer experience. In this part, we’ll first review some JavaScript basics which can be used “client-side” only, i.e. only in your browser, or by making R & JS communicate with each other. In this section, we’ll also explore common patterns for JavaScript in Shiny. Finally, we’ll quickly present some of the functions from {golem} which are built on top of JavaScript.

Note that this chapter is not supposed to be a comprehensive JavaScript course. External resources are linked all throughout this chapter and at the end if you want to dive deeper into JavaScript.

20.1 A quick introduction to JavaScript

JavaScript is a programming language which has been designed to work in the browser25. There are three ways to include the JavaScript code inside your web app:

  • As an external file, which is served to the browser alongside your main application page
  • Inside a <script> HTML tag inside your page
  • On a specific tag, for example by adding an onclick event straight on the tag

Note that the good practice when it comes to include JavaScript is to add the code inside an external file.

If you’re working with {golem}, including a JavaScript file is achieved via two functions:

  • golem::add_js_file("name"), which adds a standard JavaScript file, i.e. one which is not meant to be used to communicate with R. We’ll see in the first part of this chapter how to add JavaScript code there.
  • golem::add_js_handler("name"), which creates a file with a skeleton for Shiny handlers. We’ll see this second type of elements in the JavaScript <-> Shiny communication part.

OK, good, but what do we do now?

20.1.1 Understanding html, class, and id

You have to think of a web page as a tree, where the top of the webpage is the root node, and every element in the page is a node in this tree (this tree is called a DOM, for Document Object Model). You can work on any of these HTML nodes with JavaScript: modify it, bind to it and/or listen to events, hide and show… But first, you have to find a way to identify these elements: either as a group of elements or as a unique element inside the whole tree. That’s what html semantic elements, classes, and ids are made for. Consider this piece of code:

library(shiny)
fluidPage(
  titlePanel("Hello Shiny"), 
  actionButton("go", "go")
)
<div class="container-fluid">
  <h2>Hello Shiny</h2>
  <button id="go" type="button" class="btn btn-default action-button">go</button>
</div>

This {shiny} code creates a piece of HTML code containing three nodes: a div with a specific class (a BootStrap container), an h2, which is a level-two header, and a button which has an id and a class. Both are included in the div. Let’s detail what we’ve got here:

  • HTML tags, which are the building blocks of the “tree”: here div, h2 and button are HTML tags.
  • The button has an id, which is short for “identifier”. This id has to to be unique: this reference allows to refer to this exact element, and more specifically, it allows JavaScript and R to talk to each other: if you click on a button, you have to be sure you are referring to this specific button, and only that one.
  • Elements can have a class which can apply to multiple elements. This can be used in JavaScript, but it’s also very useful for styling elements in CSS.

20.1.2 About jQuery & jQuery selectors

The jQuery framework is natively included in Shiny.

jQuery is a fast, small, and feature-rich JavaScript library. It makes things like HTML document traversal and manipulation, event handling, animation, and Ajax much simpler with an easy-to-use API that works across a multitude of browsers.

jQuery is a very popular JavaScript library which is designed to manipulate the DOM, its events and its elements. It can be used to do a lot of things, like hide and show, change class, click somewhere… And to be able to do that, it comes with the notion of selectors, which will be put between $(). You can use, for example:

  • $("#plop") to refer to the element with the id plop

  • $(".pouet") to refer to element(s) of class pouet

  • $("button:contains('this')") to refer to the buttons with a text containing 'this'

You can also use special HTML attributes, which are specific to a tag. For example, the following HTML code:

<a href = "https://thinkr.fr" data-value = "panel2">ThinkR</a>

contains the href & data-value attributes. You can refer to these with [] after the tag name.

  • $("a[href = 'https://thinkr.fr']) refers to link(s) with href being https://thinkr.fr

  • $('a[data-value="panel2"]') refers to link(s) with data-value being "panel2"

These and other selectors are used to identify one or more node(s) in the big tree which is a web page. Once we’ve identified these elements, we can either extract or change data contained in these nodes, or invoke methods contained within these nodes. Indeed JavaScript, as R, can be used as a functional language, but most of what we do is done in an object-oriented way. In other words, you’ll interact with objects from the web page, and these objects will contain data and methods.

Note that this is not specific to jQuery: elements can also be selected with standard JavaScript. jQuery has the advantage of simplifying selections and actions and to be cross-platform, making it easier to ship applications that can work on all major browsers. And it comes with Shiny for free!

20.2 Client-side JavaScript

It’s hard to give an exhaustive list of what you can do with JavaScript inside Shiny. As a Shiny app is part JavaScript, part R, once you’ve got a good grasp of JavaScript you can quickly enhance any of your applications. That being said, a few common things can be done that would allow you to immediately optimize your application: i.e. small JavaScript functions that will prevent you from writing complex algorithmic logic in your application server.

20.2.1 Common patterns

  • $('#id').show(); and $('#id').hide(); show and hide one or more elements that match the given selector. For example, this can be use to replace:
output$ui <- renderUI({
  if (this){
    tags(...)
  } else {
    NULL
  }
})
  • alert("message") uses the built-in alert-box mechanism from the user’s browser (i.e., the alert() function is not part of jQuery but it’s built inside the user’s browser). It works well as it relies on the browser instead of relying on R or on a specific JavaScript library. You can use this functionality to replace a call to {shinyalert}: the result is a little less aesthetically pleasing, but that’s easier to implement and maintain.

  • var x = prompt("this", "that"); this function opens the built-in prompt, which is a text area where the user can input text. With this code, when the user clicks “OK”, the text is stored in the x variable, which you can then send back to R (see further part down this chapter for more info on how to do that). This can replace something like the following:

mod <- function() {
  modalDialog(
    tagList(
      textInput(ns("info"), "Your info here")
    ),
    footer = tagList(
      modalButton("Cancel"),
      actionButton(ns("ok"), "OK")
    )
  )
}

observeEvent(input$show, {
  showModal(mod())
})
observeEvent(input$ok, {
  removeModal()
})
  • $('#id').css('color', 'green'); changes the CSS attributes of the selected element(s). Here, we’re switching to green on the #id element.

  • $("#id").text( "this" ); changes the text content to “this”. This can be used to replace

output$ui <- renderUI({
  if (this){
    tags$p("First")
  } else {
    tags$p("Second")
  }
})
  • $("#id").remove(); completely removes the element from the DOM. IT can be used as a replacement for shiny::removeUI(), or as a conditional UI.

20.2.2 Where to put them - introduction to JavaScript events

OK, now that we’ve got some ideas about JS code that can be used in Shiny, where do we put them? HTML and JS have a concept called events, which are… well events that happen when the user manipulates the webpage: when the user clicks, hovers (the mouse goes over an element), presses the keyboard… All these events can be used to trigger a JavaScript function.

Here are some examples of adding JavaScript functions to DOM events:

+onclick

The onclick attribute can be added straight inside the HTML tag when possible:

tags$button(
  "Show"
  onclick = "$('#plot').show()"
)

Or with shiny::tagAppendAttributes:

plotOutput(
  "plot"
) %>% tagAppendAttributes(
  onclick = "alert('hello world')"
)

Here is for example a small Shiny app that implements this behavior:

library(shiny)
library(magrittr)
ui <- function(request){
  fluidPage(
    plotOutput(
      "plot"
    ) %>% tagAppendAttributes(
      onclick = "alert('iris plot!')"
    )
  )
}

server <- function(input, output, session){
  output$plot <- renderPlot({
    plot(iris)
  })
}

shinyApp(ui, server)

You can find a real Life example of this tagAppendAttributes in the {tidytuesday201942} app:

  • R/mod_dataviz.R#L109, where the click on the plot generates the creation of a Shiny input (we’ll see this below)

That, of course, works well with very small JavaScript code. For longer JavaScript code, you can write a function inside and external file, and add it to your app. In {golem}, this works by launching the add_js_file("name"), which will create a .js file. You’ll then need to add this function using tags$script(src="www/name.js") inside golem_add_external_resources() in R/app_ui.R file.

This, for example, could be:

inst/app/www/script.js

function alertme(id){
  // Asking information
  var name = prompt("Who are you?");
  // Showing an alert
  alert("Hello " + name + "! You're seeing " + id);
}

Then:

plotOutput(
  "plot"
) %>% tagAppendAttributes(
  onclick = "alertme('plot')"
)

Inside this inst/app/www/script.js, you can also attach a new behavior with jQuery to one or several elements. For example, you can add this alertme / onclick behavior to all plots of the app:

function alertme(id){
  var name = prompt("Who are you?");
  alert("Hello " + name + "! You're seeing " + id);
}

/* We're adding this so that the function is launched only
when the document is ready */
$(function(){ 
  // Selecting all Shiny plots
  $(".shiny-plot-output").on("click", function(){
    /* Calling the alertme function with the id 
    of the clicked plot */
    alertme(this.id);
  });
});

Then, all the plots from your app will receive this on-click event26.

Note that there is a series of Shiny events which are specific to Shiny but that can be used just like the one we’ve just seen:

function alertme(){
  var name = prompt("Who are you?");
  alert("Hello " + name + "! Welcome to my app");
}

$(function(){ 
  // Waiting for Shiny to be connected
  $(document).on('shiny:connected', function(event) {
    alertme();
  });
});

See JavaScript Events in Shiny for the full list of JavaScript events available in Shiny.

20.3 JavaScript <-> Shiny communication

Now that we’ve seen some client-side optimization, i.e. R doesn’t do anything with these events when they happen (in fact R is not even aware they happened), let’s now see how we can make these two communicate with each other.

20.3.1 From R to JavaScript

Calling JS from the server side (i.e from R) is done by defining a series of CustomMessageHandler: these are functions with one argument that can then be called using the session$sendCustomMessage() method from the server.

You can define them using this skeleton:

$( document ).ready(function() {
  Shiny.addCustomMessageHandler('fun', function(arg) {
  
  })
});

This skeleton is the one generated by golem::add_js_handler("plop").

Then, it can be called from server-side with:

session$sendCustomMessage("fun", list())

Note that the list() argument from your function will be converted to JSON, and read as such from JS. In other words, if your have an argument called x, and you call the function with list(a = 1, b = 12), then in JS you’ll be able to use x.a and x.b.

For example:

In inst/app/www/script.js

Shiny.addCustomMessageHandler('computed', function(mess) {
  alert("Computed " + mess.what + " in " + mess.sec + " secs");
})

Then in R:

observe({
  deb <- Sys.time()
  # Do the computation for id
  Sys.sleep(
    sample(1:5, 1)
  )
  session$sendCustomMessage(
    "computed", 
    list(
      what = "plop", 
      sec = round(Sys.time() - deb)
    )
  )
})

20.3.2 From JavaScript to R

How to do the other way around (from JS to R)? Shiny apps, in the browser, contain an object called Shiny, which can be used to send values to R, by creating an IputValue. For example, with:

Shiny.setInputValue("rand", Math.random())

you’ll bind an input that can be caught from the server side with:

observeEvent( input$rand , {
  print( input$rand )
})

This Shiny.setInputValue can of course be used inside any JavaScript function. Here is a small example wrapping some of the things we’ve seen previously:

In inst/app/www/script.js

function alertme(){
  var name = prompt("Who are you?");
  alert("Hello " + name + "! Welcome to my app");
  Shiny.setInputValue("username", name)
}

$(function(){ 
  // Waiting for Shiny to be connected
  $(document).on('shiny:connected', function(event) {
    alertme();
  });
  
  $(".shiny-plot-output").on("click", function(){
    /* Calling the alertme function with the id 
    of the clicked plot */
    Shiny.setInputValue("last_plot_clicked", this.id);
  });
});

These events (getting the user name and the last plot clicked), can then be caught from the server side with:

observeEvent( input$username , {
  cli::cat_rule("User name:")
  print(input$username)
})

observeEvent( input$last_plot_clicked , {
  cli::cat_rule("Last plot clicked:")
  print(input$last_plot_clicked)
})

Which will give:

> golex::run_app()
Loading required package: shiny

Listening on http://127.0.0.1:5495
── User name: ─────────────────────────────────────────────────────
[1] "Colin"
── Last plot clicked: ─────────────────────────────────────────────
[1] "plota"
── Last plot clicked: ─────────────────────────────────────────────
[1] "plopb"

Important note: if you’re using modules, you’ll need to pass the namespacing of the id to be able to get it back from the server. This can be done using the session$ns function, which comes by default in any golem-generated module. In other words, you’ll need to write something like:

$( document ).ready(function() {
  Shiny.addCustomMessageHandler('whoareyou', function(arg) {
    var name = prompt("Who are you?")
    Shiny.setInputValue(arg.id, name);
  })
});
mod_my_first_module_ui <- function(id){
  ns <- NS(id)
  tagList(
    actionButton(
      ns("showname"), "Enter your name"
    )
  )
}

mod_my_first_module_server <- function(input, output, session){
  ns <- session$ns
  observeEvent( input$showname , {
    session$sendCustomMessage(
      "whoareyou", 
      list(
        id = ns("name")
      )
    )
  })
  
  observeEvent( input$name , {
    cli::cat_rule("Username is:")
    print(input$name)
  })
}

20.4 About {golem} js functions

{golem} comes with a series of JavaScript functions that you can call from the server. These functions are added by default with golem::activate_js() in app_ui.

Then they are called with golem::invoke_js("function", "element").

This element can be one of a series of elements (most of the time scalar elements) which can be used to select the DOM node you want to interact with. It can be a full jQuery selector, an id or a class. Note that you can pass multiple elements, with invoke_js ... parameters

20.4.1 golem::invoke_js()

  • showid & hideid, showclass & hideclass show and hide elements through their id or class.
golem::invoke_js("showid", ns("plot"))
  • showhref & hidehref hide and show a link by trying to match the href content.
golem::invoke_js("showhref", "panel2")
  • clickon click on the element. You have to use the full jQuery selector.

  • show & hide show and hide elements, using the full jQuery selector.

See ?golem::activate_js for a full list of built-in functions.

20.5 Learn more about JavaScript

We have written an online, freely available bbok about Shiny & JavaScript. You’ll find it at http://connect.thinkr.fr/js4shinyfieldnotes/

Here is a list of external resources to learn more about JavaScript:

20.5.3 Intermediate / advanced JavaScript


  1. See this dev.to post for a quick introduction to the general concept of web socket

  2. This can also be done by wrapping a JS libraries inside a package, which will later be used inside an application. See for example {glouton}, which is a wrapper around the js-cookie JavaScript library.

  3. You can now work with JavaScript in a server with Node.JS, but this is out of scope of this book. See linked resources to learn more.

  4. This click behavior can also be done through $(".shiny-plot-output").click(...). We chose to display the on("click") pattern as it can be generalized to all DOM events.


ThinkR Website