Liquid Oxygen
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 or
Google’s MUI.
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 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 installed.
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.
It is also a good idea to list the dependencies in the
DESCRIPTION
file:
Imports:
htmltools,
shiny,
shiny.react
In React, a component is a function which takes props and returns an element. 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:
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:
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 and a LdLoading for starters.
In the js
directory we use yarn
to add the
Liquid Oxygen library.
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:
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.
We will use webpack to build
the liquid.js
file.
There is a handy online
tool 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:
Now let’s tweak the config a bit. We change the output to
inst/www/liquid.js
:
We add externals
to let webpack know where to look for modules provided by
shiny.react:
externals: {
'react': 'jsmodule["react"]',
'react-dom': 'jsmodule["react-dom"]',
'@/shiny.react': 'jsmodule["@/shiny.react"]'
}
Our final js/webpack.config.js
looks as follows:
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;
We are ready to build our package! First of all, we run webpack in
the js
directory:
This will generate the inst/www/webpack.js
bundle. We
should also generate the NAMESPACE file:
We can now install the package directly with
devtools::install()
and try it out!
Let’s try a simple app first to test our components:
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:
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:
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
:
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:
The module name passed to shiny.react::createElement()
can be arbitrary, but the following convention is recommended:
@emdgroup-liquid/liquid
.@/
prefix, e.g. @/liquid
.