It tackles three "issues" with Shiny modules:
- Generating scoped (namespaced) CSS and JavaScript code.
- Places all the code for a module in a single file.
- Binds CSS, and JavaScript of a module to it
This is inspired from Vue where one defines a component in a single file.
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">Count is: {{ count }}</button>
</template>
<style scoped>
button {
font-weight: bold;
}
</style>
Where the file includes the style, script, and template. We reproduce the same with R with the addition of the server component.
You can install the development version of component from GitHub with:
# install.packages("remotes")
remotes::install_github("devOpifex/component")
From a package (see lerprechaun or golem), create a component.
# create a component
# give it a name
component::create("test")
This creates a new file in R/
named component-test.R
.
Components generate code via devtools::document()
(more on that later)
add the roxygen2 roclet to your DESCRIPTION
, e.g.:
Roxygen: list(markdown = TRUE, roclets = c("collate", "namespace", "rd", "component::roclet_component"))
So create()
creates the component file but devtools::document()
generates
the usable version of the component.
component::create() => `component-<name>.R` => devtools::document() => `component-generated-<name>.R`
The default component as created by component::create()
looks something like the code below
(R/component-test.R
).
#' @component test
NULL
#' test javascript
#'
#' test javascript component.
#'
#' @param ... Any other argument.
#'
#' @keywords internal
.test_javascript <- \(...) {
"$('{{class red}}').on('mouseenter', (e) => {
$(e.target).toggleClass('{{ns red}}');
})"
}
#' test css
#'
#' test css component.
#'
#' @param ... Any other argument.
#'
#' @keywords internal
.test_css <- \(...) {
c(
"{{class red}}{color:red;}",
"h1{font-weight: bold;}"
)
}
#' test ui
#'
#' test UI component.
#'
#' @param ns Shiny's namespace function.
#' @param ... Any other argument.
#'
#' @keywords internal
.test_ui <- \(ns, ...) {
shiny::div(
shiny::h1("Component", class = ns("red")),
plotOutput(ns("plot"))
)
}
#' test server
#'
#' test server component.
#'
#' @param input,output,session Arguments passed from mdoule's server.
#' @param ... Any other argument.
#'
#' @keywords internal
.test_server <- \(input, output, session, ...) {
output$plot <- shiny::renderPlot(plot(stats::runif(200)))
}
The @component
tag indicates the name of the component,
in this example test
.
This tells component which functions to scan for:
.<component>_javascript
.<component>_css
.<component>_server
.<component>_ui
The .test_javascript
and .test_css
functions are used to generate
scoped code.
class
: transforms{{ class red }}
into.namespace-red
id
: transforms{{ id red }}
into#namespace-red
ns
: transforms{{ ns red }}
intonamespace-red
json
: transforms{{ json letters[1:2] }}
into['a', 'b']
- You may still use
{{ x }}
in which case it will evaluatex
(passed*ui
or*server
function).
These functions should return a character vector of length one or more.
The CSS is minified using sass, if you have
uglifyjs installed
it is used to minify the JavaScript otherwise it is not
(install it with npm install uglify-js -g
).
Almost identical to the UI and server functions of a shiny module
The created template cannot be used as-is, by documenting the code with
devtools::document()
we create a new file (R/component-generated-<component>.R
)
which contains:
<component>_ui
<component>_server
In this example, test_ui
, and server_ui
.
These are the functions that one should use in the application.
Always work on/edit the functions starting with a dot (e.g.: .test_ui
),
as the generated files get regenerated at every devtools::document()
call.
Example based on the abose "test" component, after running devtools::document()
.
library(shiny)
ui <- fluidPage(
title = "Test",
test_ui("test")
)
server <- function(...){
test_server("test")
}
shinyApp(ui, server)
Here is the classic counter example but scoped and using JavaScript only.
#' @component counter
NULL
#' counter javascript
#'
#' counter javascript component.
#'
#' @param ... Any other argument.
#'
#' @keywords internal
.counter_javascript <- \(...) {
"$(() => {
let index = 0;
$('{{id button}}').on('click', (e) => {
index++;
$('{{id output}}').html(index);
$('{{id output}}').toggleClass('{{ns criminal}}');
})
})"
}
#' counter css
#'
#' counter css component.
#'
#' @param ... Any other argument.
#'
#' @keywords internal
.counter_css <- \(...) {
"{{class criminal}}{
color: {{color}};
font-weight: bold;
}"
}
#' counter ui
#'
#' counter UI component.
#'
#' @param ns Shiny's namespace function.
#' @param ... Any other argument.
#'
#' @keywords internal
.counter_ui <- \(ns, ...) {
shiny::div(
shiny::h1("Counter"),
shiny::tags$button(
class = "btn btn-sm btn-primary",
id = ns("button"),
"Counter"
),
shiny::p(
id = ns("output"),
0L
)
)
}
#' counter server
#'
#' counter server component.
#'
#' @param input,output,session Arguments passed from mdoule's server.
#' @param ... Any other argument.
#'
#' @keywords internal
.counter_server <- \(input, output, session, ...) {}
Then, after documenting, it can be used as.
library(shiny)
ui <- fluidPage(
title = "Test",
counter_ui("c1", color = "red"),
counter_ui("c2", color = "blue"),
)
server <- function(...){
counter_server("c1")
counter_server("c2")
}
shinyApp(ui, server)