Compare commits

...

12 Commits

Author SHA1 Message Date
Winston Chang
337f4c9c40 Fixes for LaTeX docs 2020-04-14 09:32:07 -05:00
Winston Chang
e86c0c4be4 Add shinyAppTemplate to pkgdown.yml 2020-04-13 17:58:17 -05:00
Winston Chang
44a485b07a Add informative comments 2020-04-13 15:31:24 -05:00
Winston Chang
9138adf8a1 Move 12_template to app_template dir 2020-04-13 14:37:00 -05:00
Winston Chang
e588fc5a4a Updates from code review 2020-04-13 13:14:30 -05:00
Winston Chang
19965c9eb7 Rename utils.R to sort.R 2020-04-13 11:59:24 -05:00
Winston Chang
98e390bc1b Rename 12_counter to 12_template 2020-04-13 11:56:31 -05:00
Winston Chang
3994055056 App template updates 2020-04-08 17:14:15 -05:00
Winston Chang
374f7c2aa2 Rename tests/shinytests/ to tests/shinytest/ 2020-04-08 17:14:15 -05:00
Winston Chang
27ad5d6110 Template update 2020-04-08 17:14:15 -05:00
Winston Chang
d374f1dc88 Refinements to app template 2020-04-08 17:14:15 -05:00
trestletech
38349f354d Added skeleton function and example 2020-04-08 17:14:15 -05:00
19 changed files with 501 additions and 11 deletions

View File

@@ -102,6 +102,7 @@ URL: http://shiny.rstudio.com
BugReports: https://github.com/rstudio/shiny/issues
Collate:
'app.R'
'app_template.R'
'bookmark-state-local.R'
'stack.R'
'bookmark-state.R'

View File

@@ -230,6 +230,7 @@ export(setSerializer)
export(shinyApp)
export(shinyAppDir)
export(shinyAppFile)
export(shinyAppTemplate)
export(shinyOptions)
export(shinyServer)
export(shinyUI)

230
R/app_template.R Normal file
View File

@@ -0,0 +1,230 @@
#' Generate a Shiny application from a template
#'
#' This function populates a directory with files for a Shiny application.
#'
#' In an interactive R session, this function will, by default, prompt the user
#' which components to add to the application.
#'
#' The full example application includes the following files and directories:
#'
#' ```
#' appdir/
#' |- app.R
#' |- R
#' | |- my-module.R
#' | |-- sort.R
#' `-- tests
#' |- server.R
#' |- server
#' | |- test-mymodule.R
#' | `- test-server.R
#' |- shinytest.R
#' |- shinytest
#' | `- mytest.R
#' |- testthat.R
#' `-- testthat
#' |- helper-load.R
#' `- test-sort.R
#' ```
#'
#' Some notes about these files:
#' * `app.R` is the main application file.
#' * All files in the `R/` subdirectory are automatically sourced when the
#' application is run.
#' * `R/sort.R` and `R/my-module.R` are automatically sourced when
#' the application is run. The first contains a function `lexical_sort()`,
#' and the second contains code for a [Shiny module](moduleServer()) which
#' is used in the application.
#' * `tests/` contains various tests for the application. You may
#' choose to use or remove any of them. They can be executed by the
#' [runTests()] function.
#' * `tests/server.R` is a test runner for test files in
#' `tests/server/`.
#' * `tests/server/test-mymodule.R` is a test for the module.
#' * `tests/shinytest.R` is a test runner for test files in the
#' `tests/shinytest/` directory.
#' * `tests/shinytest/mytest.R` is a test that uses the
#' [shinytest](https://rstudio.github.io/shinytest/) package to do
#' snapshot-based testing.
#' * `tests/testthat.R` is a test runner for test files in the
#' `tests/testthat/` directory.
#' * `tests/testthat/helper-load.R` is a helper script that is automatically
#' loaded before running `test-mymodule.`R. (This is performed by the testthat
#' package.)
#' * `tests/testthat/test-sort.R` is a set of tests that use the
#' [testthat](https://testthat.r-lib.org/) package for testing.
#'
#' @param path Path to create new shiny application template.
#' @param examples Either one of "default", "ask", "all", or any combination of
#' "app", "rdir", "module", "shinytest", "testthat", and "server". In an
#' interactive session, "default" falls back to "ask"; in a non-interactive
#' session, "default" falls back to "all". With "ask", this function will
#' prompt the user to select which template items will be added to the new app
#' directory. With "all", all template items will be added to the app
#' directory.
#'
#' @export
shinyAppTemplate <- function(path = NULL, examples = "default")
{
if (is.null(path)) {
stop("Please provide a `path`.")
}
choices <- c(
app = "app.R : Main application file",
rdir = "R/sort.R : Helper file with R code",
module = "R/my-module.R : Example module",
shinytest = "tests/shinytest/ : Tests using shinytest package",
testthat = "tests/testthat/ : Tests using testthat",
server = "tests/server/ : Tests of server and module code"
)
if (identical(examples, "default")) {
if (interactive()) {
examples <- "ask"
} else {
examples <- "all"
}
}
if (!identical(examples, "ask") &&
!identical(examples, "all") &&
any(! examples %in% names(choices)))
{
stop('`examples` must be one of "default", "ask", "all", or any combination of "',
paste(names(choices), collapse = '", "'), '".')
}
if (identical(examples, "ask")) {
response <- select_menu(
c(all = "All", choices),
title = paste0(
"Select which of the following to add at ", path, "/ :"
),
msg = "Enter one or more numbers (with spaces), or an empty line to exit: \n"
)
examples <- names(response)
}
examples <- unique(examples)
if ("all" %in% examples) {
examples <- names(choices)
}
if (length(examples) == 0) {
return(invisible())
}
# Check if a directory is empty, ignoring certain files
dir_is_empty <- function(path) {
files <- list.files(path, all.files = TRUE, no.. = TRUE)
# Ignore .DS_Store files, which are sometimes automatically created on macOS
files <- setdiff(files, ".DS_Store")
return(length(files) != 0)
}
# Helper to resolve paths relative to our example
example_path <- function(path) {
system.file("app_template", path, package = "shiny")
}
# Copy the files for a tests/ subdirectory
copy_test_dir <- function(name, with_rdir, with_module) {
tests_dir <- file.path(path, "tests")
dir.create(tests_dir, showWarnings = FALSE, recursive = TRUE)
files <- dir(example_path("tests"), recursive = TRUE)
# Note: This is not the same as using dir(pattern = "^shinytest"), since
# that will not match files inside of shinytest/.
files <- files[grepl(paste0("^", name), files)]
# Filter out files related to R/sort.R, if applicable.
if (!with_rdir) {
files <- files[!grepl("utils", files)]
}
# Filter out module files, if applicable.
if (!with_module) {
files <- files[!grepl("module", files)]
}
# Create any subdirectories if needed
dirs <- setdiff(unique(dirname(files)), ".")
for (dir in dirs) {
dir.create(file.path(tests_dir, dir), showWarnings = FALSE, recursive = TRUE)
}
file.copy(
file.path(example_path("tests"), files),
file.path(path, "tests", files)
)
}
if (is.null(path)) {
stop("`path` is missing.")
}
if (file.exists(path) && !dir.exists(path)) {
stop(path, " exists but is not a directory.")
}
if (dir.exists(path) && dir_is_empty(path)) {
if (interactive()) {
response <- readline(paste0(
ensure_trailing_slash(path),
" is not empty. Do you want to create a Shiny app in this directory anyway? [y/n] "
))
if (tolower(response) != "y") {
return(invisible())
}
}
} else {
dir.create(path)
}
# app.R - If "app", populate with example; otherwise use empty file.
app_file <- file.path(path, "app.R")
if ("app" %in% examples) {
if (file.exists(app_file)) {
message("Not writing ", app_file, "because file already exists.")
} else {
writeChar(
as.character(htmlTemplate(
example_path("app.R"),
rdir = "rdir" %in% examples,
module = "module" %in% examples
)),
con = app_file,
eos = NULL
)
}
}
# R/ dir with utils and/or module
r_dir <- file.path(path, "R")
if ("rdir" %in% examples) {
dir.create(r_dir, showWarnings = FALSE, recursive = TRUE)
file.copy(example_path("R/sort.R"), r_dir, recursive = TRUE)
}
if ("module" %in% examples) {
dir.create(r_dir, showWarnings = FALSE, recursive = TRUE)
file.copy(example_path("R/my-module.R"), r_dir, recursive = TRUE)
}
# tests/ dir
if ("shinytest" %in% examples) {
copy_test_dir("shinytest", "rdir" %in% examples, "module" %in% examples)
}
if ("testthat" %in% examples) {
copy_test_dir("testthat", "rdir" %in% examples, "module" %in% examples)
}
if ("server" %in% examples) {
copy_test_dir("server", "rdir" %in% examples, "module" %in% examples)
}
if ("app" %in% examples) {
message("Shiny application created at ", ensure_trailing_slash(path))
}
}

View File

@@ -316,6 +316,15 @@ resolve <- function(dir, relpath) {
return(abs.path)
}
# Given a string, make sure it has a trailing slash.
ensure_trailing_slash <- function(path) {
if (!grepl("/$", path)) {
path <- paste0(path, "/")
}
path
}
isWindows <- function() .Platform$OS.type == 'windows'
# This is a wrapper for download.file and has the same interface.
@@ -1812,3 +1821,20 @@ cat_line <- function(...) {
cat(paste(..., "\n", collapse = ""))
}
select_menu <- function(choices, title = NULL, msg = "Enter one or more numbers (with spaces), or an empty line to exit: \n")
{
if (!is.null(title)) {
cat(title, "\n", sep = "")
}
nc <- length(choices)
op <- paste0(format(seq_len(nc)), ": ", choices)
fop <- format(op)
cat("", fop, "", sep = "\n")
repeat {
answer <- readline(msg)
answer <- strsplit(answer, "[ ,]+")[[1]]
if (all(answer %in% seq_along(choices))) {
return(choices[as.integer(answer)])
}
}
}

View File

@@ -0,0 +1,27 @@
mymoduleUI <- function(id, label = "Counter") {
# Al uses of Shiny input/output IDs in the UI must be namespaced,
# as in ns("x").
ns <- NS(id)
tagList(
actionButton(ns("button"), label = label),
verbatimTextOutput(ns("out"))
)
}
mymoduleServer <- function(id) {
# moduleServer() wraps a function to create the server component of a
# module.
moduleServer(
id,
function(input, output, session) {
count <- reactiveVal(0)
observeEvent(input$button, {
count(count() + 1)
})
output$out <- renderText({
count()
})
count
}
)
}

View File

@@ -0,0 +1,5 @@
# Given a numeric vector, convert to strings, sort, and convert back to
# numeric.
lexical_sort <- function(x) {
as.numeric(sort(as.character(x)))
}

52
inst/app_template/app.R Normal file
View File

@@ -0,0 +1,52 @@
ui <- fluidPage(
{{
# These blocks of code are processed with htmlTemplate()
if (isTRUE(module)) {
' # ======== Modules ========
# mymoduleUI is defined in R/my-module.R
mymoduleUI("mymodule1", "Click counter #1"),
mymoduleUI("mymodule2", "Click counter #2"),
# =========================
'
}
}}
wellPanel(
sliderInput("size", "Data size", min = 5, max = 20, value = 10),
{{
if (isTRUE(rdir)) {
' div("Lexically sorted sequence:"),'
} else {
' div("Sorted sequence:"),'
}
}}
verbatimTextOutput("sequence")
)
)
server <- function(input, output, session) {
{{
if (isTRUE(module)) {
' # ======== Modules ========
# mymoduleServer is defined in R/my-module.R
mymoduleServer("mymodule1")
mymoduleServer("mymodule2")
# =========================
'
}
}}
data <- reactive({
{{
if (isTRUE(rdir)) {
' # lexical_sort from R/sort.R
lexical_sort(seq_len(input$size))'
} else {
' sort(seq_len(input$size))'
}
}}
})
output$sequence <- renderText({
paste(data(), collapse = " ")
})
}
shinyApp(ui, server)

View File

@@ -0,0 +1,6 @@
files <- list.files("./server", full.names = FALSE)
origwd <- getwd()
setwd("./server")
on.exit(setwd(origwd), add=TRUE)
lapply(files, source, local=environment())

View File

@@ -0,0 +1,19 @@
# Use testthat just for expectations
library(testthat)
# See ?testServer for more information
testServer(mymoduleServer, {
# Set initial value of a button
session$setInputs(button = 0)
# Check the value of the reactiveVal `count()`
expect_equal(count(), 1)
# Check the value of the renderText()
expect_equal(output$out, "1")
# Simulate a click
session$setInputs(button = 1)
expect_equal(count(), 2)
expect_equal(output$out, "2")
})

View File

@@ -0,0 +1,11 @@
# Use testthat just for expectations
library(testthat)
testServer('../..', {
# Set the `size` slider and check the output
session$setInputs(size = 6)
expect_equal(output$sequence, "1 2 3 4 5 6")
session$setInputs(size = 12)
expect_equal(output$sequence, "1 2 3 4 5 6 7 8 9 10 11 12")
})

View File

@@ -0,0 +1,3 @@
library(shinytest)
shinytest::testApp("../")

View File

@@ -0,0 +1,7 @@
app <- ShinyDriver$new("../../")
app$snapshotInit("mytest")
app$snapshot()
app$setInputs(`mymodule1-button` = "click")
app$setInputs(`mymodule1-button` = "click")
app$snapshot()

View File

@@ -0,0 +1,6 @@
library(testthat)
# Run in the "current" environment, because shiny::runTests() is going to
# provision a new environment that's just for our test. And we'll want access to
# the supporting files that were already loaded into that env.
testthat::test_dir("./testthat", env = environment())

View File

@@ -0,0 +1,11 @@
# The RStudio IDE offers a "Run Tests" button when it sees testthat tests but it runs
# in its own environment/process. Which means that any helpers we've loaded into our
# environment won't be visible. So we add this helper not because it's actually needed
# in the typical `shiny::runTests` workflow, but to make that IDE button work.
# Once the IDE adds proper support for this style, we'll be able to drop these files.
#
# Note that this may redundantly source the files in your R/ dir depending on your
# workflow.
library(shiny)
shiny::loadSupport("../../", renv = globalenv())

View File

@@ -0,0 +1,5 @@
# Test the lexical_sort function from R/utils.R
test_that("Lexical sorting works", {
expect_equal(lexical_sort(c(1, 2, 3)), c(1, 2, 3))
expect_equal(lexical_sort(c(1, 2, 3, 13, 11, 21)), c(1, 11, 13, 2, 21, 3))
})

View File

@@ -4,29 +4,34 @@
\alias{markdown}
\title{Insert inline Markdown}
\usage{
markdown(mds, extensions = TRUE, ...)
markdown(mds, extensions = TRUE, .noWS = NULL, ...)
}
\arguments{
\item{mds}{A character vector of Markdown source to convert to HTML. If the
vector has more than one element, resulting HTML is concatenated.}
vector has more than one element, a single-element character vector of
concatenated HTML is returned.}
\item{extensions}{Enable Github syntax extensions, defaults to \code{TRUE}.}
\item{extensions}{Enable Github syntax extensions; defaults to \code{TRUE}.}
\item{.noWS}{Character vector used to omit some of the whitespace that would
normally be written around generated HTML. Valid options include \code{before},
\code{after}, and \code{outside} (equivalent to \code{before} and \code{end}).}
\item{...}{Additional arguments to pass to \code{\link[commonmark:markdown_html]{commonmark::markdown_html()}}.
These arguments are \emph{\link[rlang:dyn-dots]{dynamic}}.}
}
\value{
an \code{html}-classed character vector of rendered HTML
a character vector marked as HTML.
}
\description{
This function accepts a character vector of
\href{https://en.wikipedia.org/wiki/Markdown}{Markdown}-syntax text and renders
it to HTML that may be included in a UI.
This function accepts
\href{https://en.wikipedia.org/wiki/Markdown}{Markdown}-syntax text and returns
HTML that may be included in Shiny UIs.
}
\details{
Prior to interpretation as Markdown, leading whitespace is trimmed from text
with \code{\link[glue:trim]{glue::trim()}}. This makes it possible to insert Markdown and for it to
be processed correctly even when the call to \code{markdown()} is indented.
Leading whitespace is trimmed from Markdown text with \code{\link[glue:trim]{glue::trim()}}.
Whitespace trimming ensures Markdown is processed correctly even when the
call to \code{markdown()} is indented within surrounding R code.
By default, \link[commonmark:extensions]{Github extensions} are enabled, but this
can be disabled by passing \code{extensions = FALSE}.

View File

@@ -52,6 +52,6 @@ below to see their documentation.
\describe{
\item{fastmap}{\code{\link[fastmap]{is.key_missing}}, \code{\link[fastmap]{key_missing}}}
\item{htmltools}{\code{\link[htmltools]{a}}, \code{\link[htmltools]{br}}, \code{\link[htmltools]{code}}, \code{\link[htmltools]{div}}, \code{\link[htmltools]{em}}, \code{\link[htmltools]{h1}}, \code{\link[htmltools]{h2}}, \code{\link[htmltools]{h3}}, \code{\link[htmltools]{h4}}, \code{\link[htmltools]{h5}}, \code{\link[htmltools]{h6}}, \code{\link[htmltools]{hr}}, \code{\link[htmltools]{HTML}}, \code{\link[htmltools]{htmlTemplate}}, \code{\link[htmltools]{img}}, \code{\link[htmltools]{includeCSS}}, \code{\link[htmltools]{includeHTML}}, \code{\link[htmltools]{includeMarkdown}}, \code{\link[htmltools]{includeScript}}, \code{\link[htmltools]{includeText}}, \code{\link[htmltools]{is.singleton}}, \code{\link[htmltools]{p}}, \code{\link[htmltools]{pre}}, \code{\link[htmltools]{singleton}}, \code{\link[htmltools]{span}}, \code{\link[htmltools]{strong}}, \code{\link[htmltools]{suppressDependencies}}, \code{\link[htmltools]{tag}}, \code{\link[htmltools]{tagAppendAttributes}}, \code{\link[htmltools]{tagAppendChild}}, \code{\link[htmltools]{tagAppendChildren}}, \code{\link[htmltools]{tagGetAttribute}}, \code{\link[htmltools]{tagHasAttribute}}, \code{\link[htmltools]{tagList}}, \code{\link[htmltools]{tags}}, \code{\link[htmltools]{tagSetChildren}}, \code{\link[htmltools]{validateCssUnit}}, \code{\link[htmltools]{withTags}}}
\item{htmltools}{\code{\link[htmltools]{HTML}}, \code{\link[htmltools]{a}}, \code{\link[htmltools]{br}}, \code{\link[htmltools]{code}}, \code{\link[htmltools]{div}}, \code{\link[htmltools]{em}}, \code{\link[htmltools]{h1}}, \code{\link[htmltools]{h2}}, \code{\link[htmltools]{h3}}, \code{\link[htmltools]{h4}}, \code{\link[htmltools]{h5}}, \code{\link[htmltools]{h6}}, \code{\link[htmltools]{hr}}, \code{\link[htmltools]{htmlTemplate}}, \code{\link[htmltools]{img}}, \code{\link[htmltools]{includeCSS}}, \code{\link[htmltools]{includeHTML}}, \code{\link[htmltools]{includeMarkdown}}, \code{\link[htmltools]{includeScript}}, \code{\link[htmltools]{includeText}}, \code{\link[htmltools]{is.singleton}}, \code{\link[htmltools]{p}}, \code{\link[htmltools]{pre}}, \code{\link[htmltools]{singleton}}, \code{\link[htmltools]{span}}, \code{\link[htmltools]{strong}}, \code{\link[htmltools]{suppressDependencies}}, \code{\link[htmltools]{tag}}, \code{\link[htmltools]{tagAppendAttributes}}, \code{\link[htmltools]{tagAppendChild}}, \code{\link[htmltools]{tagAppendChildren}}, \code{\link[htmltools]{tagGetAttribute}}, \code{\link[htmltools]{tagHasAttribute}}, \code{\link[htmltools]{tagList}}, \code{\link[htmltools]{tagSetChildren}}, \code{\link[htmltools]{tags}}, \code{\link[htmltools]{validateCssUnit}}, \code{\link[htmltools]{withTags}}}
}}

74
man/shinyAppTemplate.Rd Normal file
View File

@@ -0,0 +1,74 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/app_template.R
\name{shinyAppTemplate}
\alias{shinyAppTemplate}
\title{Generate a Shiny application from a template}
\usage{
shinyAppTemplate(path = NULL, examples = "default")
}
\arguments{
\item{path}{Path to create new shiny application template.}
\item{examples}{Either one of "default", "ask", "all", or any combination of
"app", "rdir", "module", "shinytest", "testthat", and "server". In an
interactive session, "default" falls back to "ask"; in a non-interactive
session, "default" falls back to "all". With "ask", this function will
prompt the user to select which template items will be added to the new app
directory. With "all", all template items will be added to the app
directory.}
}
\description{
This function populates a directory with files for a Shiny application.
}
\details{
In an interactive R session, this function will, by default, prompt the user
which components to add to the application.
The full example application includes the following files and directories:\preformatted{appdir/
|- app.R
|- R
| |- my-module.R
| |-- sort.R
`-- tests
|- server.R
|- server
| |- test-mymodule.R
| `- test-server.R
|- shinytest.R
|- shinytest
| `- mytest.R
|- testthat.R
`-- testthat
|- helper-load.R
`- test-sort.R
}
Some notes about these files:
\itemize{
\item \code{app.R} is the main application file.
\item All files in the \verb{R/} subdirectory are automatically sourced when the
application is run.
\item \code{R/sort.R} and \code{R/my-module.R} are automatically sourced when
the application is run. The first contains a function \code{lexical_sort()},
and the second contains code for a \href{moduleServer()}{Shiny module} which
is used in the application.
\item \verb{tests/} contains various tests for the application. You may
choose to use or remove any of them. They can be executed by the
\code{\link[=runTests]{runTests()}} function.
\item \code{tests/server.R} is a test runner for test files in
\verb{tests/server/}.
\item \code{tests/server/test-mymodule.R} is a test for the module.
\item \code{tests/shinytest.R} is a test runner for test files in the
\verb{tests/shinytest/} directory.
\item \code{tests/shinytest/mytest.R} is a test that uses the
\href{https://rstudio.github.io/shinytest/}{shinytest} package to do
snapshot-based testing.
\item \code{tests/testthat.R} is a test runner for test files in the
\verb{tests/testthat/} directory.
\item \code{tests/testthat/helper-load.R} is a helper script that is automatically
loaded before running \code{test-mymodule.}R. (This is performed by the testthat
package.)
\item \code{tests/testthat/test-sort.R} is a set of tests that use the
\href{https://testthat.r-lib.org/}{testthat} package for testing.
}
}

View File

@@ -168,6 +168,7 @@ reference:
- title: Utility functions
desc: Miscellaneous utilities that may be useful to advanced users or when extending Shiny.
contents:
- shinyAppTemplate
- req
- validate
- session