--- title: "Tutorial: Porting Blueprint to Shiny" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Tutorial: Porting Blueprint to Shiny} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ### Introduction [Blueprint](https://blueprintjs.com/) is a [React](https://reactjs.org/)-based UI library from Palantir. It provides a rich set of components for building web interfaces and it is similar in concept to Microsoft's [Fluent UI](https://developer.microsoft.com/en-us/fluentui#/) or Google's [MUI](https://mui.com/core/). In this tutorial we will (begin to) create a `blueprint` R package, which will make it possible to use Blueprint in R/Shiny akin to how [shiny.fluent](https://github.com/Appsilon/shiny.fluent) does it for Fluent UI. It should give you enough understanding of shiny.react to allow you to use other React libraries in your projects, either by creating "wrapper" R packages or directly in you Shiny app. This tutorial is aimed at advanced users who feel comfortable with both Shiny and React. You will need R and [Node.js](https://nodejs.org/en/) installed. ### Creating the package To start off we create a new package called blueprint. The `js` directory will contain the Node.js toolchain and JavaScript sources which will be compiled into a single file. Only that file will be needed to use the package, so we add `js` to `.Rbuildignore` to decrease the size of our package. ```r usethis::create_package("blueprint") usethis::use_build_ignore("js") ``` It is also a good idea to list the dependencies in the `DESCRIPTION` file: ``` Imports: htmltools, shiny, shiny.react ``` ### The R interface In React, a [component](https://reactjs.org/docs/glossary.html#components) is a function which takes [props](https://reactjs.org/docs/glossary.html#props) and returns an [element](https://reactjs.org/docs/glossary.html#elements). These concepts map to R directly. In R, elements are created with `shiny.react::reactElement(module, name, props)`. In the browser, shiny.react will create the element by calling `React.createElement(jsmodule[module][name], props)`. It is our task to ensure that `jsmodule[module][name]` yields the right component. To accomplish it, we will later create a `blueprint.js` script which will set up the `jsmodule` global appropriately. To free the users of our package of having to include this script manually, we will use an HTML dependency. In `R/components.R` let's define: ```r blueprintDependency <- function() { htmltools::htmlDependency( name = "blueprint", version = "0.1.0", package = "blueprint", src = "www", script = "blueprint.js" ) } ``` To define components succinctly, let's create a helper. Remember - components are functions which take props and return elements: ```r component <- function(name) { function(...) shiny.react::reactElement( module = "@blueprintjs/core", name = name, props = shiny.react::asProps(...), deps = blueprintDependency() ) } ``` We can now add Blueprint components to our package easily! Let's try a [Switch](https://blueprintjs.com/docs/#core/components/switch) and a [ProgressBar](https://blueprintjs.com/docs/#core/components/progress-bar) for starters. ```r #' @export ProgressBar <- component("ProgressBar") #' @export Switch <- component("Switch") ``` ### Adding Blueprint In the `js` directory we use `yarn` to add the Blueprint library. The [documentation](https://blueprintjs.com/docs/#blueprint.quick-start) also suggests adding `react` and `react-dom`, but we skip them as they are already provided by shiny.react. ```sh yarn init --yes yarn add @blueprintjs/core ``` We will use a bundler to generate the `blueprint.js` script from the following `js/src/index.js` file: ```js const Blueprint = require('@blueprintjs/core'); require('@blueprintjs/core/lib/css/blueprint.css'); window.jsmodule = { ...window.jsmodule, '@blueprintjs/core': Blueprint }; ``` This script will make the Blueprint library available as `jsmodule[@blueprintjs/core]` on the browser. It will also load the necessary CSS. ### Bundling We will use [webpack](https://webpack.js.org/) to build the `blueprint.js` file. There is a handy [online tool](https://createapp.dev/webpack) which we can use to generate a configuration for that webpack. Let's just pick CSS from the Styling section and copy the the script to `js/webpack.config.js`. We also add dev dependencies as suggested by the tool: ```sh yarn add --dev webpack webpack-cli css-loader style-loader ``` Now let's tweak the config a bit. We change the output to `inst/www/blueprint.js`: ```js output: { path: path.join(__dirname, '..', 'inst', 'www'), filename: 'blueprint.js' } ``` We add [`externals`](https://webpack.js.org/configuration/externals/) to let webpack know where to look for modules provided by shiny.react: ```js externals: { 'react': 'jsmodule["react"]', 'react-dom': 'jsmodule["react-dom"]', '@/shiny.react': 'jsmodule["@/shiny.react"]' } ``` Lastly, we need a little hack for a [problem](https://github.com/palantir/blueprint/issues/4393) present in Blueprint as of writing this: ```js plugins: [ new webpack.DefinePlugin({ 'process.env': '{}' }) ] ``` Our final `js/webpack.config.js` looks as follows: ```js const webpack = require('webpack'); const path = require('path'); const config = { entry: './src/index.js', output: { path: path.join(__dirname, '..', 'inst', 'www'), filename: 'blueprint.js' }, module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] } ] }, externals: { 'react': 'jsmodule["react"]', 'react-dom': 'jsmodule["react-dom"]', '@/shiny.react': 'jsmodule["@/shiny.react"]' }, plugins: [ new webpack.DefinePlugin({ 'process.env': '{}' }) ] }; module.exports = config; ``` ### Building the package We are ready to build our package! First of all, we run webpack in the `js` directory: ```sh yarn webpack ``` This will generate the `inst/www/webpack.js` bundle. We should also generate the NAMESPACE file: ```r devtools::document() ``` We can now install the package directly with `devtools::install()` and try it out! ### Using the package Let's try a simple app first to test our components: ```r library(shiny) library(shiny.react) library(blueprint) shinyApp( ui = tagList( Switch(label = "Animate"), ProgressBar() ), server = function(input, output) {} ) ``` Cool! Let's try something more advanced: ```r withDefault <- function(x, default) { if (is.null(x)) default else x } shinyApp( ui = tagList( Switch( onChange = JS("(event) => Shiny.setInputValue('animate', event.target.checked)"), defaultChecked = TRUE, label = "Animate" ), reactOutput("progress") ), server = function(input, output) { output$progress <- renderReact({ ProgressBar(animate = withDefault(input$animate, TRUE)) }) } ) ``` ### Creating input wrappers Even simple components can be cumbersome to use in Shiny, as evident in the last example. It is a good idea to create `.shinyInput` wrappers to simplify the life of your users. We change our `js/src/index.js` to the following: ```js const Blueprint = require('@blueprintjs/core'); const { InputAdapter } = require('@/shiny.react') require('@blueprintjs/core/lib/css/blueprint.css'); const Switch = InputAdapter(Blueprint.Switch, (value, setValue) => ({ checked: value, onChange: (event) => setValue(event.target.checked), })); window.jsmodule = { ...window.jsmodule, '@blueprintjs/core': require('@blueprintjs/core'), '@/blueprint': { Switch } }; ``` We also add these lines to `R/components.R`: ```r input <- function(name, defaultValue) { function(inputId, ..., value = defaultValue) shiny.react::reactElement( module = "@/blueprint", name = name, props = shiny.react::asProps(inputId = inputId, ..., value = value), deps = blueprintDependency() ) } #' @export Switch.shinyInput <- input("Switch", FALSE) ``` After rebuilding and reinstalling the package we can now rewrite the last Shiny app example as: ```r shinyApp( ui = tagList( Switch.shinyInput( inputId = "animate", value = TRUE, label = "Animate" ), reactOutput("progress") ), server = function(input, output) { output$progress <- renderReact({ ProgressBar(animate = input$animate) }) } ) ``` ### Notes The module name passed to `shiny.react::createElement()` can be arbitrary, but the following convention is recommended: * For modules coming directly from [npm](https://www.npmjs.com/), use the npm name, e.g. [`@blueprintjs/core`](https://www.npmjs.com/package/@blueprintjs/core). * For modules with custom code, use the R package name with `@/` prefix, e.g. `@/blueprint`.