--- title: "Building an app with shinypal" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Building an app with shinypal} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", # the code below describes a running Shiny app, so it is shown but not run eval = FALSE ) ``` shinypal turns a [Shiny](https://shiny.posit.co/) app into a point-and-click workflow builder: the user assembles a pipeline step by step, and shinypal shows (and exports) the equivalent reproducible R code via `{shinymeta}`. This vignette walks through building your own. A shinypal app has two parts: 1. a thin **host app** that lays out the shinypal interface and initializes it, and 2. a directory of **modules**, one per grouping of analysis steps, that you write. ```{r setup} library(shinypal) ``` ## The host app The host calls `shinypal_ui()` in its UI and `shinypal_setup()` at the top of its server function. Both take a vector of paths to your module directories. ```{r host-app} modules <- list.dirs("modules", recursive = FALSE) ui <- bslib::page_navbar( title = "My workflow builder", bslib::nav_panel("Build", shinypal_ui(modules)) ) server <- function(input, output, session) { shinypal_setup( input, output, session, modules = modules, download_template = "modules/report.qmd" ) } shiny::shinyApp(ui, server) ``` `shinypal_ui()` renders two cards (the menu of *possible* steps on the left and the draggable *workflow* on the right) plus a sidebar holding the live report and a download button. `shinypal_setup()` initializes the shared reactive state, wires the report and download handler, and sources each module's `ui-aux.R` and `server.R`. ## Anatomy of a module Each module is a folder containing at least three files: | File | Sourced by | Purpose | |--------------|---------------------|-------------------------------------------------------------------| | `ui-main.R` | `shinypal_ui()` | the panel advertising the possible steps included in the module | | `ui-aux.R` | `shinypal_setup()` | defines the `fun_workflow` and `fun_report` helpers for the steps | | `server.R` | `shinypal_setup()` | defines the server logic for each step to the workflow | `ui-aux.R` and `server.R` are sourced with `local = TRUE`, so they share the host's `input`/`output`/`session` and can see each other's definitions. ### `ui-main.R` This file is *evaluated* and its value becomes a panel in the menu, so it should return a single [bslib::accordion_panel()]. Give it an action button the server will listen for. ```{r ui-main} bslib::accordion_panel( "Filter rows", "Keep only the rows that match a condition.", shiny::actionButton("add_filter", "Add this step") ) ``` ### `ui-aux.R` Defines two functions for each step with index `ind`: one that draws the step's panel in the workflow accordion, and one that draws its block in the report. Use the shinypal UI helpers so the panel gets a remove button, a dataset selector, and a copy-able code block for free. ```{r ui-aux} filter_workflow <- function(ind) { accordion_panel_remove_button( ind, "Filter rows", select_dataset_input(ind), select_column_input(ind, "Column to filter on:"), shiny::numericInput(paste0("min_", ind), "Minimum value", value = 0), df_modal_button(ind) ) } filter_report <- function(ind) { verbatimTextOutput_copy(ind) } ``` ### `server.R` When the *Add* button is clicked, grab a fresh index with `next_step_index()`, build a [shinymeta::metaReactive2()] that both *computes* the result and *records* the code that produced it, then register everything with `add_shinypal_data_step()`. ```{r server} shiny::observeEvent(input$add_filter, { ind <- next_step_index() # metaReactive2 returns the data when called, and its code when expanded. # Name it occs_ so shinypal can find it as an intermediate dataset. data <- shinymeta::metaReactive2(varname = paste0("occs_", ind), { df <- get_int_data(input[[paste0("dataset_", ind)]])() col <- input[[paste0("column_", ind)]] shiny::req(col) shinymeta::metaExpr( dplyr::filter(df, !!col >= !!input[[paste0("min_", ind)]]) ) }) add_shinypal_data_step( ind, data = data, fun_workflow = filter_workflow, fun_report = filter_report, libs = "dplyr", select_dataset = TRUE, column_ids = paste0("column_", ind) ) }) ``` `add_shinypal_data_step()` stores the result under `occs_`, renders its generated code into the report, wires the "view data" modal, and (because `select_dataset = TRUE`) keeps the dataset dropdown in sync as other steps come and go. A later step can then consume this step's output by calling `get_int_data()`. ## Plot steps A step that produces a figure instead of a dataset uses `add_shinypal_plot_step()` with a [shinymeta::metaRender2()] render. Point `output_prefix` at the `plotOutput()` id used in the report. ```{r plot-step} plot_render <- shinymeta::metaRender2(shiny::renderPlot, { df <- get_int_data(input[[paste0("dataset_", ind)]])() shinymeta::metaExpr(plot(df)) }) add_shinypal_plot_step( ind, plot = plot_render, fun_workflow = plot_workflow, fun_report = function(ind) shiny::plotOutput(paste0("plot_", ind)), output_prefix = "plot_" ) ``` ## The report template `shinypal_setup(download_template = ...)` points at a Quarto/R Markdown template with two knitr-expandable variables, `{{libraries}}` and `{{code}}`, which shinypal fills with the assembled `library()` calls and step code. Files a user uploads through a [shiny::fileInput()] can be bundled into the download by registering them with `file_observe()`. ## Deploying to the browser with shinylive shinypal apps can be exported with [shinylive](https://posit-dev.github.io/r-shinylive/) so they run entirely in the browser via webR. Because some things behave differently there (e.g., no system `zip`, and packages without a WebAssembly build can't load), use `is_shinylive()` to gate that behavior: ```{r shinylive} # only attempt to load a package that has no WebAssembly build outside webR if (!is_shinylive() && requireNamespace("paleobioDB", quietly = TRUE)) { library(paleobioDB) } ```