+ - 0:00:00
Notes for current slide
Notes for next slide

Mothership Connection

Communicating with Shiny

Garrick Aden-Buie

rstudio::conf(2020, "JavaScript for Shiny Users")

1

HTML + JS + CSS +
Shiny, oh my!

2

The Many Ways to Web Dev in Shiny

  1. Write your front end in raw web languages

  2. Just like HTML, but in R

  3. Use helpers to inline the CSS/JS

  4. Let htmltools and Shiny manage dependencies

3

Including Extras in Shiny

Raw HTML

wwww/index.html
ui.R
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{{ headContent() }}
<title>Fancy Pants App</title>
</head>
<body>
{{ button }}
{{ slider }}
</body>
</html>
htmlTemplate("template.html",
button = actionButton("action", "Action"),
slider = sliderInput("x", "X", 1, 100, 50)
)
4

Including Extras in Shiny

Raw HTML

wwww/index.html
server.R
R
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="shared/jquery.js"></script>
<script src="shared/shiny.js"></script>
<link rel="stylesheet" href="shared/shiny.css"/>
<title>Fancy Pants App</title>
</head>
<body>
<button class="btn btn-default action-button"
id="action" type="button">
Action
</button>
<pre id="summary" class="shiny-text-output"></pre>
</body>
</html>
output$summary <- renderPrint({
input$action
})
button <- shiny::actionButton("action", "Action")
cat(format(button))
## <button id="action" type="button" class="btn btn-default action-button">Action</button>
textOut <- shiny::verbatimTextOutput("summary")
cat(format(textOut))
## <pre id="summary" class="shiny-text-output noplaceholder"></pre>
5

Including Extras in Shiny

Raw HTML

HTML, but R

HTML
ui.R
Note
<html lang="en">
<head>
<script src="fancy.js"></script>
<link rel="stylesheet" href="fancy.css"/>
</head>
<body>
<style>/* styles */</style>
<script>// javascript...</script>
<script src="fancyShoes.js"></script>
</body>
</html>
fluidPage(
tags$head(
tags$script(src = "fancy.js"),
tags$link(
rel = "stylesheet",
href = "fancy.css"
)
),
tags$style("/* styles */"),
tags$script("// javascript"),
tags$script(src = "fancyShoes.js")
)

The source files should live in www/

Or you need to use

shiny::addResourcePath("fancy", "fancy/path/")

Also use HTML() to write worry-free

tags$script(HTML(
"el.innerHTML = '<div></div>'"
))
6

Including Extras in Shiny

Raw HTML

HTML, but R

Inline

Drop right in, path is what you see

fluidPage(
includeCSS("fancy.css"),
includeScript("fancy.js")
)

Avoid adding multiple copies of the same file with

fluidPage(
includeCSS("fancy.css"),
singleton(includeScript("fancy.js")),
singleton(includeScript("fancy.js"))
)

https://shiny.rstudio.com/articles/css.html

7

Including Extras in Shiny

Raw HTML

HTML, but R

Inline

htmltools

fancyPkg
fancyUI
app.R
fancy_pants_dependency <- function() {
htmltools::htmlDependency(
name = "fancyPants",
version = "1.2.3",
package = "fancyPkg",
src = "pants",
script = "fancy.js",
stylesheet = "fancy.css",
all_files = FALSE
)
}
fancy_pants <- function(style = "shiny") {
htmltools::tagList(
# ... pants UI ...,
fancy_pants_dependency()
)
}
fluidPage(
fancy_pants("jeans"),
fancy_pants("shiny"),
fancy_pants("stretchy")
)

But the dependencies are only loaded once!

html

<html lang="en">
<head>
<script src="fancy.js"></script>
<link rel="stylesheet" href="fancy.css"/>
</head>
<body>
<div class="fancy" id="jeans"></div>
<div class="shiny" id="jeans"></div>
<div class="stretchy" id="jeans"></div>
</body>
</html>

Also...

htmltools::htmlDependency(
name = "fancyPants",
version = "1.2.3",
package = "fancyPkg",
src = c(
file = "pants",
href = "https://cdn.fast.com/fancy",
),
script = "fancy.js",
stylesheet = "fancy.css",
all_files = FALSE
)
8

Including Extras in Shiny

What method do you like best?

Write down at least one pro and con of using each method.

  • Raw HTML (htmlTemplate())

  • HTML written in R

  • includeCSS() and includeScript()

  • htmltools::htmlDependency()

02:00
9

Including Extras in Shiny

What method do you like best?

Write down at least one pro and con of using each method.

  • Raw HTML (htmlTemplate())

  • HTML written in R

  • includeCSS() and includeScript()

  • htmltools::htmlDependency()

02:00

Compare your list with your neighbors. Does their list change your mind about any of your answers?

02:00
10

Shiny Events

11

$(jQuery)

12

JavaScript Frameworks Over Time

Source: Google Trends

13

jQuery was hugely popular, and still is!

Some analyses suggest it's used by 86% of pages on the internet.

It's also used by 100% of Shiny apps.

What's up with $?

14

What's up with $?

$ is a valid variable name in JavaScript

var $ = jQuery
15

What's up with $?

$ is a valid variable name in JavaScript

var $ = jQuery

_ is too and a few libraries take advantage of that (e.g. lodash, underscore)

16

What's up with $?

$ is a valid variable name in JavaScript

var $ = jQuery

_ is too and a few libraries take advantage of that (e.g. lodash, underscore)

Anytime you see...

$('.shiny')
// or
$().on('click')
17

What's up with $?

$ is a valid variable name in JavaScript

var $ = jQuery

_ is too and a few libraries take advantage of that (e.g. lodash, underscore)

Anytime you see...

$('.shiny')
// or
$().on('click')

think

jQuery('shiny')
// or
jQuery().on('click')
18

jQuery and Vanilla JavaScript

jQuery was way ahead of its time, but vanilla JavaScript caught up

jQuery

const $el = $('.shiny')

Vanilla

const el = document
.querySelectorAll('.shiny')
19

jQuery and Vanilla JavaScript

jQuery was way ahead of its time, but vanilla JavaScript caught up

jQuery

const $el = $('.shiny')

Vanilla

const el = document
.querySelectorAll('.shiny')

The result is very similar, but jQuery adds extra methods.

$el instanceof jQuery //true
$el.hide()
// the elements are hidden!
el instanceof NodeList //true
el.hide()
// TypeError: el.hide is not a function
20

You Might Not Need jQuery

21

You Might Not Need jQuery

unless you do certain things

22

You Might Not Need jQuery

unless you do certain things

(and there's nothing wrong with using it)

23

You Might Not Need jQuery

unless you do certain things

(and there's nothing wrong with using it)

🔖 youmightnotneedjquery.com

24

You Might Not Need jQuery

unless you do certain things

(and there's nothing wrong with using it)

🔖 youmightnotneedjquery.com

🔖 youmightnotneedjs.com

25

Two goals:

  1. show the very very basics of jQuery

  2. When do you *need to use jQuery?

Finding Elements with jQuery

Vanilla

const el = document
.querySelectorAll('.shiny')

jQuery

const $el = $('.shiny')
26

Finding Elements with jQuery

Vanilla

const el = document
.querySelectorAll('.shiny')

jQuery

const $el = $('.shiny')
const el = document
.getElementById('shiny')
const $el = $('#shiny')
27

Creating Elements with jQuery

Vanilla

const el = document
.createElement('div')
el.id = 'shiny'
document.body.appendChild(el)
28

Creating Elements with jQuery

Vanilla

const el = document
.createElement('div')
el.id = 'shiny'
document.body.appendChild(el)

jQuery

$('<div>')
.setAttr('id', 'shiny')
.appendTo('body')
29

Adding, Removing, Toggling a Class

Vanilla

const el = document
.getElementById('shiny')
el.classList.add('fancy')
el.classList.remove('fancy')
el.classList.toggle('fancy')
30

Adding, Removing, Toggling a Class

Vanilla

const el = document
.getElementById('shiny')
el.classList.add('fancy')
el.classList.remove('fancy')
el.classList.toggle('fancy')

jQuery

const $el = $('#shiny')
$el.addClass('fancy')
$el.removeClass('fancy')
$el.toggleClass('fancy')
31

Adding, Removing, Toggling a Class

Vanilla

const el = document
.getElementById('shiny')
el.classList.add('fancy')
el.classList.remove('fancy')
el.classList.toggle('fancy')

jQuery

const $el = $('#shiny')
$el.addClass('fancy')
$el.removeClass('fancy')
$el.toggleClass('fancy')
const el = document
.getElementById('shiny')
el.classList.contains('fancy')
const $el = $('#shiny')
$el.hasClass('fancy')
32

A detail that isn't obvious from this example is that the $el is an object and the jQuery methods apply to all of the objects...

Whereas we have to write extra code in vanilla to do the same thing

Adding, Removing, Toggling a Class

Vanilla

const els = document
.querySelectorAll('.shiny')
els.forEach(
el => el.classList.add('fancy')
)

jQuery

const $els = $('.shiny')
$els.addClass('fancy')
33

Listening to Events

Vanilla

const el = document.querySelectorAll('.shiny')
el.addEventListener('click', ev => {
// respond to event
})

jQuery

const $el = $('.shiny')
$el.on('click', ev => {
// respond to event
})
34

Listening to Events

Vanilla

const el = document.querySelectorAll('.shiny')
el.addEventListener('click', ev => {
// respond to event
})

jQuery

const $el = $('.shiny')
$el.on('click', '.fancy', ev => {
// respond to event if it happened
// on an element with .fancy class
})
35

Listening to Events

Vanilla

const el = document.querySelectorAll('.shiny')
el.addEventListener('click', ev => {
// respond to event
})

jQuery

$(document).on('click', '.fancy', ev => {
// respond to click events on .fancy
// *even if* the .fancy element is added later
})
36

Listening to Events

Vanilla

document.addEventListener('click', ev => {
if (ev.target.classList.contains('.fancy')) {
// then go head and respond
}
})

jQuery

$(document).on('click', '.fancy', ev => {
// respond to click events on .fancy
// *even if* the .fancy element is added later
})
37

Listening to Events

Vanilla

document.addEventListener('DOMContentLoaded', function() {
// Run this code when the DOM is good and ready
})
38

Listening to Events

Vanilla

document.addEventListener('DOMContentLoaded', function() {
// Run this code when the DOM is good and ready
})

jQuery

$(function() {
// Whenever you're ready, browser.
})
39

Listening to Events

You can create your own events in Vanilla JavaScript and in jQuery

40

Listening to Events

You can create your own events in Vanilla JavaScript and in jQuery

But you can't respond to custom events created in jQuery using Vanilla JavaScript

41

Listening to Events

You can create your own events in Vanilla JavaScript and in jQuery

But you can't respond to custom events created in jQuery using Vanilla JavaScript

So you need to use jQuery to handle Shiny's custom events

42

Shiny Events

43

Shiny State of Events

Event Name When
shiny:connected Session first starts
shiny:disconnected Session ends
shiny:sessioninitialized Shiny is ready
shiny:idle Shiny is idle
shiny:busy Shiny is busy
44

Shiny Output Events

Event Name When
shiny:outputinvalidated Element will be updated
45

Shiny Output Events

Event Name When
shiny:outputinvalidated Element will be updated
shiny:recalculating Shiny is thinking about this element
46

Shiny Output Events

Event Name When
shiny:outputinvalidated Element will be updated
shiny:recalculating Shiny is thinking about this element
shiny:recalculated Shiny server is done thinking
47

Shiny Output Events

Event Name When
shiny:outputinvalidated Element will be updated
shiny:recalculating Shiny is thinking about this element
shiny:recalculated Shiny server is done thinking
shiny:value The element changed on the page
48

Shiny Output Events

Event Name When
shiny:outputinvalidated Element will be updated
shiny:recalculating Shiny is thinking about this element
shiny:recalculated Shiny server is done thinking
shiny:value The element changed on the page
shiny:error Recalculation did not compute
49

Shiny Output Events

Event Name When
shiny:outputinvalidated Element will be updated
shiny:recalculating Shiny is thinking about this element
shiny:recalculated Shiny server is done thinking
shiny:value The element changed on the page
shiny:error Recalculation did not compute
shiny:visualchange Ouput resized, hidden or shown
50

Shiny Input Events

Event Name When
shiny:inputchanged The input changed(?)
shiny:updateinput Shiny updated the input
51

Shiny Events

repl_example("shiny-events-1")

Find the #run button

When the plot is recalculating: .on('shiny:recalculating')

  • add the disabled class to the button
  • use .setAttribute() to set disabled to true

Then undo the above steps when the output is ready: .on('shiny:value')

  • Note: you need to remove the disabled attribute

    .removeAttribute('disabled')

52

Shiny Events

repl_example("shiny-events-2")

I've added Font Awesome icons with

rmarkdown::html_dependency_font_awesome()
  • Store the run button's original .innerHTML

  • When the plot is recalculating replace the button text with

    <i class="fas fa-spinner fa-spin fa-lg"></i>
  • When the plot is done, restore the original button text

53

Shiny Events

repl_example("shiny-events-3")

Now I've added style.css

addResourcePath("figures", js4shiny:::js4shiny_file('man', 'figures'))
tags$head(includeCSS("style.css"))

and a loader inside plot-container.

  • Use jQuery to find the loader div

  • Then use jQuery's .hide() and .show() methods to hide the plot and show the loader when the plot is recalculating

  • And reverse when the plot is done

54

Shiny Events

repl_example("shiny-events-4")

  • Does this give you any ideas for your own apps?

  • shinyjs

    • disable(), enable()

    • show(), hide()

  • Questions about events?

55

Calling Shiny

56

From R ➞ Shiny

server.R
session$sendCustomMessage("fancyMessage", data)
57

From R ➞ Shiny

server.R
session$sendCustomMessage("fancyMessage", TRUE)
58

From R ➞ Shiny

server.R
session$sendCustomMessage("fancyMessage", c(13, 21, 42))
59

From R ➞ Shiny

server.R
session$sendCustomMessage("fancyMessage",
list(type ="fancy", value = 42)
)
60

From R ➞ Shiny

server.R
session$sendCustomMessage("fancyMessage",
list(type ="fancy", value = 42)
)
jsonlite::toJSON(list(type = "fancy", value = 42))
## {"type":["fancy"],"value":[42]}
61

From R ➞ Shiny

server.R
session$sendCustomMessage("fancyMessage", c(13, 21, 42))
jsonlite::toJSON(c(13, 21, 42))
## [13,21,42]
62

From R ➞ Shiny

server.R
session$sendCustomMessage("fancyMessage", TRUE)
jsonlite::toJSON(TRUE, auto_unbox = TRUE)
## true
63

From R ➞ Shiny

Then, we need to tell Shiny how to handle the message

Shiny.addCustomMessageHandler('fancyMessage', function(message) {
// ... do things with the message ...
})
64

From R ➞ Shiny

You can define the function separately if you want

function fancyMessageHandler(message) {
// ... do things with the message ...
}
Shiny.addCustomMessageHandler('fancyMessage', fancyMessageHandler)
65

From R ➞ Shiny

And you can change the name of the argument

Shiny.addCustomMessageHandler('fancyMessage', function(x) {
// ... do things with the x ...
})
66

From R ➞ Shiny

But your handler function needs one and only one argument

Shiny.addCustomMessageHandler('fancyMessage', function(x, y) {
// Shiny will yell at you!
})
67

From R ➞ Shiny

Putting the two together, your message might conditionally trigger an action

server.R
session$sendCustomMessage("fancyMessage", TRUE)
fancy.js
Shiny.addCustomMessageHandler('fancyMessage', function(condition) {
if (condition) {
// show element
} else {
// hide element
}
})
68

From R ➞ Shiny

Putting the two together, your message might update text

server.R
session$sendCustomMessage("fancyMessage", 42)
fancy.js
Shiny.addCustomMessageHandler('fancyMessage', function(value) {
const numberPants = document.getElementById('number-of-pants')
numberPants.textContent = value
})
69

From R ➞ Shiny

Putting the two together, your message might update several things

server.R
session$sendCustomMessage("fancyMessage",
list(type ="fancy", value = 42)
)
fancy.js
Shiny.addCustomMessageHandler('fancyMessage', function(opts) {
const numberPants = document.getElementById('number-of-pants')
numberPants.textContent = opts.value
numberPants.classList.add(opts.type)
})
70

From R ➞ Shiny

If your message is a list, destructuring is your friend

server.R
session$sendCustomMessage("fancyMessage",
list(type ="fancy", value = 42)
)
71

What's destructuring?

From R ➞ Shiny

If your message is a list, destructuring is your friend

server.R
session$sendCustomMessage("fancyMessage",
list(type ="fancy", value = 42)
)
fancy.js
Shiny.addCustomMessageHandler('fancyMessage', function({type, value}){
const numberPants = document.getElementById('number-of-pants')
numberPants.textContent = value
numberPants.classList.add(type)
})
72

What's destructuring?

From Shiny ➞ R

fancy.js
Shiny.setInputValue('fancyPants', 42)
73

From Shiny ➞ R

fancy.js
Shiny.setInputValue('fancyPants', 42)
server.R
# inside observe({})
input$fancyPants
## [1] 42
74

From Shiny ➞ R

fancy.js
let nPants = document.getElementById('n-fancy-pants')
nPants.addEventListener('click', function(event) {
Shiny.setInputValue('fancyPants', event.target.value)
})
server.R
# inside observe({})
input$fancyPants
## [1] 42
75

From Shiny ➞ R

Shiny won't resend values that don't change, unless...

fancy.js
Shiny.setInputValue('fancyPants', true, {priority: 'event'})
76

Try it in the console

repl_example("shiny-setInputValue")

  • Run the app and send it to your browser

  • Open that JavaScript console and run something like

    Shiny.setInputValue('hi', 'rstudio::conf')
  • Try sending strings, numbers, arrays and objects

02:00
77

R Shiny

👨🏼‍💻
for a fancier htmlwidget

78

This is my last "fancier" I promise

Better Data Updates

One more slide to get us back in the head space of Frappe Charts

Our goal

  1. We want our chart to be able to receieve updates from Shiny

    Re-render without re-drawing the whole plot.

    bit.ly/js4shiny-frappe-update

79

Our goal

  1. We want our chart to be able to receieve updates from Shiny

    Re-render without re-drawing the whole plot.

    bit.ly/js4shiny-frappe-update

  1. We want to send data back to Shiny about which element of the plot is currently selected

    isNavigable === true

    bit.ly/js4shiny-frappe-nav

80

HTML + JS + CSS +
Shiny, oh my!

2
Paused

Help

Keyboard shortcuts

, , Pg Up, k Go to previous slide
, , Pg Dn, Space, j Go to next slide
Home Go to first slide
End Go to last slide
Number + Return Go to specific slide
b / m / f Toggle blackout / mirrored / fullscreen mode
c Clone slideshow
p Toggle presenter mode
t Restart the presentation timer
?, h Toggle this help
oTile View: Overview of Slides
Esc Back to slideshow