17 Using JavaScript
Prelude
Note you can build a successful, production-grade shiny(Chang et al. 2022) application without ever writing a single line of JavaScript code. Even more when you can use a lot of tools that already bundle JavaScript functionalities: a great example of that being shinyjs (Attali 2021), which allows you to interact with your application using JavaScript, without writing a single line of JavaScript.
We chose to include this chapter in this book as it will help you get a better understanding on how shiny works at its core, and show you that getting at ease with JavaScript can help you get better at building web applications using R in the long run. It can also help you extend shiny with other JavaScript libraries, for example, using htmlwidgets (Vaidyanathan et al. 2023), when you get better at writing JavaScript.
That being said, note also that every inclusion of external JavaScript code or library can present a security risk for your application, so don’t include code you don’t know/understand in your application unless you are sure of what you are doing. As a rule of thumb, always go for an existing and tested solution when you need JavaScript widgets/functionalities, instead of trying to implement them yourself.
17.1 Introduction
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. 2022) code returns a series of HTML tags:
<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. What happens under the hood is a little bit complex and out of scope for this book, but the general idea is that R talks to your browser through a web socket (that you can imagine as a small “phone line” with both software listening at each end, both being able to send messages to the other),66 and this browser talks to R through the same web socket.
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.
It’s important to note here that the communication happens in both directions: from R to JavaScript, and from JavaScript to R.
In fact, when we write a piece of code like sliderInput("first_input", "Select a number", 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
).
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, smaller, 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 application.67
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 and 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 (Fay, Guyader, Rochette, et al. 2023) 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.
17.2 A quick introduction to JavaScript
17.2.1 About JavaScript
JavaScript is a programming language which has been designed to work in the browser.68
To play with a JavaScript console, the fastest way is to open your favorite web browser, and to open the developer tools.
In Google Chrome, it’s available under View > Developer > Developer Tools.
This will open a new interface where you can have access to a JavaScript console under the Console tab.
Here, you can try your first JavaScript code!
For example, you can try running var message = "Hello world"; alert(message);
.
As you might have guessed, we will not be focusing on playing with JavaScript in your browser console: what we want to know is how to insert JavaScript code inside a shiny application.
17.2.2 Including JavaScript code in your app
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 good practice when it comes to including 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 element in theJavaScript <-> R communication
part. -
golem::add_js_binding("name")
, for more advanced use cases, when you want to build your own custom inputs, i.e. when you want to create a custom HTML element that can be used to interact with shiny. See shiny.rstudio.com/articles/js-custom-input.html for more information about how to complete this skeleton.
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, or follow one of the resources linked at the end of this chapter.
17.2.3 Understanding HTML, class, and id
You have to think of a web page as a tree, where the top of the web page 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 here:
- HTML tags, which are the building blocks of the “tree”: here
div
,h2
andbutton
are HTML tags. - The button has an
id
, which is short for “identifier”. Note that this id has to be unique: the id of an element allows you to refer to this exact element. In the context of shiny, it allows JavaScript and R to talk to each other. For example, if you are rendering a plot, you have to be sure it is rendered at the correct spot in the UI, hence the need for a unique id inrenderPlot()
. Same goes for your inputs: if you are computing a value based on an input value, you have to be sure that this value is the correct 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.
17.2.4 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 = "first" name="number" class = "widediv">Hey</div>
// Query with the ID
document.querySelector("#first")
document.getElementById("first")
// With the class
document.querySelectorAll(".widediv")
document.getElementsByClassName("widediv")
// With the name attribute
document.getElementsByName("number")
// 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. Indeed, some companies (for internal reason) are still using old versions of Internet Explorer: a constraint you want to be aware of before starting to build the app, hence a question that you want to ask during the Design step.
17.2.5 About DOM events
When users navigate to a web page, they will generate events on the page: clicking, hovering over 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 web page 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 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 = "firstinput">
<script>
document.getElementById("firstinput").addEventListener(
"keypress",
function(){
alert("Pressed!")
}
)
</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:
But wait, what is this weird $()
?
That’s jQuery
, and we will discover it in the very next section!
17.2.6 About jQuery
and 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 home page (https://jquery.com)
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:
$("#firstinput")
to refer to the element with the idfirstinput
$(".widediv")
to refer to element(s) of classwidediv
$("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:
contains the href
and data-value
attributes.
You can refer to these with []
after the tag name.
$("a[href = 'https://thinkr.fr']")
refers to link(s) withhref
beinghttps://thinkr.fr
$('a[data-value="panel2"]')
refers to link(s) withdata-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 is a cross-platform library, 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 syntaxes, and put both when possible, so that you can choose the one you are the most comfortable with.
17.3 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.
17.3.1 Common patterns
alert("message")
uses the built-in alert-box mechanism from the user’s browser (i.e., thealert()
function is not part ofjQuery
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 2021): 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 thex
variable, which you can then send back to R (see later in this chapter for more info on how to do that). This can replace something like the following:
# Initiating a modalDialog that will ask the user to enter
# some information
mod <- function() {
# The modal box definition
modalDialog(
# Simple body with a textInput
tagList(
textInput("info", "Your info here")
),
footer = tagList(
modalButton("Cancel"),
actionButton("ok", "OK")
)
)
}
# When the user clicks on the "show" button in the UI,
# the modalDialog() is displayed
observeEvent(input$show, {
showModal(mod())
})
# Whenever the "ok" button is clicked, the modal is removed
observeEvent(input$ok, {
print(input$info)
removeModal()
})
$('#id').css('color', 'green');
, or in vanilla JavaScriptdocument.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 JavaScriptdocument.getElementById("id").innerText = "this";
changes the text content to “this”. This can be used to replace the following:
output$ui <- renderUI({
# Conditionnal rendering of the UI
if (this){
tags$p("First")
} else {
tags$p("Second")
}
})
-
$("#id").remove();
, or in vanilla JavaScriptvar elem = document.querySelector('#some-element'); elem.parentNode.removeChild(elem);
completely removes the element from the DOM. It can be used as a replacement forshiny::removeUI()
, or as a conditional UI. Note that this code doesn’t remove the input values on the server side: the elements only disappear from the UI, but nothing is sent to the server side. For a safe implementation, see shinyjs.
17.3.2 Where to put them: Back to JavaScript Events
OK, now that we have 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 web page: when the user clicks, hovers (the mouse goes over an element), presses keys on 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:
# Building a button using the native HTML tag
# (i.e. not using the actionButton() function)
# This button only goal is to launch this JS code
# when it is clicked
tags$button(
"Show",
onclick = "$('#plot').show()"
)
Or with shiny::tagAppendAttributes()
:
# Using tagAppendAttributes() allows to add attributes to the
# outputed UI element
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(){
fluidPage(
# We create a plotOutput, which will show an alert when
# it is clicked
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 2023l) app:
- R/mod_dataviz.R#L109, where clicking the plot generates the creation of a shiny input (we will see this below)
That, of course, works well with very short JavaScript code.
For longer JavaScript code, you can write a function inside an 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(
# Calling the function which has been defined in the
# external script
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 event.69
Note that there is a series of events which are specific to shiny, but that can be used just like the one we have just seen:
// We define a function that will ask for the visitor name, and
// then show an alert to welcome the visitor
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.
17.4 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.
17.4.1 From R to JavaScript
Calling JS from the server side (i.e. from R) is done by defining a series of CustomMessageHandler
functions: 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 from golem::add_js_handler("first_handler")
.
Then, it can be called from server side with:
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
:
// We define a handler called "computed", that can be called
// from the server side of the {shiny} application
Shiny.addCustomMessageHandler('computed', function(mess) {
// The received value (in mess) is serialized in JSON,
// so we can access the list element with object.name
alert("Computed " + mess.what + " in " + mess.sec + " secs");
})
- Then in R:
17.4.2 From JavaScript to R
How can you do the opposite (from JavaScript to R)?
shiny apps, in the browser, contain an object called Shiny
, which may be used to send values to R by creating an InputValue
.
For example, with:
// This function from the Shiny JavaScript object
// Allows to register an input name, and a value
Shiny.setInputValue("rand", Math.random())
you will bind an input that can be caught from the server side with:
# Once the input is set, it can be caught with R using:
observeEvent( input$rand , {
print( input$rand )
})
Shiny.setInputValue
70 can, of course, be used inside any JavaScript function.
Here is a small example that wraps up 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.
The `this` object here refers to the clicked element*/
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:
# We wait for the output of alertme(), which will set the
# "username" input value
observeEvent( input$username , {
cli::cat_rule("User name:")
print(input$username)
})
# This will print the id of the last clicked plot
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] "plotb"
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 the following:
$( document ).ready(function() {
// Setting a custom handler that will
// ask the users their name
// then set the returned value to a Shiny input
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
# Whenever the button is clicked,
# we call the CustomMessageHandler
observeEvent( input$showname , {
# Calling the "whoareyou" handler
golem::invoke_js(
"whoareyou",
# The id is namespaced,
# so that we get it back on the server-side
list(
id = ns("name")
)
)
})
# Waiting for input$name to be set with JavaScript
observeEvent( input$name , {
cli::cat_rule("Username is:")
print(input$name)
})
}
17.5 About {shinyjs}
JS functions
As said in the introduction to this chapter, running JavaScript code that you don’t fully control/understand can be tricky and might open doors for external attacks. In many cases, for the most common JavaScript manipulations, it’s safer to go for a package that has already been proved efficient: shinyjs.
This package, licensed in MIT since version 2.0.0, can be used to perform common JavaScript tasks: show, hide, alert, click, etc.
See deanattali.com/shinyjs/ for more information about how to use this package.
17.6 One last thing: API calls
If your application uses API calls, chances are that right now you have been 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 user’s browser, the limitation will apply to the user’s 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 data
fetch("https://api.punkapi.com/v2/beers/random")
// What do we do when we receive the data
.then((data) =>{
// TTurn the data to JSON
data.json().then((res) => {
// Send the json to R
Shiny.setInputValue("beer", res, {priority: 'event'})
})
})
// Define what happens if we fail to fetch
.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.
17.7 Learn more about JavaScript
If you want to interact straight from R with NodeJS (JavaScript in the terminal), you can try the {bubble}
(Fay 2023b) 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:
17.7.1 {shiny}
and JavaScript
We have written an online, freely available book about shiny and JavaScript: JavaScript 4
{shiny}
- Field Notes.JavaScript for
{shiny}
Users, companion website to the rstudio::conf(2020) workshop.