Chapter 18 Using JavaScript

At its core, building a Shiny app is building a JavaScript application that can talk with an R session. This process is invisible to most Shiny developers, who usually do everything in R. In fact, most of the Shiny apps out there are 100% 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.

For example, this simple {shiny} (Chang et al. 2020) code returns a series of HTML tags:

fluidPage(
  h2("hey"), 
  textInput("act", "Ipt")
)
<div class="container-fluid">
  <h2>hey</h2>
  <div class="form-group shiny-input-container">
    <label class="control-label" for="act">Ipt</label>
    <input id="act" type="text" class="form-control" value=""/>
  </div>
</div>

Later on, when the app is launched, {shiny} binds events to UI elements, and 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 the JavaScript side of the websocket receives one of these events, the page the user sees is modified (for example, a plot is drawn). On the R end of the websocket, i.e when R receives data from the web page, a value is fetched, and something is computed.

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 end56 ), 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 the JavaScript runtime (in the browser) listens to any event happening on the slider with the id "plop", and whenever it detects that something happens to this element, something (most of the time its value) is sent back to R, and R does computation based on that value. 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 the image received from R in the <img> tags with the id bla).

So even if everything is written in R, we are writing a web application, i.e. HTML, CSS and JavaScript elements. Once you have realized that, the possibilities are endless: in fact almost anything doable in a “classic” web app can be done in Shiny with a little bit of tweaking. 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, etc. Knowing just enough JavaScript can improve the quality of your app: especially when you have been using R to render some complex UIs: think conditional panels, simulating a button click from the server, hide and show elements, etc. 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 application57 .

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 chapter, we will first review some JavaScript basics which can be used “client-side” only, i.e. only in your browser. Then, we will talk about making R & JS communicate with each other, and explore some common patterns for JavaScript in Shiny. Finally, we will quickly present some of the functions available in {golem} (Guyader et al. 2020) that can be used to launch JavaScript.

Note that this chapter does not try 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.

18.1 A quick introduction to JavaScript

JavaScript is a programming language which has been designed to work in the browser58 . 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
  • Inline, on a specific tag, for example by adding an onclick event straight on a tag

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

If you are 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? Note that in this chapter, we will not be covering basic JavaScript object and manipulation. Feel free to refer to the first chapter of JavaScript 4 Shiny - Field Notes for a detailed introduction to objects and object manipulation.

18.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 events to it and/or listen to events, hide and show, etc. 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 is what HTML semantic elements, classes, and ids are made for. Consider this piece of code:

library(shiny)
fluidPage(
  titlePanel("Hello Shiny"), 
   textInput("act", "Ipt")
)
<div class="container-fluid">
  <h2>Hello Shiny</h2>
  <div class="form-group shiny-input-container">
    <label class="control-label" for="act">Ipt</label>
    <input id="act" type="text" class="form-control" value=""/>
  </div>
</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 have 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 you 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 is also very useful for styling elements in CSS.

18.1.2 Querying in Vanilla JavaScript

In “Vanilla” JavaScript (i.e without any external plugins installed), you can query these elements using methods from the document object. For example:

// Given
<div id = "pouet" name="plop" class = "plouf">Wesh</div>

// Query with the ID
document.querySelector("#pouet")
document.getElementById("pouet")

// With the class
document.querySelectorAll(".plouf")
document.getElementsByClassName("plouf") 

// With the name attribute
document.getElementsByName("plop") 

// Using the tag name
document.getElementsByTagName("div")

Note that some of these methods have been introduced with ES6, which is a version of JavaScript that came out in 2015. This version of JavaScript is supported by most browsers since mid-2016 (and June 2017 for Firefox) (see JavaScript Versions from W3Schools). Most of your users should now be using a browser version that is compatible with ES6, but that is something that you might want to keep in mind: browser version matters when it comes to using JavaScript.

18.1.3 About DOM events

When users navigate to a webpage, they will generate events on the page: clicking, hovering elements, pressing keys, etc. All these events are listened to by the JavaScript runtime, plus some events that are not generated by the users: for example, there is a “ready” event generated when the webpage has finished loading. Most of these events are linked to a specific node in the tree: for example, if you click on something, you are clicking on a node in the DOM. That is where JavaScript events come into play: when an event is triggered in JavaScript, you can link to it a “reaction”, in other words a piece of JavaScript code that is executed when this event occurs.

Here are some examples of events:

  • click / dblclick

  • focus

  • keypress, keydown, keyup

  • mousedown, mouseenter, mouseleave, mousemove, mouseout, mouseover, mouseup

  • scroll

For a full list, please refer to https://developer.mozilla.org/fr/docs/Web/Events.

Once you have this list in mind, you can then select elements in the DOM, add an addEventListener to them, and define a callback function (which is executed when the event is triggered). For example, the code below adds an event to the input when a key is pressed, showing a native alert() to the user.

<input type="text" id = "plop">
<script> 
  document.getElementById("plop").addEventListener(
    "keypress", 
    function(){
      alert("pouet")
    }
  )
</script>

Note that Shiny also generates events, meaning that you can customize the behavior of your application based on these events. Here is a code that launches an alert when Shiny is connected:

$(document).on('shiny:connected', function(event) {
  alert('Connected to the server'); 
}); 

But wait, what is this weird $()? That’s jQuery, and we will discover it in the very next section!

18.1.4 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 objects, change object classes, click somewhere, etc. 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 have identified these elements, we can either extract or change data contained in these nodes, or invoke methods contained within these nodes. Indeed JavaScript, like 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 will 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!

Choosing jQuery or vanilla JavaScript is up to you: and in the rest of this chapter we will try to mix both syntax, and put both when possible, so that you can choose the one you are the most comfortable with.

18.2 Client-side JavaScript

It is 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 have 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.

18.2.1 Common patterns

  • $('#id').show(); and $('.class').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
  }
})

Note that this will not drastically improve the performance of your application. Though it will help to make it lighter in terms of code and easier to grasp in terms of readability: everything that can be created in the UI stays in the UI, and everything that needs to be performed by R is in the server.

  • 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 is 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} (Attali and Edwards 2020): the result is a little less aesthetically pleasing, but is 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');, or in vanilla JavaScript document.getElementById("demo").style.color = "green"; changes the CSS attributes of the selected element(s). Here, we are switching to green on the #id element.

  • $("#id").text("this"), or in vanilla JavaScript document.getElementById("id").innerText = "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();, or in vanilla JavaScript var elem = document.querySelector('#some-element'); elem.parentNode.removeChild(elem); completely removes the element from the DOM. It can be used as a replacement for shiny::removeUI(), or as a conditional UI.

18.2.2 Where to put them - Back to JavaScript Events

OK, now that we have got some ideas about JS code that can be used in Shiny, where do we put it? 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, etc. 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} (Fay 2020l) app:

  • R/mod_dataviz.R#L109, where the click on the plot generates the creation of a Shiny input (we will 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. The JavaScript file is then automatically linked in your application.

This, for example, could be:

  • In 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 in R
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 event59 .

Note that there is a series of Shiny events which are specific to Shiny but that can be used just like the one we have 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.

18.3 JavaScript <-> Shiny communication

Now that we have seen some client-side optimization, i.e. R does not 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.

18.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 side. Or if you are using {golem}, using the invoke_js() function.

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())
# OR
golem::invoke_js("fun", ...)

Note that the list() argument from your function will be converted to JSON, and read as such from JavaScript. In other words, if you have an argument called x, and you call the function with list(a = 1, b = 12), then in JavaScript you will 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)
    )
  )
})

18.3.2 From JavaScript to R

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

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

you will 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 have 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 are using modules, you will 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 will 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)
  })
}

Another thing to note about this id creation is that you can generate ids that are not defined in R beforehand. For example, let’s create the code below:

library(shiny)
ui <- function(){
  tagList(
    h3("No input in R")
  )
}

server <- function(
  input, 
  output, 
  session
){
  
  observeEvent( input$notfromr , {
    print(input$notfromr)
  })
  

}

shinyApp(ui, server)

Then, go into your developer console and type Shiny.setInputValue("notfromr", Math.random()). This should print a random number in your console, even if this input wasn’t defined in your UI function.

18.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

18.4.1 golem::invoke_js()

  • showid & hideid, showclass & hideclass show and hide elements using 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, note that 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.

18.5 One Last Thing: API calls

If your application uses API calls, chances are that right now you have being doing them straight from R. But there are downsides to that approach. Notably, if the API limits requests based on an IP and your application gets a lot of traffic, your users will end up being unable to use the app because of this restriction.

So, why not switch to writing these API calls in JavaScript? As JavaScript is run inside the users’ browser, the limitation will apply to the users’ IPs, not the one where the application is deployed, allowing you to more easily scale your application.

You can write this API call using the fetch() JavaScript function. It can then be used inside a Shiny JavaScript handler, or as a response to a DOM event (for example, with tags$button("Get Me One!", onclick = "get_rand_beer()"), as we will see below).

Here is the general skeleton that would work when the API does not need authentication and returns JSON.

  • Inside JavaScript (here, we create a function that will be available on an onclick event)
// FUNCTION definition
const get_rand_beer = () => {
    // FETCHING THE API DATA
    fetch("https://api.punkapi.com/v2/beers/random")
      // DEFINE WHAT HAPPENS WHEN JAVASCRIPT RECEIVES THE DATA
      .then((data) =>{
        // TURN THE DATA TO JSON
        data.json().then((res) => {
          // SEND THE JSON TO R
          Shiny.setInputValue("beer", res, {priority: 'event'})
        })
      })
      // DEFINE WHAT HAPPENS WHEN THERE IS AN ERROR FETCHING THE API
      .catch((error) => {
        alert("Error catching result from API")
      })
  };
  • Observe the event in your server:
observeEvent( input$beer , {
  # Do things with beer
})

Note that the data shared between R and JavaScript is serialized to JSON, so you will have to manipulate that format once you receive it in R.

Learn more about fetch() at Using Fetch.

18.6 Learn more about JavaScript

If you want to interact straight from R with NodeJS (JavaScript in the terminal), you can try the {bubble} (Fay 2020b) package. Be aware that you will need to have a working NodeJS installation on your machine.

It can be installed from GitHub

remotes::install_github("ColinFay/bubble")

You can use it in RMarkdown chunks by setting the {knitr} engine:

bubble::set_node_engine()

Or straight in the command line with:

node_repl()

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

18.6.1 Shiny & JavaScript

18.6.4 Intermediate / advanced JavaScript

References

Attali, Dean, and Tristan Edwards. 2020. Shinyalert: Easily Create Pretty Popup Messages (Modals) in ’Shiny’. https://CRAN.R-project.org/package=shinyalert.

Chang, Winston, Joe Cheng, JJ Allaire, Yihui Xie, and Jonathan McPherson. 2020. Shiny: Web Application Framework for R. https://CRAN.R-project.org/package=shiny.

Fay, Colin. 2020b. Bubble: Launch and Interact with a Nodejs Session.

Fay, Colin. 2020l. Tidytuesday201942: A Golem App for Tidy Tuesday. http://www.github.com/ColinFay/tidytuesday201942.

Guyader, Vincent, Colin Fay, Sébastien Rochette, and Cervan Girard. 2020. Golem: A Framework for Robust Shiny Applications. https://github.com/ThinkR-open/golem.


  1. See this post on dev.to https://dev.to/buzzingbuzzer/comment/g0g for a quick introduction to the general concept of web sockets↩︎

  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} (Fay 2020f), 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