Garrick Aden-Buie
rstudio::conf(2020, "JavaScript for Shiny Users")
Write your front end in raw web languages
Just like HTML, but in R
Use helpers to inline the CSS/JS
Let htmltools and Shiny manage dependencies
<!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))
<!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>
<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>'"))
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
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)
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
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
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.
$
is a valid variable name in JavaScript
var $ = jQuery
$
is a valid variable name in JavaScript
var $ = jQuery
_
is too and a few libraries take advantage of that (e.g. lodash, underscore)
$
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')
$
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')// orjQuery().on('click')
jQuery was way ahead of its time, but vanilla JavaScript caught up
jQuery
const $el = $('.shiny')
Vanilla
const el = document .querySelectorAll('.shiny')
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 //trueel.hide()// TypeError: el.hide is not a function
You Might Not Need jQuery
You Might Not Need jQuery
unless you do certain things
You Might Not Need jQuery
unless you do certain things
(and there's nothing wrong with using it)
You Might Not Need jQuery
unless you do certain things
(and there's nothing wrong with using it)
You Might Not Need jQuery
unless you do certain things
(and there's nothing wrong with using it)
Two goals:
show the very very basics of jQuery
When do you *need to use jQuery?
Vanilla
const el = document .querySelectorAll('.shiny')
jQuery
const $el = $('.shiny')
Vanilla
const el = document .querySelectorAll('.shiny')
jQuery
const $el = $('.shiny')
const el = document .getElementById('shiny')
const $el = $('#shiny')
Vanilla
const el = document .createElement('div')el.id = 'shiny'document.body.appendChild(el)
Vanilla
const el = document .createElement('div')el.id = 'shiny'document.body.appendChild(el)
jQuery
$('<div>') .setAttr('id', 'shiny') .appendTo('body')
Vanilla
const el = document .getElementById('shiny')el.classList.add('fancy') el.classList.remove('fancy')el.classList.toggle('fancy')
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')
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')
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
Vanilla
const els = document .querySelectorAll('.shiny')els.forEach( el => el.classList.add('fancy'))
jQuery
const $els = $('.shiny')$els.addClass('fancy')
Vanilla
const el = document.querySelectorAll('.shiny')el.addEventListener('click', ev => { // respond to event})
jQuery
const $el = $('.shiny')$el.on('click', ev => { // respond to event})
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})
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})
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})
Vanilla
document.addEventListener('DOMContentLoaded', function() { // Run this code when the DOM is good and ready})
Vanilla
document.addEventListener('DOMContentLoaded', function() { // Run this code when the DOM is good and ready})
jQuery
$(function() { // Whenever you're ready, browser.})
You can create your own events in Vanilla JavaScript and in jQuery
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
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
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 |
Event Name | When |
---|---|
shiny:outputinvalidated | Element will be updated |
Event Name | When |
---|---|
shiny:outputinvalidated | Element will be updated |
shiny:recalculating | Shiny is thinking about this element |
Event Name | When |
---|---|
shiny:outputinvalidated | Element will be updated |
shiny:recalculating | Shiny is thinking about this element |
shiny:recalculated | Shiny server is done thinking |
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 |
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 |
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 |
Event Name | When |
---|---|
shiny:inputchanged | The input changed(?) |
shiny:updateinput | Shiny updated the input |
Learn more: shiny.rstudio.com/articles/js-events.html
repl_example("shiny-events-1")
Find the #run
button
When the plot is recalculating: .on('shiny:recalculating')
.setAttribute()
to set disabled to trueThen undo the above steps when the output is ready: .on('shiny:value')
Note: you need to remove the disabled attribute
.removeAttribute('disabled')
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
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
repl_example("shiny-events-4")
Does this give you any ideas for your own apps?
shinyjs
disable()
, enable()
show()
, hide()
Questions about events?
session$sendCustomMessage("fancyMessage", data)
session$sendCustomMessage("fancyMessage", TRUE)
session$sendCustomMessage("fancyMessage", c(13, 21, 42))
session$sendCustomMessage("fancyMessage", list(type ="fancy", value = 42))
session$sendCustomMessage("fancyMessage", list(type ="fancy", value = 42))
jsonlite::toJSON(list(type = "fancy", value = 42))
## {"type":["fancy"],"value":[42]}
session$sendCustomMessage("fancyMessage", c(13, 21, 42))
jsonlite::toJSON(c(13, 21, 42))
## [13,21,42]
session$sendCustomMessage("fancyMessage", TRUE)
jsonlite::toJSON(TRUE, auto_unbox = TRUE)
## true
Then, we need to tell Shiny how to handle the message
Shiny.addCustomMessageHandler('fancyMessage', function(message) { // ... do things with the message ...})
You can define the function separately if you want
function fancyMessageHandler(message) { // ... do things with the message ...}Shiny.addCustomMessageHandler('fancyMessage', fancyMessageHandler)
And you can change the name of the argument
Shiny.addCustomMessageHandler('fancyMessage', function(x) { // ... do things with the x ...})
But your handler function needs one and only one argument
Shiny.addCustomMessageHandler('fancyMessage', function(x, y) { // Shiny will yell at you!})
Putting the two together, your message might conditionally trigger an action
session$sendCustomMessage("fancyMessage", TRUE)
Shiny.addCustomMessageHandler('fancyMessage', function(condition) { if (condition) { // show element } else { // hide element }})
Putting the two together, your message might update text
session$sendCustomMessage("fancyMessage", 42)
Shiny.addCustomMessageHandler('fancyMessage', function(value) { const numberPants = document.getElementById('number-of-pants') numberPants.textContent = value})
Putting the two together, your message might update several things
session$sendCustomMessage("fancyMessage", list(type ="fancy", value = 42))
Shiny.addCustomMessageHandler('fancyMessage', function(opts) { const numberPants = document.getElementById('number-of-pants') numberPants.textContent = opts.value numberPants.classList.add(opts.type)})
If your message is a list, destructuring is your friend
session$sendCustomMessage("fancyMessage", list(type ="fancy", value = 42))
What's destructuring?
If your message is a list, destructuring is your friend
session$sendCustomMessage("fancyMessage", list(type ="fancy", value = 42))
Shiny.addCustomMessageHandler('fancyMessage', function({type, value}){ const numberPants = document.getElementById('number-of-pants') numberPants.textContent = value numberPants.classList.add(type)})
What's destructuring?
Shiny.setInputValue('fancyPants', 42)
Shiny.setInputValue('fancyPants', 42)
# inside observe({})input$fancyPants
## [1] 42
let nPants = document.getElementById('n-fancy-pants')nPants.addEventListener('click', function(event) { Shiny.setInputValue('fancyPants', event.target.value)})
# inside observe({})input$fancyPants
## [1] 42
Shiny won't resend values that don't change, unless...
Shiny.setInputValue('fancyPants', true, {priority: 'event'})
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
👨🏼💻
for a fancier htmlwidget
This is my last "fancier" I promise
One more slide to get us back in the head space of Frappe Charts
We want our chart to be able to receieve updates from Shiny
Re-render without re-drawing the whole plot.
We want our chart to be able to receieve updates from Shiny
Re-render without re-drawing the whole plot.
We want to send data back to Shiny about which element of the plot is currently selected
isNavigable === true
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 |
o | Tile View: Overview of Slides |
Esc | Back to slideshow |
Garrick Aden-Buie
rstudio::conf(2020, "JavaScript for Shiny Users")
Write your front end in raw web languages
Just like HTML, but in R
Use helpers to inline the CSS/JS
Let htmltools and Shiny manage dependencies
<!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))
<!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>
<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>'"))
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
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)
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
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
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.
$
is a valid variable name in JavaScript
var $ = jQuery
$
is a valid variable name in JavaScript
var $ = jQuery
_
is too and a few libraries take advantage of that (e.g. lodash, underscore)
$
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')
$
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')// orjQuery().on('click')
jQuery was way ahead of its time, but vanilla JavaScript caught up
jQuery
const $el = $('.shiny')
Vanilla
const el = document .querySelectorAll('.shiny')
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 //trueel.hide()// TypeError: el.hide is not a function
You Might Not Need jQuery
You Might Not Need jQuery
unless you do certain things
You Might Not Need jQuery
unless you do certain things
(and there's nothing wrong with using it)
You Might Not Need jQuery
unless you do certain things
(and there's nothing wrong with using it)
You Might Not Need jQuery
unless you do certain things
(and there's nothing wrong with using it)
Two goals:
show the very very basics of jQuery
When do you *need to use jQuery?
Vanilla
const el = document .querySelectorAll('.shiny')
jQuery
const $el = $('.shiny')
Vanilla
const el = document .querySelectorAll('.shiny')
jQuery
const $el = $('.shiny')
const el = document .getElementById('shiny')
const $el = $('#shiny')
Vanilla
const el = document .createElement('div')el.id = 'shiny'document.body.appendChild(el)
Vanilla
const el = document .createElement('div')el.id = 'shiny'document.body.appendChild(el)
jQuery
$('<div>') .setAttr('id', 'shiny') .appendTo('body')
Vanilla
const el = document .getElementById('shiny')el.classList.add('fancy') el.classList.remove('fancy')el.classList.toggle('fancy')
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')
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')
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
Vanilla
const els = document .querySelectorAll('.shiny')els.forEach( el => el.classList.add('fancy'))
jQuery
const $els = $('.shiny')$els.addClass('fancy')
Vanilla
const el = document.querySelectorAll('.shiny')el.addEventListener('click', ev => { // respond to event})
jQuery
const $el = $('.shiny')$el.on('click', ev => { // respond to event})
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})
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})
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})
Vanilla
document.addEventListener('DOMContentLoaded', function() { // Run this code when the DOM is good and ready})
Vanilla
document.addEventListener('DOMContentLoaded', function() { // Run this code when the DOM is good and ready})
jQuery
$(function() { // Whenever you're ready, browser.})
You can create your own events in Vanilla JavaScript and in jQuery
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
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
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 |
Event Name | When |
---|---|
shiny:outputinvalidated | Element will be updated |
Event Name | When |
---|---|
shiny:outputinvalidated | Element will be updated |
shiny:recalculating | Shiny is thinking about this element |
Event Name | When |
---|---|
shiny:outputinvalidated | Element will be updated |
shiny:recalculating | Shiny is thinking about this element |
shiny:recalculated | Shiny server is done thinking |
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 |
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 |
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 |
Event Name | When |
---|---|
shiny:inputchanged | The input changed(?) |
shiny:updateinput | Shiny updated the input |
Learn more: shiny.rstudio.com/articles/js-events.html
repl_example("shiny-events-1")
Find the #run
button
When the plot is recalculating: .on('shiny:recalculating')
.setAttribute()
to set disabled to trueThen undo the above steps when the output is ready: .on('shiny:value')
Note: you need to remove the disabled attribute
.removeAttribute('disabled')
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
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
repl_example("shiny-events-4")
Does this give you any ideas for your own apps?
shinyjs
disable()
, enable()
show()
, hide()
Questions about events?
session$sendCustomMessage("fancyMessage", data)
session$sendCustomMessage("fancyMessage", TRUE)
session$sendCustomMessage("fancyMessage", c(13, 21, 42))
session$sendCustomMessage("fancyMessage", list(type ="fancy", value = 42))
session$sendCustomMessage("fancyMessage", list(type ="fancy", value = 42))
jsonlite::toJSON(list(type = "fancy", value = 42))
## {"type":["fancy"],"value":[42]}
session$sendCustomMessage("fancyMessage", c(13, 21, 42))
jsonlite::toJSON(c(13, 21, 42))
## [13,21,42]
session$sendCustomMessage("fancyMessage", TRUE)
jsonlite::toJSON(TRUE, auto_unbox = TRUE)
## true
Then, we need to tell Shiny how to handle the message
Shiny.addCustomMessageHandler('fancyMessage', function(message) { // ... do things with the message ...})
You can define the function separately if you want
function fancyMessageHandler(message) { // ... do things with the message ...}Shiny.addCustomMessageHandler('fancyMessage', fancyMessageHandler)
And you can change the name of the argument
Shiny.addCustomMessageHandler('fancyMessage', function(x) { // ... do things with the x ...})
But your handler function needs one and only one argument
Shiny.addCustomMessageHandler('fancyMessage', function(x, y) { // Shiny will yell at you!})
Putting the two together, your message might conditionally trigger an action
session$sendCustomMessage("fancyMessage", TRUE)
Shiny.addCustomMessageHandler('fancyMessage', function(condition) { if (condition) { // show element } else { // hide element }})
Putting the two together, your message might update text
session$sendCustomMessage("fancyMessage", 42)
Shiny.addCustomMessageHandler('fancyMessage', function(value) { const numberPants = document.getElementById('number-of-pants') numberPants.textContent = value})
Putting the two together, your message might update several things
session$sendCustomMessage("fancyMessage", list(type ="fancy", value = 42))
Shiny.addCustomMessageHandler('fancyMessage', function(opts) { const numberPants = document.getElementById('number-of-pants') numberPants.textContent = opts.value numberPants.classList.add(opts.type)})
If your message is a list, destructuring is your friend
session$sendCustomMessage("fancyMessage", list(type ="fancy", value = 42))
What's destructuring?
If your message is a list, destructuring is your friend
session$sendCustomMessage("fancyMessage", list(type ="fancy", value = 42))
Shiny.addCustomMessageHandler('fancyMessage', function({type, value}){ const numberPants = document.getElementById('number-of-pants') numberPants.textContent = value numberPants.classList.add(type)})
What's destructuring?
Shiny.setInputValue('fancyPants', 42)
Shiny.setInputValue('fancyPants', 42)
# inside observe({})input$fancyPants
## [1] 42
let nPants = document.getElementById('n-fancy-pants')nPants.addEventListener('click', function(event) { Shiny.setInputValue('fancyPants', event.target.value)})
# inside observe({})input$fancyPants
## [1] 42
Shiny won't resend values that don't change, unless...
Shiny.setInputValue('fancyPants', true, {priority: 'event'})
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
👨🏼💻
for a fancier htmlwidget
This is my last "fancier" I promise
One more slide to get us back in the head space of Frappe Charts
We want our chart to be able to receieve updates from Shiny
Re-render without re-drawing the whole plot.
We want our chart to be able to receieve updates from Shiny
Re-render without re-drawing the whole plot.
We want to send data back to Shiny about which element of the plot is currently selected
isNavigable === true