Creating a JavaScript Library for your Shiny Application

Ashley Baldry

Shiny & JavaScript

  • {shiny} + JS = ❤️
  • tags$script(HTML("
        $(document).ready(function() {
            $('[data-toggle="tooltip"]').tooltip()
        })
    "))
  • tags$script(src="custom.js")
  • Shiny.inputBindings.register(inputBinding, "your.inputBinding")
  • 🤷

Motivation

Demo of the {designer} application

Motivation

Structure of the JavaScript code in the {designer} package for version 0.1.0

  • Adding more components would make the code harder to read
  • Lots of duplicated code that could be simplified

JavaScript Library

Library Set-Up

  • Choose your IDE of choice
  • Create a directory in your project for JS code
    • Standard name: srcjs
    • If included in an R package, add folder to .Rbuildignore
  • Download and install Node.js
  • Open srcjs directory up in your IDE
  • Run npm init in the terminal and initialise your project

Library Set-Up

Running npm init in VS Code

Dependency Management

  • Node.js includes a package manager, npm, within its installation
    • This works like {renv}, including a lock file within the project
  • Include shiny and jQuery as dependencies
    • npm install github:rstudio/shiny
    • npm install @types/jquery@3.5.14
  • Project specific dependencies can also be added here

Important

Create a .gitignore in your JavaScript directory and include node_modules

Dependency Management

JS library structure after installing required dependencies

Bundling/Minifying Code

import { build } from 'esbuild'

build({
  entryPoints: ['index.js'],
  bundle: true,
  sourcemap: true,
  outfile: '../inst/app/www/designer.min.js',
  platform: 'node',
  minify: true
}).catch(
  () => process.exit(1)
)
  • npm install esbuild --save-dev
    • --save-dev only includes package for development purposes
  • Include a source map to help debug errors in console

Bundling/Minifying Code

JS library structure after installing bundling library

Linting

  • Linting helps improve code quality and consistency
  • npm install eslint --save-dev
    • Extension in VS Code that applies the eslint linting standards
    • Customisable by adding own rules into .eslintrc.yml

Linting

JS library structure after installing linting

Communication Between Files

  • JavaScript files work in a modular fashion
  • Only exported objects are accessible in other modules
  • Objects have to be explicitly imported before use
  • Add "type": "module" to package.json to enable import to work
// component/component.js
export class Component {
  html = '<div></div>'
  constructor () {
    // runs when class is created
  }
  
  createComponent () {
    return this.html
  }
}

// component/button.js
import {Component} from './Component'

class Button extends Component {
  html = '<button ...>...</button>'
  constructor () {
    // runs Component constructor
    super()
  }
}

Class Inheritance

  • A way to reduce duplicated code
  • Extremely useful when creating 20+ components
    • Component class contains several methods accessible to individual components
    • Overwrite methods when needed
// component/component.js
export class Component {
  html = '<div></div>'
  constructor () {
    // runs when class is created
  }
  
  createComponent () {
    return this.html
  }
}

// component/button.js
import {Component} from './Component'

class Button extends Component {
  html = '<button ...>...</button>'
  constructor () {
    // runs Component constructor
    super()
  }
}

Library Structure

JS library structure after modularising code

Unit Testing

  • Many testing frameworks available in JS
  • Jest has similar structure to {testthat}
    • Add tests to __tests__ directory
    • Suffix filename with .test.js
  • npm install --save-dev jest
  • npm install --save-dev @babel/plugin-transform-modules-commonjs
    • Required to run tests using modular format

Unit Testing

// component/button.js
import Component from 'Component'

class Button extends Component {
  html = '<button ...>...</button>'
  constructor = {
    // runs Component constructor
    super()
  }
}

// component/__tests__/Button.test.js
import { Button } from '../Button'

test('sanity test - button constructs successfully', () => {
  const button = new Button()
  expect(button.html).toBe('<button ...>...</button>')
})

Unit Testing

Running npm run test in VS Code

Continuous Integration (CI)

Include JavaScript unit tests as part of your GitHub Actions

name: Run JS Tests (Jest)
on:
  push:
    branches: [dev, main]
  pull_request:
    branches: [dev, main]
defaults:
  run:
    working-directory: srcjs
jobs:
  js-unit-tests:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Install modules
      run: npm install
    - name: Run tests
      run: npm run test 

CI Results

JavaScript unit test results of the {designer} package


Test Suites: 32 passed, 32 total
Tests:       38 passed, 38 total
Snapshots:   0 total
Time:        5.948 s
Ran all test suites.
Done in 6.82s.

Finished Result

Structure of JavaScript code of the {designer} package

Finished Result

Minified JavaScript file in the dev branch of the {designer} package

Q&A.