usethis::create_package("myLeprechaunApp")
Shiny frameworks (part 4, leprechaun
)
This is the fourth post in a series on shiny app frameworks. In this post, I’ll build a ‘leaner and smaller’ shiny app using the leprechaun
package and framework.
Framework comparisons
The original post that inspired this series compares ‘vanilla shiny’ (bare-bones shiny application), golem
, leprechaun
, and rhino
across a series of dimensions (framework reliability, target type of developer, overall developing experience, etc.).
This series focuses on thee technical areas: Start, Build, and Use.
Start covers the steps required to begin building a shiny app with the framework (from the console and IDE), and any additional packages or dependencies.
Build covers the development process, which includes writing and storing code, data, external resources (like CSS or JavaScript), testing, etc.
Use shows how developers can launch their application using the given framework/package locally (i.e., within the RStudio (Posit) IDE), common workflow tips, and any aspects of the framework I found confusing while building the application.
In part 1, I built a ‘minimal’ shiny app (
VanillaApp
)In part 2, I structured the shiny application as an R package using
usethis
anddevtools
, (myPkgApp
).In post number three, I used the popular
golem
framework (myGolemApp
)
The GitHub repo with all shiny app setups is here.
myLeprechaunApp
leprechaun
apps are built using the same methods as R packages (devtools
and usethis
), but are intended to be a ‘leaner and smaller’ version of golem
.
Start
Click Code to see output
Code
✔ Creating '../projects/myLeprechaunApp/'
✔ Setting active project to '/Users/mjfrigaard/projects/myLeprechaunApp'
✔ Creating 'R/'
✔ Writing 'DESCRIPTION'
Package: myLeprechaunApp
Title: What the Package Does (One Line, Title Case)
Version: 0.0.0.9000
Authors@R (parsed):
* First Last <first.last@example.com> [aut, cre] (YOUR-ORCID-ID)
Description: What the package does (one paragraph).
License: `use_mit_license()`, `use_gpl3_license()` or friends to
pick a license
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.2.3
✔ Writing 'NAMESPACE'
✔ Writing 'myLeprechaunApp.Rproj'
✔ Adding '^myLeprechaunApp\\.Rproj$' to '.Rbuildignore'
✔ Adding '.Rproj.user' to '.gitignore'
✔ Adding '^\\.Rproj\\.user$' to '.Rbuildignore'
✔ Opening '/Users/mjfrigaard/projects/myLeprechaunApp/' in new RStudio session
✔ Setting active project to '<no active project>'
When creating a new leprechaun
package in the IDE, it’s identical to the R package setup.
After the new project opens, install and load the leprechaun
package, then run leprechaun::scaffold()
:
install.packages("leprechaun")
library(leprechaun)
leprechaun::scaffold()
Click Code to see output
Code
── Scaffolding leprechaun app ─────────────────────────────────────────
── Creating lock file ──
✔ Creating .leprechaun
── Adding dependencies ──
✔ Adding 'shiny' to Imports in DESCRIPTION
✔ Adding 'bslib' to Imports in DESCRIPTION
✔ Adding 'htmltools' to Imports in DESCRIPTION
✔ Adding 'pkgload' to Suggests in DESCRIPTION
── Generating code ──
✔ Creating R/ui.R
✔ Creating R/assets.R
✔ Creating R/run.R
✔ Creating R/server.R
✔ Creating R/leprechaun-utils.R
✔ Creating R/_disable_autoload.R
✔ Creating R/zzz.R
✔ Creating R/input-handlers.R
✔ Creating inst/dev
✔ Creating inst/assets
✔ Creating inst/img
✔ Creating inst/run/app.R
── Ignoring files ──
✔ Adding '^\\.leprechaun$' to '.Rbuildignore'
This results in the following folder tree:
myLeprechaunApp/
├── DESCRIPTION
├── NAMESPACE
├── R/
│ ├── _disable_autoload.R
│ ├── assets.R
│ ├── input-handlers.R
│ ├── leprechaun-utils.R
│ ├── run.R
│ ├── server.R
│ ├── ui.R
│ └── zzz.R
├── inst/
│ ├── assets/
│ ├── dev/
│ ├── img/
│ └── run/
│ └── app.R
└── myLeprechaunApp.Rproj
7 directories, 12 files
This structure should look familiar if you’ve been following along with this series. The standard R package files and folders (DESCRIPTION
, NAMESPACE
, R/
, and myLeprechaunApp.Rproj
) are accompanied by multiple sub-folders in inst/
(recall that inst/
contents are available in the package when the package is installed).
Setup
In this section I’ll cover the initial files in the new leprechaun
application.
R/
-
The
R/
folder contents are below:- Some of these files should look familiar (
R/ui.R
,R/server.R
, andR/run.R
)
└── R/ ├── _disable_autoload.R ├── assets.R ├── input-handlers.R ├── leprechaun-utils.R ├── run.R ├── server.R ├── ui.R └── zzz.R
- Some of these files should look familiar (
-
The initial application files are created using
leprechaun::scaffold()
, which takes the following options as function arguments:-
ui
controls the application layout (can be"fluidPage"
or"navbarPage"
, defaults to"navbarPage"
) -
bs_version
Bootstrap version (“If shiny > 1.6 is installed defaults to version 5, otherwise version 4” ) -
overwrite
: Overwrite all files?
-
-
assets.R
: contains theserveAssets()
function, which will identify the modules using CSS or JavaScript and createdependencies
, a list of metadata on the app. If you run the function after initially building yourleprechaun
app, you’ll see the following:Click on Code to view code in
R/assets.R
Code
#' Dependencies #' #' @param modules JavaScript files names that require #' the `type = module`. #' @importFrom htmltools htmlDependency #' #' @keywords internal serveAssets <- function(modules = NULL) { # JavaScript files javascript <- list.files( system.file(package = "myLeprechaunApp"), recursive = TRUE, pattern = ".js$" ) modules <- get_modules(javascript, modules) javascript <- remove_modules(javascript, modules) # CSS files css <- list.files( system.file(package = "myLeprechaunApp"), recursive = TRUE, pattern = ".css$" ) # so dependency processes correctly names(css) <- rep("file", length(css)) names(javascript) <- rep("file", length(javascript)) # serve dependencies dependencies <- list() standard <- htmlDependency( "myLeprechaunApp", version = utils::packageVersion("myLeprechaunApp"), package = "myLeprechaunApp", src = ".", script = javascript, stylesheet = css ) dependencies <- append(dependencies, list(standard)) if (!is.null(modules)) { modules <- htmlDependency( "myLeprechaunApp-modules", version = utils::packageVersion("myLeprechaunApp"), package = "myLeprechaunApp", src = ".", script = modules, meta = list(type = "module") ) dependencies <- append(dependencies, list(modules)) } return(dependencies) } #' Module #' #' Retrieve and add modules from a vector of files. #' #' @param files JavaScript files #' @param modules JavaScript files names that require #' the `type = module`. #' @importFrom htmltools htmlDependency #' #' @keywords internal #' @name js-modules remove_modules <- function(files, modules) { if (is.null(modules)) { return(files) } # make pattern pattern <- collapse_files(modules) # remove modules files[!grepl(pattern, files)] } #' @rdname js-modules #' @keywords internal get_modules <- function(files, modules) { if (is.null(modules)) { return(NULL) } # make pattern pattern <- collapse_files(modules) # remove modules files[grepl(pattern, files)] } # collapse files into a pattern collapse_files <- function(files) { pattern <- paste0(files, collapse = "$|") paste0(pattern, "$") }
serveAssets()
Click on Code to view the initial output from
serveAssets()
Code
[[1]] List of 10 $ name : chr "myLeprechaunApp" $ version : chr "0.0.0.9000" $ src :List of 1 ..$ file: chr "." $ meta : NULL $ script : Named chr(0) ..- attr(*, "names")= chr(0) $ stylesheet: Named chr(0) ..- attr(*, "names")= chr(0) $ head : NULL $ attachment: NULL $ package : chr "myLeprechaunApp" $ all_files : logi TRUE - attr(*, "class")= chr "html_dependency"
_disable_autoload.R
is a way to disable theshiny::loadSupport()
function. By default, shiny will load any top-level supporting.R
files in theR/
directory adjacent to theapp.R
/server.R
/ui.R
files.-
input-handlers.R
:Click on Code to view code in
R/input-handlers.R
Code
#' Input Dataframe #' #' Converts the input received from the WebSocket #' to a data.frame. #' #' @param data Input data received from WebSocket. #' #' @keywords internal leprechaun_handler_df <- function(data){ do.call("rbind", lapply(data)) } #' Input List #' #' Forces the input received from the WebSocket #' to a list. This should really not be needed as #' it is handled like so by default. #' #' @param data Input data received from WebSocket. #' #' @keywords internal leprechaun_handler_list <- function(data){ return(data) } .onAttach <- function(...) { shiny::registerInputHandler( "myLeprechaunApp.list", leprechaun_handler_list, force = TRUE ) shiny::registerInputHandler( "myLeprechaunApp.df", leprechaun_handler_df, force = TRUE ) }
-
leprechaun-utils.R
initially contains themake_send_message()
function (which is used in theR/server.R
below).Click on Code to view code in
R/leprechaun-utils.R
Code
#' Create a Helper to Send Messages #' #' Create a function to send custom messages to the front-end, #' this function makes it such that the namespace is carried #' along. #' The namespace is appended as `ns`. #' The namespace with the optional hyphen is #' included as `ns2`. #' #' @param session Shiny session to derive namespace #' @param prefix A prefix to add to all types. #' Note that the prefix is followed by a hyphen `-`. #' #' @examples #' \dontrun{ #' send_message <- make_send_message(session) #' send_message("do-sth") #' send_message("do-sth-else", x = 1) #' #' # with prefix #' send_message <- make_send_message(session, prefix = "PREFIX") #' #' # this sends a message of type: #' # PREFIX-so-th #' send_message("do-sth") #' } #' #' @noRd #' @keywords internal make_send_message <- function(session, prefix = NULL) { ns <- session$ns(NULL) ns2 <- ns if (length(ns) > 0 && ns != "") { ns2 <- paste0(ns2, "-") } function(msgId, ...) { if (!is.null(prefix)) { msgId <- sprintf("%s-%s", prefix, msgId) } session$sendCustomMessage( msgId, list( ns = ns, ns2 = ns2, ... ) ) } }
-
run.R
contains functions for running the production (run()
) and development version of the application (run_dev()
):Click on Code to view code in
R/run.R
Code
#' Run #' #' Run application #' #' @param ... Additional parameters to pass to [shiny::shinyApp]. #' #' @importFrom shiny shinyApp #' #' @export run <- function(...){ shinyApp( ui = ui, server = server, ... ) } #' Run Development #' #' Runs the development version which includes #' the build step. #' #' @keywords internal run_dev <- function(){ file <- system.file("run/app.R", package = "myLeprechaunApp") shiny::shinyAppFile(file) }
-
server.R
by default createssend_message
withmake_send_message(session)
(seeR/leprechaun-utils.R
above).Click on Code to view code in
R/server.R
Code
#' Server #' #' Core server function. #' #' @param input,output Input and output list objects #' containing said registered inputs and outputs. #' @param session Shiny session. #' #' @noRd #' @keywords internal server <- function(input, output, session){ send_message <- make_send_message(session) }
-
ui.R
holds theui()
andassets()
functions.assets()
loads the resources called in theR/assets.R
file (seeserveAssets()
function above).Click on Code to view code in
ui()
Code
#' Shiny UI #' #' Core UI of package. #' #' @param req The request object. #' #' @import shiny #' @importFrom bslib bs_theme #' #' @keywords internal ui <- function(req) { fluidPage( theme = bs_theme(version = 5), assets(), h1("myLeprechaunApp") ) }
Click on Code to view code in
assets()
Code
#' Assets #' #' Includes all assets. #' This is a convenience function that wraps #' [serveAssets] and allows easily adding additional #' remote dependencies (e.g.: CDN) should there be any. #' #' @importFrom shiny tags #' #' @keywords internal assets <- function() { list( serveAssets(), # base assets (assets.R) tags$head( # Place any additional depdendencies here # e.g.: CDN ) ) }
-
zzz.R
containsshiny
’saddResourcePath()
function for adding images to the application (ininst/img/
)Click on Code to view code in
R/zzz.R
Code
.onLoad <- function(...){ shiny::addResourcePath( "img", system.file("img", package = "myLeprechaunApp") ) }
inst/run/app.R
-
app.R
contains a file that looks like it would be used to run the application, but it’s not. This file contains a call toleprechaun::build()
, thenpkgload::load_all()
.Click on Code to view code in
inst/run/app.R
- This file is not run directly (check
leprechaun::add_app_file()
):
- This file is not run directly (check
Build
Building leprechaun
apps is similar to golem
/R packages. New code is placed in the R/ folder, and application resources (CSS, SASS, JavaScript files) are added using one of the leprechaun::use_*
functions:
More assets can be added using the leprechaun::use_packer()
function.
Develop
The leprechaun::scaffold()
defaults to a navbarPage()
, but I’ll switch to a fluidPage()
for this example.
After devtools::load_all()
and devtools::document()
, restarting and loading the package, I can run the application with run()
.
run()
add_module()
Creating modules is simple with leprechaun::add_module()
.
-
The initial UI module:
leprechaun::add_module("var_input")
✔ Creating R/module_var_input.R
- Similar to
golem
, this creates functions for the UI and server portions of the module.
#' var_input UI #' #' @param id Unique id for module instance. #' #' @keywords internal var_inputUI <- function(id){ ns <- NS(id) tagList( h2("var_input"), ) }
- The initial server module:
#' var_input Server #' #' @param id Unique id for module instance. #' #' @keywords internal var_input_server <- function(id){ moduleServer( id, function( input, output, session ){ ns <- session$ns send_message <- make_send_message(session) # your code here } ) } # UI # var_inputUI('id') # server # var_input_server('id')
- Note the
send_message <- make_send_message(session)
invar_input_server()
. I will show how this is used in the JavaScript section below.
- Similar to
The code for the var_input
and plot_display
modules are below.
-
The
R/module_var_input.R
file:Click on Code to view code in
R/module_var_input.R
Code
#' var_input UI #' #' @param id Unique id for module instance. #' #' @keywords internal #' #' @return shiny UI module #' @export var_inputUI #' #' @description A shiny Module. #' #' @importFrom shiny NS tagList selectInput #' @importFrom shiny sliderInput textInput var_inputUI <- function(id){ ns <- shiny::NS(id) shiny::tagList( shiny::selectInput( inputId = ns("y"), label = "Y-axis:", choices = c( "IMDB rating" = "imdb_rating", "IMDB number of votes" = "imdb_num_votes", "Critics Score" = "critics_score", "Audience Score" = "audience_score", "Runtime" = "runtime" ), selected = "audience_score" ), shiny::selectInput( inputId = ns("x"), label = "X-axis:", choices = c( "IMDB rating" = "imdb_rating", "IMDB number of votes" = "imdb_num_votes", "Critics Score" = "critics_score", "Audience Score" = "audience_score", "Runtime" = "runtime" ), selected = "imdb_rating" ), shiny::selectInput( inputId = ns("z"), label = "Color by:", choices = c( "Title Type" = "title_type", "Genre" = "genre", "MPAA Rating" = "mpaa_rating", "Critics Rating" = "critics_rating", "Audience Rating" = "audience_rating" ), selected = "mpaa_rating" ), shiny::sliderInput( inputId = ns("alpha"), label = "Alpha:", min = 0, max = 1, step = 0.1, value = 0.5 ), shiny::sliderInput( inputId = ns("size"), label = "Size:", min = 0, max = 5, value = 2 ), shiny::textInput( inputId = ns("plot_title"), label = "Plot title", placeholder = "Enter plot title" ) ) } #' var_input Server #' #' @param id Unique id for module instance. #' #' @keywords internal #' #' @return shiny server module #' @export var_input_server #' #' @importFrom shiny NS moduleServer reactive var_input_server <- function(id){ moduleServer( id, function( input, output, session ){ ns <- session$ns send_message <- make_send_message(session) # your code here return( list( "x" = shiny::reactive({ input$x }), "y" = shiny::reactive({ input$y }), "z" = shiny::reactive({ input$z }), "alpha" = shiny::reactive({ input$alpha }), "size" = shiny::reactive({ input$size }), "plot_title" = shiny::reactive({ input$plot_title }) ) ) } ) } # UI # var_inputUI('id') # server # var_input_server('id')
-
The
R/module_plot_display.R
file:- My
plot_dispay
module collects the data from var_input and creates the plot with the custompoint_plot()
function:
Click on Code to view code in
R/module_plot_display.R
Code
#' plot_display UI #' #' @param id Unique id for module instance. #' #' @return shiny UI module #' @export plot_displayUI #' #' @description A shiny Module. #' #' @importFrom shiny NS tagList tags #' @importFrom shiny plotOutput plot_displayUI <- function(id){ ns <- shiny::NS(id) shiny::tagList( shiny::tags$br(), shiny::tags$blockquote( shiny::tags$em( shiny::tags$h6( "The code for this application comes from the ", shiny::tags$a("Building web applications with Shiny", href = "https://rstudio-education.github.io/shiny-course/" ), "tutorial" ) ) ), shiny::plotOutput(outputId = ns("scatterplot")) ) } #' plot_display Server #' #' @param id Unique id for module instance. #' #' @keywords internal plot_display_server <- function(id, var_input){ moduleServer( id, function( input, output, session ){ ns <- session$ns send_message <- make_send_message(session) # your code here movies <- myLeprechaunApp::movies inputs <- shiny::reactive({ plot_title <- tools::toTitleCase(var_inputs$plot_title()) list( x = var_inputs$x(), y = var_inputs$y(), z = var_inputs$z(), alpha = var_inputs$alpha(), size = var_inputs$size(), plot_title = plot_title ) }) output$scatterplot <- shiny::renderPlot({ plot <- point_plot( df = movies, x_var = inputs()$x, y_var = inputs()$y, col_var = inputs()$z, alpha_var = inputs()$alpha, size_var = inputs()$size ) plot + ggplot2::labs( title = inputs()$plot_title, x = stringr::str_replace_all( tools::toTitleCase( inputs()$x), "_", " "), y = stringr::str_replace_all( tools::toTitleCase( inputs()$y), "_", " ")) + ggplot2::theme_minimal() + ggplot2::theme(legend.position = "bottom") }) } ) } # UI # plot_displayUI('id') # server # plot_display_server('id')
- My
After creating the modules, adding them to the UI (R/ui.R
) and server (R/server.R
) is straightforward.
-
The
R/ui.R
file:Code
#' Shiny UI #' #' Core UI of package. #' #' @param req The request object. #' #' @import shiny #' @importFrom bslib bs_theme #' #' @keywords internal ui <- function(req) { fluidPage( theme = bs_theme(version = 5), assets(), h1("myLeprechaunApp"), # Begin new code --> shiny::sidebarLayout( shiny::sidebarPanel( var_inputUI("vars") ), shiny::mainPanel( plot_displayUI("plot") ) ) ## End new code <-- ) }
-
The
R/server.R
file:- The server also has the
make_send_message()
function in it by default (more on that below).
Click on Code to view code in
R/server.R
Code
#' Server #' #' Core server function. #' #' @param input,output Input and output list objects #' containing said registered inputs and outputs. #' @param session Shiny session. #' #' @noRd #' @keywords internal server <- function(input, output, session){ send_message <- make_send_message(session) ## New code --> selected_vars <- var_input_server("vars") plot_display_server("plot", var_inputs = selected_vars) ## New code <-- }
- The server also has the
The other components of myLeprechaunApp
were created using the standard usethis
package development functions.
use_data_raw()
- the
movies
data was added toinst/extdata
and loaded into the package withusethis::use_data_raw()
use_r()
-
usethis::use_r()
createdR/utils_plot_display.R
to hold thepoint_plot()
functionClick on Code to view code in
R/utils_plot_display.R
Code
#' Plot points (shiny) #' #' @param df input dataset (tibble or data.frame) #' @param x_var x variable #' @param y_var y variable #' @param col_var color variable #' @param alpha_var alpha value #' @param size_var size value #' #' @return plot object #' @export point_plot #' #' @importFrom ggplot2 ggplot aes geom_point #' #' @examples #' \donttest{ #' load( #' list.files( #' system.file("extdata", package = "myLeprechaunApp"), #' pattern = "movies", #' full.names = TRUE) #' ) #' point_plot(df = movies, #' x_var = "critics_score", #' y_var = "imdb_rating", #' col_var = "critics_rating", #' alpha_var = 1/3, #' size_var = 2) #' } point_plot <- function(df, x_var, y_var, col_var, alpha_var, size_var) { ggplot2::ggplot(data = df, ggplot2::aes(x = .data[[x_var]], y = .data[[y_var]], color = .data[[col_var]])) + ggplot2::geom_point(alpha = alpha_var, size = size_var) }
Now I can run devtools::load_all()
, devtools::document()
, restart and load the package, then run()
myLeprechaunApp
inst/
leprechaun
uses the inst/
folder similar to the golem
framework, but instead of only loading the files in inst/app/www
, leprechaun
apps include four sub-folders that are ready at application runtime.
packer
To demonstrate how the make_send_message()
function works, I’ll walk through the JavaScript example from the package website.
-
Run
packer::scaffold_leprechaun()
packer::scaffold_leprechaun()
Click on Code to view the output from
packer::scaffold_leprechaun()
── Scaffolding leprechaun ────────────────────────────────────────────── ✔ Initialiased npm ✔ webpack, webpack-cli, webpack-merge installed with scope "dev" ✔ Added npm scripts ✔ Created srcjs directory ✔ Created srcjs/config directory ✔ Created webpack config files ── Adding files to .gitignore and .Rbuildignore ── ✔ Setting active project to '/Users/mjfrigaard/projects/myLeprechaunApp' ✔ Adding '^srcjs$' to '.Rbuildignore' ✔ Adding '^node_modules$' to '.Rbuildignore' ✔ Adding '^package\\.json$' to '.Rbuildignore' ✔ Adding '^package-lock\\.json$' to '.Rbuildignore' ✔ Adding '^webpack\\.dev\\.js$' to '.Rbuildignore' ✔ Adding '^webpack\\.prod\\.js$' to '.Rbuildignore' ✔ Adding '^webpack\\.common\\.js$' to '.Rbuildignore' ✔ Adding 'node_modules' to '.gitignore' ── Scaffold built ── ℹ Run `bundle` to build the JavaScript files ℹ Run `leprechaun::use_packer()`
-
leprechaun::use_packer()
✔ Creating inst/dev/packer.R ✔ Adding 'packer' to Suggests in DESCRIPTION ! This requires `leprechaun::build()` or the `leprechaun::build_roclet`
-
leprechaun::build()
✔ Running packer.R ✔ Bundled
Now I can see what new files have been added to the package/app.
-
In the
inst/dev/
folder:- I can see the
packer.R
file has been added
inst/dev/ └── packer.R 1 directory, 1 file
Click on Code to view the output from
packer.R
Code
#' Bundle for Prod #' #' Bundles packer using packer. packer_bundle <- function(){ has_packer <- requireNamespace("packer", quietly = TRUE) if(!has_packer){ warning( "Requires `packer` package: `install.packages('packer')`\n", "Skipping.", call. = FALSE ) return() } packer::bundle() } packer_bundle()
- I can see the
-
In the
srcjs/
folder:- I can see how
modules/message.js
andindex.js
create the alert withShiny.addCustomMessageHandler
srcjs/ ├── config │ ├── entry_points.json │ ├── externals.json │ ├── loaders.json │ ├── misc.json │ └── output_path.json ├── index.js └── modules └── message.js
The JavaScript in
modules/message.js
andindex.js
// srcjs/modules/message.js export const message = (msg) => { alert(msg); }// srcjs/index.js import { message } from './modules/message.js'; import 'shiny'; // In shiny server use: // session$sendCustomMessage('show-packer', 'hello packer!') .addCustomMessageHandler('show-packer', (msg) => { Shinymessage(msg.text); })
- I can see how
To use the JS message scripts in srcjs/
, I add the following to R/server.R
:
-
In
R/server.R
send_message <- make_send_message(session) send_message("show-packer", text = "this is a message from your server()")
After running devtools::load_all()
and devtools::document()
, the application loads with an alert:
send_message()
I can also include messages from modules.
-
In
R/module_plot_display.R
send_message <- make_send_message(session) send_message("show-packer", text = "this is a message from your plot_display module")
send_message()
(module)Read more about sending JavaScript messages here on the shiny website.
img/
I’ll demonstrate how to use the inst/
folder by adding an image to the application.
-
Assume I want to add
leprechaun.jpg
to my UI. I start by adding the file toinst/img/
:inst/ └── img/ └── leprechaun.jpg <- new image file!
-
Then I add the
img/
path to the code to UI:ui <- function(req) { fluidPage( theme = bs_theme(version = 5), assets(), h1("myLeprechaunApp"), shiny::sidebarLayout( shiny::sidebarPanel( var_inputUI("vars") ), shiny::mainPanel( # new image shiny::tags$img(src = "img/leprechaun.jpg"), plot_displayUI("plot") ) ) ) }
Once again, run devtools::load_all()
and devtools::document()
, restarting and loading the package, then run the application with run()
inst/img/
Sass
leprechaun
also has helper functions for adding additional resources (or assets) to an application. I’ll work through the SASS example from the website below.
To add a Sass file, I can use leprechaun
’s use_sass()
function.
-
Run
leprechaun::use_sass()
(no arguments):leprechaun::use_sass()
- This will add files to
assets/
anddev/
and I see the following messages:
✔ Creating scss ✔ Creating inst/dev/sass.R ✔ Adding 'sass' to Suggests in DESCRIPTION ✔ Adding '^scss$' to '.Rbuildignore' ! This requires `leprechaun::build()` or the `leprechaun::build_roclet`
- Below are the new files in
inst/dev/
andsass/
:
inst/ ├── scss/ │ ├── _core.scss │ └── main.scss └── dev/ └── sass.R
- This will add files to
The scss/
folder is created by leprechaun::use_sass()
, and it includes _core.scss
and main.scss
.
-
_core.scss
: the original file is belowhtml{.error { color: red } }
-
I will change the
color:
fromred
to green (#38B44A
) using$accent: #38B44A;
$accent: #38B44A; html{ h1 {color: $accent; } }
-
Then save this file and run
leprechaun::build()
leprechaun::build()
✔ Running packer.R ✔ Bundled ✔ Running sass.R
dev/
-
The
inst/dev/sass.R
file contains asass_build()
function-
sass_build()
looks in thescss/
folder formain.scss
and creates theinst/assets/style.min.css
file.
Click on Code to view code in
inst/dev/sass.R
Code
#' Build CSS #' #' Build the sass sass_build <- function() { has_sass <- requireNamespace("sass", quietly = TRUE) if (!has_sass) { warning( "Requires `sass` package: `install.packages('sass')`\n", "Skipping.", call. = FALSE ) return() } output <- sass::sass( sass::sass_file( "scss/main.scss" ), cache = NULL, options = sass::sass_options( output_style = "compressed" ), output = "inst/assets/style.min.css" ) invisible(output) } sass_build()
-
Once again, I run devtools::load_all()
, devtools::document()
, install and restart, then load the package and run()
myLeprechaunApp
with new Sassassets/
How does leprechaun::build()
work?
The assets/
folder contains the files generated by the .R
scripts in the dev/
folder.
-
The contents of the
inst/dev/
folder:inst/dev/ ├── packer.R └── sass.R 1 directory, 2 files
-
The contents of the
inst/assets/
folder:inst/assets/ ├── index.js └── style.min.css 1 directory, 2 files
inst/dev/sass.R
createsinst/assets/style.min.css
andinst/dev/packer.R
createsinst/assets/index.js
“Do not call this function from within the app. It helps build things, not run them.” -
build.md
guide
check serveAssets()
After running leprechaun::use_sass()
and leprechaun::build()
(which adds the scss/
folder and the .R
script in inst/dev/
), I can re-check the serveAssets()
function:
serveAssets()
[[1]]
List of 10
$ name : chr "myLeprechaunApp"
$ version : chr "0.0.0.9000"
$ src :List of 1
..$ file: chr "."
$ meta : NULL
$ script : Named chr "assets/index.js"
..- attr(*, "names")= chr "file"
$ stylesheet: Named chr [1:2] "assets/style.min.css" "html/R.css"
..- attr(*, "names")= chr [1:2] "file" "file"
$ head : NULL
$ attachment: NULL
$ package : chr "myLeprechaunApp"
$ all_files : logi TRUE
- attr(*, "class")= chr "html_dependency"
This shows me stylesheet
has been updated with "assets/style.min.css"
and script
has been updated with "assets/index.js"
(these files are loaded into the application when it runs).
Use
Running leprechaun
apps:
When I initially create a new leprechaun
package with leprechaun::scaffold()
, I can run the application after a few quick steps:
Install and restart (optional)
myLeprechaunApp
App files:
-
R/
: After the initial setup, theR/
folder of a leprechaun app contains standardui.R
,server.R
files, as well as therun.R
function for running the app.myLeprechaunApp/ └── R/ ├── _disable_autoload.R ├── assets.R ├── input-handlers.R ├── leprechaun-utils.R ├── run.R ├── server.R ├── ui.R └── zzz.R 1 directory, 8 files
- The additional files are specific to the
leprechaun
framework and workflow.
- The additional files are specific to the
Configure:
leprechaun
app configuration files use the config package (similar to golem
). Unlike the golem
package, it’s not assumed I’ll be using a config.yml
file, but I can easily add one with leprechaun::use_config()
.
use_config()
adds ainst/config.yml
andR/config.R
-
The default value in the
config.yml
files isproduction: true
, which can be read usingconfig_read()
inR/config.R
.config_read()
$production [1] TRUE
- Values can be added to
inst/config.yml
using the config file format, then theCONFIG_FILE
can be set as an environment variable
- Values can be added to
Workflow:
-
The
inst/
folder contains various sub-folders for including external app resources (images, SASS, CSS, JavaScript, etc.).myLeprechaunApp/ └── inst/ ├── assets/ ├── dev/ ├── img/ └── run/ └── app.R 5 directories, 1 file
-
leprechaun
apps are packages, so theinst/
folders are available to the application at runtime (which I can find usingsystem.file()
).- Below I’ve passed the output from
system.file(".", package = "myLeprechaunApp")
tofs::dir_tree()
to view it’s contents:
Code
├── DESCRIPTION ├── INDEX ├── Meta/ │ ├── Rd.rds │ ├── data.rds │ ├── features.rds │ ├── hsearch.rds │ ├── links.rds │ ├── nsInfo.rds │ └── package.rds ├── NAMESPACE ├── R/ │ ├── myLeprechaunApp │ ├── myLeprechaunApp.rdb │ └── myLeprechaunApp.rdx ├── assets/ │ ├── index.js │ └── style.min.css ├── data/ │ ├── Rdata.rdb │ ├── Rdata.rds │ └── Rdata.rdx ├── dev/ │ ├── packer.R │ └── sass.R ├── extdata/ │ └── movies.RData ├── help/ │ ├── AnIndex │ ├── aliases.rds │ ├── myLeprechaunApp.rdb │ ├── myLeprechaunApp.rdx │ └── paths.rds ├── html/ │ ├── 00Index.html │ └── R.css ├── img/ │ └── leprechaun.jpg └── run/ └── app.R
- I can see the
inst/
folders and files I’ve created are available tomyLeprechaunApp
at runtime:
Code
├── DESCRIPTION ├── NAMESPACE ├── assets/ │ ├── index.js │ └── style.min.css ├── dev/ │ ├── packer.R │ └── sass.R ├── extdata/ │ └── movies.RData └── img/ └── leprechaun.jpg
- Below I’ve passed the output from
Recap
leprechaun
delivers on its promise to be a ‘leaner and smaller’ version of golem.
Most of the features in golem
are also accessible in leprechaun
. Including multiple inst/
sub-folders makes adding assets to the application easier, and leprechaun
has a long list of use_*
functions for including Sass, CSS, HTML, and JavaScript. The package website has examples for getting started and adding multiple resources, but unfortunately the function Reference had limited documentation.
leprechaun
doesn’t come with any testing functions, although this can be done using testthat
and shinytest2
(just as we would with a standard R package).
For the next (and last) post in this series, I will build a shiny application using the rhino
package.