22 Introduction to {charpente}

The {charpente} package

FIGURE 22.1: The {charpente} package

22.1 Motivations

Translating an HTML template into an R API requires the creation of a package. As stated in Chapter ??, this is not a good practice to proceed as follows:

ui <- fluidPage(
  useShinydashboard(),
  tags$script(
    [1172 chars quoted with '"']
  ),
  
  box2(
    title = textOutput("box_state"),
    "Box body",
    inputId = "mybox",
    collapsible = TRUE,
    plotOutput("plot")
  ),
  actionButton("toggle_box", "Toggle Box")
)

server <- function(input, output, session) {
  output$plot <- renderPlot({
      req(!input$mybox$collapsed)
      plot(rnorm(200))
    })
    
    output$box_state <- renderText({
      state <- if (input$mybox$collapsed) "collapsed" else "uncollapsed"
      paste("My box is", state)
    })
    
    observeEvent(input$toggle_box, {
      updateBox2("mybox")
    })
}

shinyApp(ui, server)

Imagine if we had to repeat the process for more than twenty components. The package structure provides many advantages like:

  • Develop a comprehensive documentation of the underlying API
  • Design unit tests to guaranty code robustness and improve long term plan
  • Relevant file organization

22.2 General idea

{charpente} is a game changer for custom template creation. It is not yet on CRAN but has been widely used to help developing RinteRface packages. charpente drastically:

  • Eases the import of external dependencies.
  • Speeds up the HTML to R conversion, which is quite frankly a rather boring process, allowing to focus on the features rather than the syntax. This feature builds on top of the {html2R} shiny app by Alan Dipert, already mentioned in Chapter 2.
  • Eases JS code management, leveraging esbuild.

Let’s try below with html_2_R:

library(charpente)
html_2_R('<div class="divclass" id = "someid"></div>')
## 
## ── Converting code ... ──
## 
## ✓ Code converted with success.
## ℹ Copy and paste the following R code
## ────────────────────────────────────────────────────────────────────────────────
## tags$div(
##   class = "divclass",
##   id = "someid"
## )

html_2_R has a prefix parameter which adds a tags$ prefix if TRUE. It is TRUE by default which prevents errors with non exported Shiny tags like nav.

The second main benefice of charpente is the dependency management system.

22.3 A case study: {shinybulma}

In the following, we’ll illustrate charpente’s workflow, through the R in Pharma workshop exercises focused on shinybulma.

bulma is a more and more popular open source CSS framework for the web. Importantly, there isn’t any JavaScript helper in the bulma core. We’ll see later that the recent bulma JS provides such feature. For now, we only focus on HTML and CSS.

To initialize a charpente package, we do:

path <- file.path(tempdir(), "mypkg")
create_charpente(path, license = "mit")

This sets up a minimal viable package with git and optionally github remote setup, Figure 22.2.

Package structure for {charpente}

FIGURE 22.2: Package structure for {charpente}

By default, the package DESCRIPTION Imports field has shiny, htmltools and utils. charpente is never required to be a dependency of your package since it might be invasive.

In the ./R folder, charpente creates a mypkg-utils.R script containing:

  • Tools to facilitate HTML dependency management like add_dependencies (see corresponding section below), processDeps.
  • Some validation functions mentioned in Chapter 21.

Finally, you may see some exotic folders and files like srcjs, package.json, package-lock.json and node_modules. Fear not, we describe them later in 22.3.6. Overall, they are here to support to JS code management.

22.3.1 Build the HTML dependency

The interested reader may have a look at the Getting started guide, so as to know more how to get bulma. To install bulma dependencies, there are several ways:

  • The CDN method (content delivery network) which consists in getting dependencies from a dedicated server. Files are not stored locally which may be a problem if one does not have internet.
  • The local method consists in downloading the production files (minified CSS).
  • Using npm that installs bulma sources as well as production files. It means one can modify sources at anytime, which is not recommended since it would be hard to maintain.

In our case, we show the two first methods, the third being out of the scope of this book.

As shown in previous Chapter (custom-templates-dependencies), we could build the bulma dependency as follows:

library(htmltools)
bulma_deps <- htmlDependency(
  name = ...,
  version = ...,
  src = c(href = ...),
  stylesheet = ...
)

add_bulma_deps <- function(tag) {
  tagList(..., ...)
}

but this already takes too much time. This is where charpente comes into play. Specifically, the create_dependency function automatically download/points to the specified dependency by just providing its name. It means you have to know what you are looking for. Best practice is to have a look at the jsdelivr website (charpente is built on top of jsdelivr) and find the good repository, as shown Figure 22.3. create_dependency also creates the add_<DEP_NAME>_deps function in a <DEP_NAME>--dependencies.R script and opens it.

charpente_options(local = FALSE) allows to fine tune the behavior. If local is FALSE, charpente points to the CDN without downloading any file. It is substantially faster than the local option but requires an internet connection. Package developers will prefer the local = TRUE to ensure dependencies are always accessible. Extra parameters like tag control the downloaded version since HTML templates may have several flavors. It is always good to be able to test multiple versions and select the best option.

jsdelivr result for bulma

FIGURE 22.3: jsdelivr result for bulma

Once satisfied, we simply run the below code to get the latest version, or a specific version if tag is used:

# CDN method 
create_dependency("bulma", options = charpente_options(local = FALSE))
create_dependency("bulma", tag = "0.7.0", options = charpente_options(local = FALSE))

# local method (default)
create_dependency("bulma")

Moreover, create_dependency is able to filter all files, through the charpente_options:

  • minified targets all files with .min, if TRUE.
  • bundle targets all files containing .bundle, if TRUE.
  • lite targets files with lite keyword, if TRUE.
  • rtl target all files with .rtl, if TRUE. rtl design stands for right to left and is common in some countries for instance.

You may imagine that charpente_options targets .min files by default. If you don’t find any script, you probably have to change options. For instance, some templates like Bootstrap and Framework7 have bundle.min files (charpente_options(bunlde = TRUE)), whereas bulma doesn’t.

We can test our new dependency:

devtools::load_all()
findDependencies(add_bulma_deps(div()))

which works like a charm. If you chose the local option, you also get an inst/bulma-<BULMA-VERSION> folder with all relevant files sorted by type. The bulma-dependencies.R script contains the newly created add_bulma_deps function, either pointing to the CDN or the local files, depending on the chosen strategy:

# local dependency script output

#' bulma dependencies utils
#'
#' @description This function attaches bulma. dependencies to the given tag
#'
#' @param tag Element to attach the dependencies.
#'
#' @importFrom htmltools tagList htmlDependency
#' @export
add_bulma_deps <- function(tag) {
 bulma_deps <- htmlDependency(
  name = "bulma",
  version = "0.9.1",
  src = c(file = "bulma-0.9.1"),
  stylesheet = "css/bulma.min.css",
  package = "mypkg",
 )
 tagList(tag, bulma_deps)
}

# CDN dependencies

#' bulma dependencies utils
#'
#' @description This function attaches bulma. dependencies to the given tag
#'
#' @param tag Element to attach the dependencies.
#'
#' @importFrom htmltools tagList htmlDependency
#' @export
add_bulma_deps <- function(tag) {
 bulma_deps <- htmlDependency(
  name = "bulma",
  version = "0.9.1",
  src = c(href = "https://cdn.jsdelivr.net/npm/bulma@0.9.1/"),
  stylesheet = "css/bulma.min.css"
 )
 tagList(tag, bulma_deps)
}

charpente sets the roxygen skeleton so that you don’t have to worry about function imports.

22.3.2 Set up the minimal page template

According to the bulma documentation, the starter page template is:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Hello Bulma!</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css">
  </head>
  <body>
    <section class="section">
      <div class="container">
        <h1 class="title">
          Hello World
        </h1>
        <p class="subtitle">
          My first website with <strong>Bulma</strong>!
        </p>
      </div>
    </section>
  </body>
</html>

Adding some charpente magic with html_2_R, we set the path parameter to /html to get the entire template. We, replace ... by the appropriate content (see above). Since the copied HTML contains double quotations marks like <p class="subtitle"></p>, we put the string in single quotation marks.

html_2_R(
  '<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Hello Bulma!</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css">
  </head>
  <body>
    <section class="section">
      <div class="container">
        <h1 class="title">
          Hello World
        </h1>
        <p class="subtitle">
          My first website with <strong>Bulma</strong>!
        </p>
      </div>
    </section>
  </body>
</html>
  ',
  path = "/html"
)
## 
## ── Converting code ... ──
## 
## ✓ Code converted with success.
## ℹ Copy and paste the following R code
## ────────────────────────────────────────────────────────────────────────────────
## tags$html(
##   tags$head(
##     tags$meta(charset = "utf-8"),
##     tags$meta(
##       name = "viewport",
##       content = "width=device-width, initial-scale=1"
##     ),
##     tags$title("Hello Bulma!"),
##     tags$link(
##       rel = "stylesheet",
##       href = "https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css"
##     )
##   ),
##   tags$body(tags$section(
##     class = "section",
##     tags$div(
##       class = "container",
##       tags$h1(
##         class = "title",
##         "Hello World"
##       ),
##       tags$p(
##         class = "subtitle",
##         "My first website with",
##         tags$strong("Bulma"),
##         "!"
##       )
##     )
##   ))
## )

At run time, shiny adds html around the UI, thereby making it not necessary to include. We also don’t need the link(rel = "stylesheet", href = "https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css") since add_bulma_deps does already attach the dependencies to the page.

The prefix parameter defaults to TRUE, so that we don’t have to worry about whether tags functions are exported by shiny (see Chapter 2).

The bulma_page function is defined below, that we save in the R package folder:

bulma_page <- function(..., title = NULL) {
  tagList(
    tags$head(
      tags$meta(charset = "utf-8"),
      tags$meta(
        name = "viewport",
        content = "width=device-width, initial-scale=1"
      ),
      tags$title(title)
    ),
    add_bulma_deps(tags$body(...)) 
  )
}

With some practice, going from step one to the bulma page templates literally takes three minutes, while it would have taken more than 30 minutes by hand. At any time, you may replace the dependency with another version. Be careful, since charpente does not make snapshots of old versions.

22.3.3 Exercise: add bulmaJS

As stated in the above, bulma only contains CSS things! It means we need either to develop custom JS code to add interactivity or rely on any third party existing API. bulma JS is one of these!

  1. Using charpente, create a bulma js dependency. We point to vizuaalog/bulmajs since some bulmajs already exist and are not what we want. Run the following code in the R console.

Notice how many files are added to the dependency. Below, we only work with notifications:

  • Only keep notification.js and remove all the unnecessary files
  • Only keep bulma.js that gather all plugins in 1 script.

The best practice is usually to keep only what we need since some scripts may be heavy to load!

  1. Run devtools::load_all(). Modify the below code to test the newly created dependency. Hint: toggle the HTML inspector to check all appropriate dependencies are there!
ui <- bulma_page()
server <- function(input, output, session) {}
shinyApp(ui, server)

22.3.4 Add custom JS

Notifications are always useful to send user feedback. Shiny has a notification system through shiny::showNotification. Like Shiny, Bulma notifications are entirely built from JS (no need for any HTML code).

The API works as follows:

  • Bulma(target).notification(config) creates the notification based on a JSON option list (config). target expects a jQuery selector.
  • show toggles the newly instantiated notification

In other words the below code attaches the notification to the body:

Bulma('body').notification({
  body: 'Example notification',
  color: 'info'
}).show();

In the following we design the R interface and JavaScript handler (which is no more than an event listener). charpente has a function that creates both pieces, namely create_custom_handler:

create_custom_handler("notification")

We obtain the notification-handler.R script:

send_notification_message <- function(id = NULL, options = NULL, session = shiny::getDefaultReactiveDomain()) {
 message <- list(
  # your logic
 )
 
 session$sendCustomMessage(type = "notification", message)
}

and the corresponding JavaScript piece in notification.js, derived from the golem add_js_handler function:

$(function() {
  Shiny.addCustomMessageHandler('notification', function(message) {
 
  });
});

By default, the JS file is created in the srcjs directory. This is a special directory where we store all JavaScript files that depend on the package author. For instance, bulmaJS is an external dependency and is very unlikely to be edited by the package author. For that reason, it remains in the inst folder like all other external dependencies.

22.3.5 Add custom input/output bindings

In part 12.2, we created better shinydashboard boxes that one may programmatically collapse, close and restore. Until know, there was no way to setup an input binding skeleton and one had to copy and paste each time the same code. charpente has a create_input_binding and create_output_binding (functions that you can also find in the development version of golem). Contrary to the custom handler case, create_input_binding only generate the JavaScript piece since the R part is highly variable from one input to another. To get a plug and play box input binding we call:

create_input_binding("boxBinding")

which gives the input-boxBinding.js script in the srcjs folder:

var boxBinding = new Shiny.InputBinding();
$.extend(boxBinding, {
  find: function(scope) {
    // JS logic $(scope).find('whatever')
  },
  getValue: function(el) {
    // JS code to get value
  },
  setValue: function(el, value) {
    // JS code to set value
  },
  receiveMessage: function(el, data) {
    // this.setValue(el, data);
  },
  subscribe: function(el, callback) {
    $(el).on('click.boxBinding', function(e) {
      callback();
    });

  },
  unsubscribe: function(el) {
    $(el).off('.boxBinding');
  }
});
Shiny.inputBindings.register(boxBinding, 'shiny.whatever');

This function has multiple options:

  • initialized is FALSE by default. If TRUE, it adds an initialized method to the binding.
  • dev adds some console.log elements whenever relevant to help in the debugging process.
  • event is a list containing events related to the binding. By default, it generates a click event without any rate policy. To add extra events we do list(name = c("click", "whatever"), rate_policy = c(FALSE, TRUE)).

Similarly, the create_output_binding function creates a ready to use output binding JS script, in the srcjs folder (create_output_binding("menuOutput")):

var menuOutput = new Shiny.OutputBinding();
$.extend(menuOutput, {
  find: function(scope) {
    // JS logic $(scope).find('whatever')
  },
  renderValue: function(el, data) {
    // JS logic
  }
});
Shiny.outputBindings.register(menuOutput, 'shiny.whatever');

22.3.6 Organize your JS code

This naturally leads us to this part which is about JS code organization. Shiny developers may have a lot of custom JS scripts, and it is generally a bad idea to put them all under inst. Instead, we store them in srcjs. charpente has a function providing a tool to bundle the JS code for production or development, that is build_js():

  • It compresses, mangles all JS files and concatenate them in one minified file called mypkg.min.js. If mode is dev, the files are not minified.
  • In production mode (mode is prod, by default), it additionally generates source maps.

esbuild concatenates file by the order provided in the ./srcjs/main.js entry point, automatically generated by create_charpente(). The configuration is provided by charpente in the package.json file.

The script mypkg.min.js is not human readable but the generated source map allows to reconstruct the original code, which location is under the web browser srcjs folder, like all Shiny JS files. From there, we can access any mapped script and start the debugging process like setting break points.

In production, the variable names, functions, are mangled. For instance, a variable config could be called t in the minified file, which may lead to confusion.

Additionally, build_js creates the mypkg-dependencies.R file containing the HTML dependency pointing to the newly generated JS file (below for production):

#' mypkg dependencies utils
#'
#' @description This function attaches mypkg dependencies to the given tag
#'
#' @param tag Element to attach the dependencies.
#'
#' @importFrom utils packageVersion
#' @importFrom htmltools tagList htmlDependency
#' @export
add_mypkg_deps <- function(tag) {
 mypkg_deps <- htmlDependency(
  name = "mypkg",
  version = packageVersion("mypkg"),
  src = c(file = "mypkg-0.0.0.9000"),
  script = "js/mypkg.min.js",
  package = "mypkg",
 )
 tagList(tag, mypkg_deps)
}

Switching between prod and dev automatically updates the mypkg-dependencies.R JS files.

Finally, under the hood, create_js(), create_input_binding(), create_output_binding() and create_custom_handler() add a reference to the newly created script in the main.js entry point, which may look like:

// Gather all files to import here
import './init.js'
import './widgets.js'
import './test.js'

export and import must be called from the top level of a script. For instance, they cannot live inside the $( document ).ready(function(...)});, as this would trigger a build error.

charpente currently does not provide similar process for CSS, as this is still work in progress. Other tools exist like {packer} by John Coene, which leverages webpack to handle JS code.

22.3.7 Combine multiple dependencies

add_dependencies allows to select any dependency available in the ./R folder, provided that they follow the convention <depName>_dependencies.R (which is always the case if you use charpente):

#' Attach all created dependencies in the ./R directory to the provided tag
#'
#' This function only works if there are existing dependencies. Otherwise,
#' an error is raised.
#'
#' @param tag Tag to attach the dependencies.
#' @param deps Dependencies to add. Expect a vector of names. If NULL, all dependencies
#' are added.
#' @export
#'
#' @examples
#' \dontrun{
#'  library(htmltools)
#'  findDependencies(add_dependencies(div()))
#'  findDependencies(add_dependencies(div(), deps = "bulma"))
#' }
add_dependencies <- function(tag, deps = NULL) {
  if (is.null(deps)) {
    temp_names <- list.files("./R", pattern = "dependencies.R$")
    deps <- unlist(lapply(temp_names, strsplit, split = "-dependencies.R"))
  }

  if (length(deps) == 0) stop("No dependencies found.")

  deps <- lapply(deps, function(x) {
    temp <- eval(
      parse(
        text = sprintf("htmltools::findDependencies(add_%s_deps(htmltools::div()))", x)
      )
    )
    # this assumes all add_*_deps function only add 1 dependency
    temp[[1]]
  })

  htmltools::tagList(tag, deps)
}

For instance add_dependencies(div(), deps = c("bulma", "bulmajs")) adds bulma (first) and bulmajs dependencies to a div tag. You may change the order as you want since most of the time, the order matters. We update bulma_page to benefit from that feature:

bulma_page <- function(..., title = NULL) {
  tagList(
    tags$head(
      tags$meta(charset = "utf-8"),
      tags$meta(
        name = "viewport",
        content = "width=device-width, initial-scale=1"
      ),
      tags$title(title)
    ),
    add_dependencies(
      tags$body(...),
      deps = c("bulma", "mypkg")
    ) 
  )
}

As mentioned above, add_dependencies belongs to the mypkg-utils.R script so that you don’t have to import charpente in the DESCRIPTION Imports field.

22.3.8 Other {charpente} helpers

Let’s finish this section by listing other useful charpente tools. We know create_dependency to install an external dependency. As shown earlier, this code installs bulma dependencies:

However, we don’t necessarily know all package versions and may need bulma 0.9.1 or bulma 0.7.0. get_dependency_versions allows to look for all existing versions:

## ℹ Trying with https://data.jsdelivr.com/v1/package/npm/bulma
## ✓ Success!
## ────────────────────────────────────────────────────────────────────────────────
##  [1] "0.9.2"  "0.9.1"  "0.9.0"  "0.8.2"  "0.8.1"  "0.8.0"  "0.7.5"  "0.7.4" 
##  [9] "0.7.3"  "0.7.2"  "0.7.1"  "0.7.0"  "0.6.2"  "0.6.1"  "0.6.0"  "0.5.3" 
## [17] "0.5.2"  "0.5.1"  "0.5.0"  "0.4.4"  "0.4.3"  "0.4.2"  "0.4.1"  "0.4.0" 
## [25] "0.3.2"  "0.3.1"  "0.3.0"  "0.2.3"  "0.2.1"  "0.2.0"  "0.1.2"  "0.1.1" 
## [33] "0.1.0"  "0.0.28" "0.0.27" "0.0.26" "0.0.25" "0.0.24" "0.0.23" "0.0.22"
## [41] "0.0.21" "0.0.20" "0.0.19" "0.0.18" "0.0.17" "0.0.16" "0.0.15" "0.0.14"
## [49] "0.0.13" "0.0.12" "0.0.11" "0.0.10" "0.0.9"  "0.0.8"  "0.0.7"  "0.0.6" 
## [57] "0.0.5"  "0.0.4"
get_dependency_versions("bulma", latest = TRUE)
## ℹ Trying with https://data.jsdelivr.com/v1/package/npm/bulma
## ✓ Success!
## ────────────────────────────────────────────────────────────────────────────────
## [1] "0.9.2"

Specifying latest = TRUE ensures to recover the very last stable version (it excludes alpha/beta versions).

You may explore also the dependency files with get_dependency_assets, even for a specific version with tag:

## ℹ Trying with https://data.jsdelivr.com/v1/package/npm/bulma
## ✓ Success!
## ────────────────────────────────────────────────────────────────────────────────
## $url
## [1] "https://cdn.jsdelivr.net/npm/bulma@0.9.2/"
## 
## $files
##                name                                         hash
## 1         bulma.css yImvCCH14bBb1CP+8UKH4Kq6Hd77ErtYaM4V+QSvPwc=
## 2     bulma.css.map i9ZST1s/+LQDGevUi1OlRnePnesjoYhW5PJvsUWDqok=
## 3     bulma.min.css O8SsQwDg1R10WnKJNyYgd9J3rlom+YSVcGbEF5RmfFk=
## 4     bulma-rtl.css xFqhlC3rz3OH0JlaGEBeiKIB2at5/ragR+ldLLYZzQU=
## 5 bulma-rtl.css.map Jd1QERJuvEGxAD1EPmyOdUOL7Omn2gaDx6KSgqTaIEI=
## 6 bulma-rtl.min.css OhJCKE0+noWSdE9YUEx+d+3/hdatRbf8nfFQGFCBVhU=
## 
## $hasSubfolders
## [1] TRUE

This is helpful to further fine tune charpente_options, as stated previously. It is indeed possible that you don’t want bundles, minified, lite or rtl versions of scripts. Internally, create_dependency relies on get_dependency_assets.

get_installed_dependency allows to inspect which dependencies are installed. It only works if the dependencies were created locally, that is charpente_options(local = TRUE).

Finally, one may ask how to update a given dependency. update_dependency does this, provided that the dependency is installed locally. By default, it installs the latest version of the targeted dependency. It gives a diagnosis comparing the current installed version with the latest available version. The are 3 possible cases: dependencies are up to date and update_dependency("bulma") yields:

ℹ Trying with https://data.jsdelivr.com/v1/package/npm/bulma
✓ Success!
──────────────────────────────────────────────────────────────────────────────
Error in update_dependency("bulma") : Versions are identical

The installed dependencies are outdated (we have 0.7.0 with create_dependency("bulma", tag = "0.7.0", options = charpente_options(local = TRUE))), the function shows the targeted version as well as the last one:

ℹ Trying with https://data.jsdelivr.com/v1/package/npm/bulma
✓ Success!
──────────────────────────────────────────────────────────────────────────────
ℹ current version: 0.7.0 ||
target version: 0.9.1 ||
latest version: 0.9.1
! Upgrading bulma to 0.9.1
✓ Directory inst/bulma-0.9.1/css successfully created
! Remove existing file R/bulma-dependencies.R

The last use case is a downgrade, which may be possible if the package maintainer realizes that the dependency version is too unstable. In the following, we have bulma-0.9.1 installed and downgrade to 0.7.0 with update_dependency("bulma", version_target = "0.7.0"):

ℹ Trying with https://data.jsdelivr.com/v1/package/npm/bulma
✓ Success!
──────────────────────────────────────────────────────────────────────────────
ℹ current version: 0.9.1 ||
target version: 0.7.0 ||
latest version: 0.9.1
! Downgrading bulma to 0.7.0
✓ Directory inst/bulma-0.7.0/css successfully created
! Remove existing file R/bulma-dependencies.R

22.4 Other tips

22.4.1 Validate JavaScript

We could not finish this chapter without mentioning tools to validate JavaScript code. JSHint, which comes with {jstools}. Below is an example of how to check all the shinyMobile JavaScript input bindings at once:

shinyMobileJS <- system.file(
  sprintf(
    "shinyMobile-%s/js/shinyMobile.js", 
    packageVersion("shinyMobile")
  ), 
  package = "shinyMobile"
)
# jstools print messages to the console
# We don't want to see them all in the book ...
invisible(capture.output(
  temp <- jshint_file(
    input = shinyMobileJS, 
    options = jshint_options(
      jquery = TRUE, 
      globals = list("Shiny", "app")
    )
  )
))

head(tibble::as_tibble(temp$errors[, c("line", "reason")]))
## # A tibble: 6 x 2
##    line reason                                                                  
##   <int> <chr>                                                                   
## 1    34 'template literal syntax' is only available in ES6 (use 'esversion: 6').
## 2    39 'template literal syntax' is only available in ES6 (use 'esversion: 6').
## 3    43 'template literal syntax' is only available in ES6 (use 'esversion: 6').
## 4    44 'template literal syntax' is only available in ES6 (use 'esversion: 6').
## 5   123 'const' is available in ES6 (use 'esversion: 6') or Mozilla JS extensio…
## 6   124 'const' is available in ES6 (use 'esversion: 6') or Mozilla JS extensio…

You may fine tune the jshint_file behavior with the jshint_options. One is often tempted to call eval in JS code, which will result in a JSHint error. An option called evil exists to disable the corresponding test. However, we recommend to play the game, accept those error and try to fix them instead of cheating with options! An important remark about validation is that it does not check whether your code does what it should do. It just focus on checking whether the code runs! To test the JavaScript behavior, please refer to Chapter 21.2.1.2. Be extremely careful: if we consider the example mentioned in section 10.6.2, the following code is valid JavaScript:

const sendNotif = (message, type, duration) => {
  Shiny.notification.show({
    html: `<strong>${message}</strong>`,
    type: type,
    duration: duration
  });
};

sendNotif('Hello')

and will pass the validation step without any error:

jshint(
  "const sendNotif = (message, type, duration) => {
  Shiny.notification.show({
    html: `<strong>${message}</strong>`,
    type: type,
    duration: duration
  });
  };
  sendNotif('Hello');
  ",
  options = jshint_options(
    esversion = 6,
    jquery = TRUE, 
    globals = list("Shiny", "app")
  )
)$errors
## NULL

Yet the code won’t work since Shiny.notification does not exist.

22.4.2 Beautify JS code

If you work with the RStudio IDE, your JS code maybe sometimes messy with bad indentation. jstools also provides a function and addin to fix the problem. prettier_js(code) and prettier_file(input = "path/to/file.js", output = "path/to/reformated.js") does this. I often use the Prettier addin which is way faster than typing the function call (Figure 22.4).

Better JS formatting

FIGURE 22.4: Better JS formatting

22.4.3 Test JS code

charpente provides a starting point to test the JS code with test_js(), by leveraging the mocha library. All tests are assumed to be contained within the srcjs/test folder. Inside, we find test_basic.js, which was created upon package initialization, as a boilerplate:

describe('Basic test', () => {
  it('should not fail', (done) => {
    done();
  });
});

This test starts with a describe function, similar to the testthat context() function, where you provide the general idea behind the test. it is equivalent to test_that(), where we describe what specific feature is being tested. Inside, we write a series of instructions, some of them failing, others passing. Naturally, mocha works better with other assertions libraries like expect.js or should.js, whose details are out of the scope of this book.