--- title: "Tutorial: Porting Liquid Oxygen to Shiny" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Tutorial: Porting Liquid Oxygen to Shiny} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ### Introduction [Liquid Oxygen](https://emdgroup-liquid.github.io/liquid/) is a UI component library based on the Liquid Design System, focusing on accessibility and interoperability. It provides React bindings and as such can be ported to Shiny through `shiny.react`. 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 `liquid` R package, which will make it possible to use Liquid Oxygen 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 liquid. 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("liquid") 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 `liquid.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 liquidDependency <- function() { htmltools::htmlDependency( name = "liquid", version = "0.1.0", package = "liquid", src = "www", script = "liquid.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 = "@emdgroup-liquid/liquid", name = name, props = shiny.react::asProps(...), deps = liquidDependency() ) } ``` We can now add Liquid components to our package easily! Let's try a [LdButton](https://emdgroup-liquid.github.io/liquid/components/ld-button/) and a [LdLoading](https://emdgroup-liquid.github.io/liquid/components/ld-loading/) for starters. ```r #' @export LdButton <- component("LdButton") #' @export LdLoading <- component("LdLoading") ``` ### Adding Liquid In the `js` directory we use `yarn` to add the Liquid Oxygen library. ```sh yarn init --yes yarn add @emdgroup-liquid/liquid@3.0.0 ``` In order to use react components we need to find where package exports are defined first. We need to look for export keyword with names of components. In case of this package, exports can be found in `@emdgroup-liquid/liquid/dist/react`. We will use a bundler to generate the `liquid.js` script from the following `js/src/index.js` file: ```js const Liquid = require('@emdgroup-liquid/liquid/dist/react'); require('@emdgroup-liquid/liquid/dist/css/liquid.css'); window.jsmodule = { ...window.jsmodule, '@emdgroup-liquid/liquid': Liquid }; ``` This script will make the Liquid Oxygen library available as `jsmodule[@emdgroup-liquid/liquid]` on the browser. It will also load the necessary CSS. ### Bundling We will use [webpack](https://webpack.js.org/) to build the `liquid.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/liquid.js`: ```js output: { path: path.join(__dirname, '..', 'inst', 'www'), filename: 'liquid.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"]' } ``` 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: 'liquid.js' }, module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] } ] }, externals: { 'react': 'jsmodule["react"]', 'react-dom': 'jsmodule["react-dom"]', '@/shiny.react': 'jsmodule["@/shiny.react"]' } }; 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(liquid) shinyApp( ui = tagList( LdButton("Test Button"), LdLoading() ), server = function(input, output) {} ) ``` Cool! Let's try something more advanced: ```r shinyApp( ui = tagList( LdButton( "Initiate loading", onClick = JS("(event) => Shiny.setInputValue('loading', true)") ), reactOutput("spinner") ), server = function(input, output) { output$spinner <- renderReact({ if (!is.null(input$loading) && input$loading) LdLoading() else NULL }) } ) ``` ### 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 Liquid = require('@emdgroup-liquid/liquid/dist/react'); const { InputAdapter } = require('@/shiny.react') require('@emdgroup-liquid/liquid/dist/css/liquid.css'); const LdSelect = InputAdapter(Liquid.LdSelect, (value, setValue) => ({ onLdchange: (event) => { setValue(event.detail); }, })); window.jsmodule = { ...window.jsmodule, '@emdgroup-liquid/liquid': Liquid, '@/liquid': { LdSelect } }; ``` In order to create an input that can be used in Shiny server we need to create the component with a hook that will set a value of Shiny input. We can use `InputAdapter` from `shiny.react` package to do it easily. The documentation states that Liquid components dispatch ldchange events, to change value of Shiny input we need to set a value when component changes its state. For React components we use onLdchange prop and we set the value using event.detail. This property contains an array of selected items from the dropdown. If the documentation provides information which event field contains value of input use the one from documentation. If it doesn't you can set a breakpoint in the browser to investigate what fields does event object have and use the appropriate one. We also add these lines to `R/components.R`: ```r input <- function(name, defaultValue) { function(inputId, ..., value = defaultValue) { shiny.react::reactElement( module = "@/liquid", name = name, props = shiny.react::asProps(inputId = inputId, ..., value = value), deps = liquidDependency() ) } } #' @export LdOption <- component("LdOption") #' @export LdSelect.shinyInput <- input("LdSelect", NULL) #' @export LdSelect <- component("LdSelect") ``` After rebuilding and reinstalling the package we can now rewrite the last Shiny app example as: ```r shinyApp( ui = tagList( LdSelect.shinyInput( placeholder = "Pick a fruit", inputId = "fruit", value = NULL, LdOption("Apple"), LdOption("Orange"), LdOption("Strawberry") ), textOutput("selectedFruit") ), server = function(input, output) { output$selectedFruit <- renderText({ input$fruit }) } ) ``` ### 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. [`@emdgroup-liquid/liquid`](https://www.npmjs.com/package/@emdgroup-liquid/liquid). * For modules with custom code, use the R package name with `@/` prefix, e.g. `@/liquid`.