The cookies package logo, a chocolate chip cookie in a hexagon with the word cookies. Baking JavaScript into a Shiny Package

Why JavaScript?

Shiny is JavaScript.

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <script type="application/shiny-singletons"></script>
  <script type="application/html-dependencies">jquery[3.6.0];shiny-css[1.7.4];shiny-javascript[1.7.4];bootstrap[3.4.1]</script>
  <script src="jquery-3.6.0/jquery.min.js"></script>
  <link href="shiny-css-1.7.4/shiny.min.css" rel="stylesheet" />
  <script src="shiny-javascript-1.7.4/shiny.min.js"></script>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link href="bootstrap-3.4.1/css/bootstrap.min.css" rel="stylesheet" />
  <link href="bootstrap-3.4.1/accessibility/css/bootstrap-accessibility.min.css" rel="stylesheet" />
  <script src="bootstrap-3.4.1/js/bootstrap.min.js"></script>
  <script src="bootstrap-3.4.1/accessibility/js/bootstrap-accessibility.min.js"></script>  
  <title>Super Simple</title>
</head>

JavaScript is the Web

A plot of language popularity over time for 2014-2022. JavaScript is always #1.

source: GitHub

JavaScript and R are friends!

  • JavaScript for R by John Coene
  • “Creating a JavaScript Library for your Shiny Application” by Ashley Baldry
  • Numerous talks and workshops

Why a package?

Copy/paste is dangerous

“Copy-and-paste is a powerful tool, but you should avoid doing it more than twice.” — Hadley Wickham, Mine Çetinkaya-Rundel, and/or Garrett Grolemund, R for Data Science (2e)

Reusing packages is easy

Before

dir.create("./www")
# Don't forget to copy the shiny-specific js to www/script.js!

library(shiny)
addResourcePath("www", "www")

ui <- fluidPage(
  tags$head(
    tags$script(
      src = "https://cdn.jsdelivr.net/npm/js-cookie/dist/js.cookie.min.js"
    ),
    tags$script("www/script.js")
  ),
  # The rest of my UI goes here!
)

Reusing packages is easy

After

ui <- cookies::add_cookie_handlers(ui)

A Recipe for JavaScript-containing R packages

An index card with a hand-written recipe for the cookies package: 4 license caveats, 2 description rules, 1 inst subfolder, htmlDependency()s to taste, 2 way communications. Combine ingredients into a standard R package. Bake until golden.

But first…

The R4DS Online Learning Community

R4DS shiny apps

A screenshot of the R4DS Mentor Tool, a Shiny dashboard for finding unanswered questions on the R4DS Online Learning Community.

A screenshot of the R4DS Book Club Planner, a Shiny dashboard for indicating interest in book clubs on the R4DS Online Learning Community.

Mentordash

A screenshot of the R4DS Mentor Tool, a Shiny dashboard for finding unanswered questions on the R4DS Online Learning Community.

Bookclubber

A screenshot of the R4DS Book Club Planner, a Shiny dashboard for indicating interest in book clubs on the R4DS Online Learning Community.

Sign in with Slack

The sign in with Slack button

{shinyslack}

The shinyslack package logo, with the slack logo in neon.


{cookies}

cookies package logo, a chocolate chip cookie in a hexagon with the word cookies.


The recipe!

An index card with a hand-written recipe for the cookies package: 2 license caveats, 3 description rules, 1 inst subfolder, htmlDependency()s to taste, 2 way communications. Combine ingredients into a standard R package. Bake until golden.

LICENSE

  • Check JavaScript library’s license
  • Use a compatible license for your package
  • Add LICENSE.note file to clarify JavaScript license(s)
  • See “Licensing” chapter of R Packages (2e)
  • See DESCRIPTION and inst sections

DESCRIPTION

  • Add JavaScript library authors to Authors@R
    • role = cph
    • comment = "'js-cookie' JavaScript library"
  • Describe the relationship in Description
    • Enclose library mentions in ''
      • ...the 'js-cookie' JavaScript library...
    • Enclose URLs in <>
      • <https://github.com/js-cookie/js-cookie>

inst

  • Add inst folder
  • Add js subfolder
    • Add copies of the .js files here
    • Also add your own .js files here (& minify!)
    • If licenses are different, add full text here

htmltools::htmlDependency()

  • Adds each referenced file exactly once
  • Load from npm source when available (fast)
  • Load package version otherwise

Your dependencies

cookie_dependency <- function() {
  return(
    list(
      htmltools::htmlDependency(
        name = "js-cookie",
        version = "3.0.1",
        src = c(
          href = "https://cdn.jsdelivr.net/npm/js-cookie/dist/",
          file = "js"
        ),
        package = "cookies",
        script = "js.cookie.min.js"
      ),
      htmltools::htmlDependency(
        name = "cookie_input",
        version = "1.0.0",
        src = "js",
        package = "cookies",
        script = "cookie_input.js"
      )
    )
  )
}

Talking to JavaScript

R (server) to JavaScript (ui)

  • session$sendCustomMessage(type, message) to call JavaScript functions
  • Shiny.addCustomMessageHandler(type, function(message) {}); on JavaScript side to receive the message

R (server) to Javascript (ui) example

session$sendCustomMessage(
  "cookie-set",
  list(
    name = cookie_name,
    value = cookie_value,
    attributes = attributes
  )
)

Shiny.addCustomMessageHandler('cookie-set', function(msg){
  try {
    Cookies.set(msg.name, msg.value, msg.attributes);
    let cookie = Cookies.get(msg.name);
    if (cookie === undefined) {
      throw "Failed to set cookie '" + msg.name + "'.";
    }
    getCookies();
  } catch (error) {
    Shiny.setInputValue("cookie_set_error", error);
    console.log(error);
  }
});

JavaScript (ui) to R (server)

  • Shiny.setInputValue(id{:type}, value) to pass things to input$id
  • Optional: .onLoad and shiny::registerInputHandler(type, fun)
  • Shiny events like shiny:connected

JavaScript (ui) to R (server) example

function getCookies(){
  var res = Cookies.get();
  Shiny.setInputValue('cookies', res);
}

$(document).on('shiny:connected', function(ev){
  let jsCookiesStart = Cookies.get();
  Shiny.setInputValue('cookies_start', jsCookiesStart);
  getCookies();
});

window.addEventListener('focus', function() {
  getCookies();
});

Module considerations

Make sure your code works with modules!

.root_session <- function(session = shiny::getDefaultReactiveDomain()) {
  # Some hardening inspired by unexported find_ancestor_session() in shiny.
  depth <- 20L
  while (inherits(session, "session_proxy") && depth > 0) {
    session <- .subset2(session, "parent")
    depth <- depth - 1L
  }
  if (inherits(session, "ShinySession")) {
    return(session)
  } else {
    stop("Root session not found.")
  }
}

Conclusion

  • The code you want may already exist in JavaScript
  • JavaScript libraries are more reusable in Shiny as R Packages
  • Follow my recipe to bake JavaScript into R
  • Further reading:

Find me!