Getting started with EZRShiny

EZRShiny helps you build multi-page ‘shiny’ apps that all look and work the same way. Instead of assembling pages, navigation bars, cards and sidebars from ‘bslib’ yourself, you describe the app as a set of tabs and fill them with inputs and outputs, each built with one short function call.

This guide covers:

  1. Starting a new app
  2. How an app is laid out
  3. The page
  4. Tabs
  5. Inputs
  6. Outputs
  7. The server
  8. The example app
  9. Moving an existing app to EZRShiny

Starting a new app

createEZApp() makes a folder with a working app in it, ready to edit:

library(EZRShiny)

createEZApp("myApp", appName = "My App")
shiny::runApp("myApp")

The app has an upload page and a results tab with a table. Upload any CSV and click Run to see it work, then replace the pieces with your own.

How an app is laid out

Every EZRShiny app uses the same folder layout:

myApp/
├── app.R               packages, UI and server
├── Functions/          your helper functions, one or more .R files
├── Necessary_Files/    files the app needs at start, such as a data template
└── www/                images for the page, such as logos

app.R is split into numbered sections, so every app reads in the same order:

# App Startup ----
## 1.0 Load Libraries ----
library(shiny)
library(bslib)
library(shinyjs)
library(EZRShiny)

## 2.0 Load Basics ----
options(shiny.maxRequestSize = 300 * 1024^2)   # allow uploads up to 300 MB
sourceFunctions("Functions")                    # load every .R file in Functions/

## 3.0 Universal Vars ----
appName <- "My App"

# UI ----
ui <- UINav(...)

# Server ----
server <- function(input, output, session) { ... }

# Run App ----
shinyApp(ui = ui, server = server)

sourceFunctions() loads every .R file in a folder, so adding a helper is just a matter of saving a new file in Functions/. The files load in alphabetical order, which is why numbering them (1-Read_In_Data.R, 2-Plots.R) is a good habit.

The page

UINav() builds the whole page. Everything else in the UI goes inside it:

ui <- UINav(
  logoFile = "logo.png",   # from the www folder; leave out for no logo
  appName = "My App",      # leave out to use a global appName variable
  barColor = "#1F4E79",    # navigation bar color; leave out for the theme default

  singleTab("Upload", ...),
  biLevelTab("Results", ...)
)

The navigation bar shows, from left to right: the logos, the app name, one entry per tab, and a dark mode switch.

Argument What it does Default
logoFile Image file names in www/, shown in order. Several logos are allowed: c("institute.png", "lab.svg"). no logo
appName App name shown after the logos. the global appName variable, if there is one
logoHeight Height of each logo, as a CSS size. "40vh"
barColor Navigation bar color. Text switches between light and dark to stay readable. theme default
theme A bslib::bs_theme() for the whole app, for example bs_theme(preset = "cosmo", primary = "#005596"). bs_theme()

Tabs

Tabs come in two kinds: top-level tabs, which go straight into UINav(), and sub tabs, which go inside a top-level tab.

Top-level tabs

Function Holds Put inside it
singleTab() one page inputs and outputs
sidebarLevelTab() one page with a sidebar inputs and outputs
biLevelTab() a row of sub tabs subTab(), subSidebarTab()
triLevelTab() a drop-down menu, where each entry has its own row of sub tabs triSubTab(), triSubSidebarTab()

Sub tabs

Function Holds
subTab() inputs and outputs
subSidebarTab() a sidebar, plus inputs and outputs
triSubTab() a row of subTab()s or subSidebarTab()s (inside triLevelTab() only)
triSubSidebarTab() a sidebar, plus a row of sub tabs (inside triLevelTab() only)

subTwoColPage(leftSide, rightSide) isn’t a tab: it splits any page into two equal columns.

Putting them together

Sidebars take their inputs as a list() in sidebarElements. Everything after that fills the main part of the page:

ui <- UINav(
  appName = "Tab Tour",

  # One page
  singleTab("Upload",
    navUpload("dataUpload", "Upload a CSV")
  ),

  # One page with a sidebar
  sidebarLevelTab("Table",
    sidebarElements = list(
      navSelect("columns", "Columns to show", multiple = TRUE)
    ),
    navOutputTable("dataTable")
  ),

  # A row of sub tabs
  biLevelTab("Plots",
    subSidebarTab("Scatter",
      sidebarElements = list(navButton("makeScatter", "Make plot")),
      navOutputPlot("scatterPlot")
    ),
    subTab("Side by Side",
      subTwoColPage(navOutputPlot("leftPlot"), navOutputPlot("rightPlot"))
    )
  ),

  # A menu of tabs, each with its own sub tabs
  triLevelTab("Models",
    triSubTab("Linear",
      subTab("Fit", navOutputText("linearFit")),
      subTab("Residuals", navOutputPlot("linearResiduals"))
    ),
    triSubSidebarTab("Custom",
      sidebarElements = list(navText("formula", "Model formula")),
      subTab("Fit", navOutputText("customFit"))
    )
  )
)

Tab IDs

Each row of sub tabs has an id, and each sub tab has a value. You use them in the server to show, hide or select tabs. Both default to the title with spaces removed: biLevelTab("Explore Data", ...) has the ID "ExploreData", and subTab("Box Plots", ...) has the value "BoxPlots". The server helpers remove spaces too, so you can write the titles as they appear on screen.

The navigation bar itself has the ID "root". Top-level tabs keep their titles exactly, spaces included.

Each tab’s contents sit in a card 85% of the window tall. To change the height of one tab, use its height argument. To change it for every tab, set the option before building the UI:

options(EZRShiny.cardHeight = "70vh")

Inputs

Every input fills the width of its sidebar or page and takes an optional tooltipText, which adds an info icon to the label:

navButton("runData", "Run analysis", tooltipText = "Upload your data first.")

Every input starts with inputId and label, the same as in ‘shiny’. The other arguments use ’shiny’s names too:

Function Makes Other arguments (defaults)
navButton() a button that shows a spinner while its code runs
navSelect() a drop-down list choices, selected (first choice), multiple = FALSE, create = FALSE
navUpload() a file upload multiple = FALSE
navDownload() a download button
navCheckbox() an on/off switch value = FALSE (starts off)
navText() a text box value = ""
navNumeric() a number box value = 1, min = NA, max = NA (no limits)
navColor() a color picker value = "white"
navDate() a date picker range = FALSE (one date)

multiple = TRUE allows more than one choice or file. create = TRUE lets the user type in options that aren’t in choices.

navSelect("pcX", "PC for x-axis", choices = paste0("PC", 1:10))
navSelect("groups", "Groups to compare", choices = groupNames, multiple = TRUE)
navSelect("tags", "Tags", choices = c("a", "b"), multiple = TRUE, create = TRUE)

Choices for navSelect() can be left out of the UI and filled in from the server once data is loaded:

# UI
navSelect("groups", "Pick groups", multiple = TRUE)

# Server
updateSelectizeInput(session, "groups", choices = unique(theData$Group))

navSpanText() adds a line of text with an info icon, for explaining a page.

Outputs

Each output is a placeholder the server fills in with the matching render function:

UI Server
navOutputTable() output$id <- DT::renderDT(...)
navOutputPlot() output$id <- renderPlot(...)
navOutputPlotly() output$id <- plotly::renderPlotly(...)
navOutputGirafe() output$id <- ggiraph::renderGirafe(...)
navOutputPic() output$id <- renderImage(...)
sideNavOutputPic() output$id <- renderImage(...), 40% wide, for beside another output
navOutputText() output$id <- renderText(...)

Like inputs, outputs can have a label shown above them and a tooltipText that adds an info icon after the label. Use it to explain how to read a plot or table. With a tooltip and no label, only the icon is shown.

navOutputPlotly("pcaPlot", label = "PCA of all samples",
                tooltipText = "Each point is one sample, colored by group.")

The server

The server of an EZRShiny app follows a few habits that keep it predictable.

Keep the user’s data in one place. Store anything that needs to carry across the app in one reactiveValues() list:

global <- reactiveValues(
  datasets = list(
    rawData = NULL
  )
)

Set up the page on start. Hide tabs and switch off buttons and downloads that can’t be used yet:

observe({
  startSection("Run on Start")

  hideNavTabs(rootID = "Results", tabIDs = c("Table", "Plot"))
  deactivateItems(c("runAnalysis", "resultsDownload"))

  endSection("Run on Start")
})

Tie work to buttons. Code in observeEvent(input$button, ...) runs only when the button is clicked. Code that reads inputs anywhere else reruns every time any of those inputs change.

observeEvent(input$runAnalysis, {
  startSection("Run Analysis")

  ## Load globals
  rawData <- global$datasets$rawData

  ## Inputs
  alpha <- input$alpha

  ## Do things
  results <- runMyAnalysis(rawData, alpha)

  ## App interactions
  output$resultsTable <- DT::renderDT({ results })
  activateItems(c("resultsDownload"))
  showNavTabs(rootID = "Results", tabIDs = c("Table", "Plot"))
  nav_select("root", "Results")

  ## Save globals
  global$datasets$results <- results

  endSection("Run Analysis")
})
Helper What it does
activateItems(ids), deactivateItems(ids) Switch inputs, buttons or downloads on or off.
showNavTabs(rootID, tabIDs), hideNavTabs(rootID, tabIDs) Show or hide tabs. showNavTabs() also selects the first tab given.
bslib::nav_select("root", "Tab Title") Move the user to a top-level tab.
startSection(name), endSection(name) Write markers to the log, so you can see which part of the server is running.

The example app

EZRShiny comes with a complete example app, “Titanic Explorer”, that explores who survived the Titanic using R’s built-in Titanic data:

runExampleApp()

It uses every piece described above:

Tab Built with Shows
Load Data singleTab() navDownload(), navCheckbox(), navUpload(), navButton(), navSpanText()
Passengers sidebarLevelTab() navSelect() filled in from the server, navOutputTable()
Survival biLevelTab() with subSidebarTab() and subTab() navColor(), navText(), navOutputPlot(), navOutputPlotly(), sub tabs revealed with showNavTabs()
Compare Groups triLevelTab() with triSubTab() and triSubSidebarTab() navOutputGirafe(), subTwoColPage()

Every tab except Load Data is hidden until data is loaded, and each download is switched off until there’s something to download. Its files are a good starting point for your own app:

exampleFolder <- system.file("examples", "titanicExplorer", package = "EZRShiny")
list.files(exampleFolder, recursive = TRUE)
file.copy(exampleFolder, "myCopy", recursive = TRUE)

Moving an existing app to EZRShiny

If your app sources a copy of the standards file from its Functions folder:

  1. Delete the standards file from Functions/. The package replaces it.

  2. Replace its source(...) line with library(EZRShiny). Keep sourceFunctions("Functions") to load your other helpers.

  3. Move options(shiny.maxRequestSize = ...) into app.R if you need uploads over 5 MB. The package doesn’t change options for you.

  4. Logos and colors are now arguments of UINav() instead of being built in:

    ui <- UINav(
      logoFile = c("institute_logo.png", "app_logo.png"),
      logoHeight = c("35vh", "40vh"),
      appName = appName,
      barColor = "#005596",
      ...
    )
  5. cardHeight is no longer a global variable. Use options(EZRShiny.cardHeight = "85vh") or each tab’s height argument.

  6. Inputs take TRUE/FALSE instead of short words, and their arguments use ’shiny’s names. Calls with only an ID, a label and tooltipText don’t change. The rest change like this:

    Before After
    navSelect("id", "Label", "Single", "Locked", choices) navSelect("id", "Label", choices)
    navSelect("id", "Label", "Multi", "Locked", choices) navSelect("id", "Label", choices, multiple = TRUE)
    navSelect("id", "Label", "Single", "Create", choices) navSelect("id", "Label", choices, create = TRUE)
    navUpload("id", "Label", "Single") navUpload("id", "Label")
    navUpload("id", "Label", "Multi") navUpload("id", "Label", multiple = TRUE)
    navCheckbox("id", "Label", "T") navCheckbox("id", "Label", value = TRUE)
    navCheckbox("id", "Label", "F") navCheckbox("id", "Label")
    navNumeric("id", "Label", 5) unchanged, but min and max now default to no limit instead of 0 and 1
    navNumeric(..., theValue = 5) navNumeric(..., value = 5)
    navColor(..., colorValue = "red") navColor(..., value = "red")
    navDate("id", "Label", TRUE) navDate("id", "Label", range = TRUE)

    Passing an old short word such as "Single" where TRUE or FALSE is expected stops with an error that explains the change.