mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-11 16:08:19 -05:00
Compare commits
18 Commits
bindAfterR
...
docs/tests
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0a8939240 | ||
|
|
6fc06281bd | ||
|
|
f724128d41 | ||
|
|
518ef0f9f8 | ||
|
|
f5b395485e | ||
|
|
31aca7aa70 | ||
|
|
b38a630224 | ||
|
|
1b7709411b | ||
|
|
2b48aa0d91 | ||
|
|
6fdf23752e | ||
|
|
8542f5d017 | ||
|
|
e7b830755a | ||
|
|
23c7b0683a | ||
|
|
5805895581 | ||
|
|
90539bff25 | ||
|
|
62bb21d5b6 | ||
|
|
4f85268d44 | ||
|
|
611e517bb8 |
@@ -111,7 +111,7 @@ Suggests:
|
||||
ragg,
|
||||
showtext,
|
||||
sass
|
||||
URL: https://shiny.rstudio.com/
|
||||
URL: https://shiny.posit.co/
|
||||
BugReports: https://github.com/rstudio/shiny/issues
|
||||
Collate:
|
||||
'globals.R'
|
||||
|
||||
20
NEWS.md
20
NEWS.md
@@ -6,18 +6,30 @@
|
||||
|
||||
### New features and improvements
|
||||
|
||||
* Closed #789: `<script>` loaded from dynamic UI are no longer loaded using synchronous `XMLHttpRequest` (via jQuery). (#3666)
|
||||
* Closed #789: Dynamic UI is now rendered asynchronously, thanks in part to the newly exported `Shiny.renderDependenciesAsync()`, `Shiny.renderHtmlAsync()`, and `Shiny.renderContentAsync()`. Importantly, this means `<script>` tags are now loaded asynchronously (the old way used `XMLHttpRequest`, which is synchronous). In addition, `Shiny` now manages a queue of async tasks (exposed via `Shiny.shinyapp.taskQueue`) so that order of execution is preserved. (#3666)
|
||||
|
||||
* For `reactiveValues()` objects, whenever the `$names()` or `$values()` methods are called, the keys are now returned in the order that they were inserted. (#3774)
|
||||
|
||||
* `Map` objects are now initialized at load time instead of build time. This avoids potential problems that could arise from storing `fastmap` objects into the built Shiny package. (#3775)
|
||||
|
||||
* Closed #3635: `window.Shiny.outputBindings` and `window.Shiny.inputBindings` gain a `onRegister()` method, to register callbacks that execute whenever a new binding is registered. Internally, Shiny uses this to check whether it should re-bind to the DOM when a binding has been registered. (#3638)
|
||||
* Allow for `shiny:::toJSON()` to respect if `digits=` has class `"AsIs"` which represents if `use_signif=` is `TRUE` or `FALSE`. This is useful for testing to keep the digits smaller. For example, setting `options(shiny.json.digits = 4)` will save 4 digits after the decimal, rather than the default of `I(16)` which will save 16 significant digits. (#3819)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fixed #3771: Sometimes the error `ion.rangeSlider.min.js: i.stopPropagation is not a function` would appear in the JavaScript console. (#3772)
|
||||
|
||||
* Fixed #3833: When `width` is provided to `textAreaInput()`, we now correctly set the width of the `<textarea>` element. (#3838)
|
||||
|
||||
* Fixes #3840: `updateSliderInput()` now warns when attempting to set invalid `min`, `max`, or `value` values. Sending an invalid update message to an input no longer causes other update messages to fail. (#3843)
|
||||
|
||||
|
||||
# shiny 1.7.4.1
|
||||
|
||||
## Full changelog
|
||||
|
||||
* Closed #3849: In R-devel, a warning was raised when Shiny was loaded because `as.numeric_version()` was called with a number instead of a string. (#3850)
|
||||
|
||||
|
||||
# shiny 1.7.4
|
||||
|
||||
## Full changelog
|
||||
@@ -167,7 +179,7 @@ This release focuses on improvements in three main areas:
|
||||
|
||||
1. Better theming (and Bootstrap 4) support:
|
||||
* The `theme` argument of `fluidPage()`, `navbarPage()`, and `bootstrapPage()` all now understand `bslib::bs_theme()` objects, which can be used to opt-into Bootstrap 4, use any Bootswatch theme, and/or implement custom themes without writing any CSS.
|
||||
* The `session` object now includes `$setCurrentTheme()` and `$getCurrentTheme()` methods to dynamically update (or obtain) the page's `theme` after initial load, which is useful for things such as [adding a dark mode switch to an app](https://rstudio.github.io/bslib/articles/bslib.html#dynamic) or some other "real-time" theming tool like `bslib::bs_themer()`.
|
||||
* The `session` object now includes `$setCurrentTheme()` and `$getCurrentTheme()` methods to dynamically update (or obtain) the page's `theme` after initial load, which is useful for things such as [adding a dark mode switch to an app](https://rstudio.github.io/bslib/articles/theming.html#dynamic) or some other "real-time" theming tool like `bslib::bs_themer()`.
|
||||
* For more details, see [`{bslib}`'s website](https://rstudio.github.io/bslib/)
|
||||
|
||||
2. Caching of `reactive()` and `render*()` (e.g. `renderText()`, `renderTable()`, etc) expressions.
|
||||
@@ -560,7 +572,7 @@ This is a significant release for Shiny, with a major new feature that was nearl
|
||||
|
||||
* Fixed #1600: URL-encoded bookmarking did not work with sliders that had dates or date-times. (#1961)
|
||||
|
||||
* Fixed #1962: [File dragging and dropping](https://www.rstudio.com/blog/shiny-1-0-4/) broke in the presence of jQuery version 3.0 as introduced by the [rhandsontable](https://jrowen.github.io/rhandsontable/) [htmlwidget](https://www.htmlwidgets.org/). (#2005)
|
||||
* Fixed #1962: [File dragging and dropping](https://posit.co/blog/shiny-1-0-4/) broke in the presence of jQuery version 3.0 as introduced by the [rhandsontable](https://jrowen.github.io/rhandsontable/) [htmlwidget](https://www.htmlwidgets.org/). (#2005)
|
||||
|
||||
* Improved the error handling inside the `addResourcePath()` function, to give end users more informative error messages when the `directoryPath` argument cannot be normalized. This is especially useful for `runtime: shiny_prerendered` Rmd documents, like `learnr` tutorials. (#1968)
|
||||
|
||||
|
||||
@@ -159,8 +159,8 @@ utils::globalVariables(".GenericCallEnv", add = TRUE)
|
||||
#' ```
|
||||
#'
|
||||
#' To use different settings for a session-scoped cache, you can set
|
||||
#' `self$cache` at the top of your server function. By default, it will create
|
||||
#' a 200 MB memory cache for each session, but you can replace it with
|
||||
#' `session$cache` at the top of your server function. By default, it will
|
||||
#' create a 200 MB memory cache for each session, but you can replace it with
|
||||
#' something different. To use the session-scoped cache, you must also call
|
||||
#' `bindCache()` with `cache="session"`. This will create a 100 MB cache for
|
||||
#' the session:
|
||||
|
||||
@@ -374,8 +374,7 @@ collapseSizes <- function(padding) {
|
||||
#' @param inverse `TRUE` to use a dark background and light text for the
|
||||
#' navigation bar
|
||||
#' @param collapsible `TRUE` to automatically collapse the navigation
|
||||
#' elements into a menu when the width of the browser is less than 940 pixels
|
||||
#' (useful for viewing on smaller touchscreen device)
|
||||
#' elements into an expandable menu on mobile devices or narrow window widths.
|
||||
#' @param fluid `TRUE` to use a fluid layout. `FALSE` to use a fixed
|
||||
#' layout.
|
||||
#' @param windowTitle the browser window title (as a character string). The
|
||||
@@ -1200,19 +1199,25 @@ uiOutput <- htmlOutput
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' ui <- fluidPage(
|
||||
#' p("Choose a dataset to download."),
|
||||
#' selectInput("dataset", "Dataset", choices = c("mtcars", "airquality")),
|
||||
#' downloadButton("downloadData", "Download")
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' # Our dataset
|
||||
#' data <- mtcars
|
||||
#' # The requested dataset
|
||||
#' data <- reactive({
|
||||
#' get(input$dataset)
|
||||
#' })
|
||||
#'
|
||||
#' output$downloadData <- downloadHandler(
|
||||
#' filename = function() {
|
||||
#' paste("data-", Sys.Date(), ".csv", sep="")
|
||||
#' # Use the selected dataset as the suggested file name
|
||||
#' paste0(input$dataset, ".csv")
|
||||
#' },
|
||||
#' content = function(file) {
|
||||
#' write.csv(data, file)
|
||||
#' # Write the dataset to the `file` that will be downloaded
|
||||
#' write.csv(data(), file)
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
|
||||
@@ -16,12 +16,6 @@
|
||||
s3_register("knitr::knit_print", "reactive")
|
||||
s3_register("knitr::knit_print", "shiny.appobj")
|
||||
s3_register("knitr::knit_print", "shiny.render.function")
|
||||
|
||||
# Shiny 1.4.0 bumps jQuery 1.x to 3.x, which caused a problem
|
||||
# with static-rendering of htmlwidgets, and htmlwidgets 1.5
|
||||
# includes a fix for this problem
|
||||
# https://github.com/rstudio/shiny/issues/2630
|
||||
register_upgrade_message("htmlwidgets", 1.5)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#' from a list of values.
|
||||
#'
|
||||
#' By default, `selectInput()` and `selectizeInput()` use the JavaScript library
|
||||
#' \pkg{selectize.js} (<https://github.com/selectize/selectize.js>) instead of
|
||||
#' \pkg{selectize.js} (<https://selectize.dev/) instead of
|
||||
#' the basic select input element. To use the standard HTML select input
|
||||
#' element, use `selectInput()` with `selectize=FALSE`.
|
||||
#'
|
||||
@@ -172,7 +172,7 @@ needOptgroup <- function(choices) {
|
||||
|
||||
#' @rdname selectInput
|
||||
#' @param ... Arguments passed to `selectInput()`.
|
||||
#' @param options A list of options. See the documentation of \pkg{selectize.js}
|
||||
#' @param options A list of options. See the documentation of \pkg{selectize.js}(<https://selectize.dev/docs/usage>)
|
||||
#' for possible options (character option values inside [base::I()] will
|
||||
#' be treated as literal JavaScript code; see [renderDataTable()]
|
||||
#' for details).
|
||||
@@ -287,7 +287,7 @@ selectizeStaticDependency <- function(version) {
|
||||
#'
|
||||
#' By default, `varSelectInput()` and `selectizeInput()` use the
|
||||
#' JavaScript library \pkg{selectize.js}
|
||||
#' (<https://github.com/selectize/selectize.js>) to instead of the basic
|
||||
#' (<https://selectize.dev/>) to instead of the basic
|
||||
#' select input element. To use the standard HTML select input element, use
|
||||
#' `selectInput()` with `selectize=FALSE`.
|
||||
#'
|
||||
@@ -383,7 +383,7 @@ varSelectInput <- function(
|
||||
|
||||
#' @rdname varSelectInput
|
||||
#' @param ... Arguments passed to `varSelectInput()`.
|
||||
#' @param options A list of options. See the documentation of \pkg{selectize.js}
|
||||
#' @param options A list of options. See the documentation of \pkg{selectize.js}(<https://selectize.dev/docs/usage>)
|
||||
#' for possible options (character option values inside [base::I()] will
|
||||
#' be treated as literal JavaScript code; see [renderDataTable()]
|
||||
#' for details).
|
||||
|
||||
@@ -52,7 +52,7 @@ textAreaInput <- function(inputId, label, value = "", width = NULL, height = NUL
|
||||
|
||||
style <- css(
|
||||
# The width is specified on the parent div.
|
||||
width = if (!is.null(width)) "width: 100%;",
|
||||
width = if (!is.null(width)) "100%",
|
||||
height = validateCssUnit(height),
|
||||
resize = resize
|
||||
)
|
||||
|
||||
@@ -2187,8 +2187,8 @@ maskReactiveContext <- function(expr) {
|
||||
#' @param autoDestroy If `TRUE` (the default), the observer will be
|
||||
#' automatically destroyed when its domain (if any) ends.
|
||||
#' @param ignoreNULL Whether the action should be triggered (or value
|
||||
#' calculated, in the case of `eventReactive`) when the input is
|
||||
#' `NULL`. See Details.
|
||||
#' calculated, in the case of `eventReactive`) when the input event expression
|
||||
#' is `NULL`. See Details.
|
||||
#' @param ignoreInit If `TRUE`, then, when this `observeEvent` is
|
||||
#' first created/initialized, ignore the `handlerExpr` (the second
|
||||
#' argument), whether it is otherwise supposed to run or not. The default is
|
||||
|
||||
@@ -33,8 +33,12 @@ createUniqueId <- function(bytes, prefix = "", suffix = "") {
|
||||
}
|
||||
|
||||
toJSON <- function(x, ..., dataframe = "columns", null = "null", na = "null",
|
||||
auto_unbox = TRUE, digits = getOption("shiny.json.digits", 16),
|
||||
use_signif = TRUE, force = TRUE, POSIXt = "ISO8601", UTC = TRUE,
|
||||
auto_unbox = TRUE,
|
||||
# Shiny has had a legacy value of 16 significant digits
|
||||
# We can use `I(16)` mixed with the default behavior in jsonlite's `use_signif=`
|
||||
# https://github.com/jeroen/jsonlite/commit/728efa9
|
||||
digits = getOption("shiny.json.digits", I(16)), use_signif = is(digits, "AsIs"),
|
||||
force = TRUE, POSIXt = "ISO8601", UTC = TRUE,
|
||||
rownames = FALSE, keep_vec_names = TRUE, strict_atomic = TRUE) {
|
||||
|
||||
if (strict_atomic) {
|
||||
|
||||
@@ -48,32 +48,6 @@ is_installed <- function(pkg, version = NULL) {
|
||||
installed && isTRUE(get_package_version(pkg) >= version)
|
||||
}
|
||||
|
||||
register_upgrade_message <- function(pkg, version, error = FALSE) {
|
||||
|
||||
msg <- sprintf(
|
||||
"This version of '%s' is designed to work with '%s' >= %s.
|
||||
Please upgrade via install.packages('%s').",
|
||||
environmentName(environment(register_upgrade_message)),
|
||||
pkg, version, pkg
|
||||
)
|
||||
|
||||
cond <- if (error) stop else packageStartupMessage
|
||||
|
||||
if (pkg %in% loadedNamespaces() && !is_installed(pkg, version)) {
|
||||
cond(msg)
|
||||
}
|
||||
|
||||
# Always register hook in case pkg is loaded at some
|
||||
# point the future (or, potentially, but less commonly,
|
||||
# unloaded & reloaded)
|
||||
setHook(
|
||||
packageEvent(pkg, "onLoad"),
|
||||
function(...) {
|
||||
if (!is_installed(pkg, version)) cond(msg)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# Simplified version rlang:::s3_register() that just uses
|
||||
# warning() instead of rlang::warn() when registration fails
|
||||
# https://github.com/r-lib/rlang/blob/main/R/compat-s3-register.R
|
||||
@@ -190,11 +164,9 @@ system_file <- function(..., package = "base") {
|
||||
normalizePath(files, winslash = "/")
|
||||
}
|
||||
|
||||
# A wrapper for `system.file()`, which caches the results, because
|
||||
# `system.file()` can be slow. Note that because of caching, if
|
||||
# `system_file_cached()` is called on a package that isn't installed, then the
|
||||
# package is installed, and then `system_file_cached()` is called again, it will
|
||||
# still return "".
|
||||
# A wrapper for `system.file()`, which caches the package path because
|
||||
# `system.file()` can be slow. If a package is not installed, the result won't
|
||||
# be cached.
|
||||
system_file_cached <- local({
|
||||
pkg_dir_cache <- character()
|
||||
|
||||
@@ -206,7 +178,9 @@ system_file_cached <- local({
|
||||
not_cached <- is.na(match(package, names(pkg_dir_cache)))
|
||||
if (not_cached) {
|
||||
pkg_dir <- system.file(package = package)
|
||||
pkg_dir_cache[[package]] <<- pkg_dir
|
||||
if (nzchar(pkg_dir)) {
|
||||
pkg_dir_cache[[package]] <<- pkg_dir
|
||||
}
|
||||
} else {
|
||||
pkg_dir <- pkg_dir_cache[[package]]
|
||||
}
|
||||
|
||||
@@ -37,29 +37,43 @@
|
||||
#'
|
||||
#'
|
||||
#' # Testing a module --------------------------------------------------------
|
||||
#' myModuleServer <- function(id, multiplier = 2, prefix = "I am ") {
|
||||
#' # Testing the server function doesn't require a UI, but we've included it
|
||||
#' # here for completeness. In this simple app, a user clicks a button to
|
||||
#' # multiply a value by the module's multiplier argument. In the tests below,
|
||||
#' # we'll make sure the value is 1, 2, 4, etc. with each button click.
|
||||
#' multModuleUI <- function(id) {
|
||||
#' ns <- NS(id)
|
||||
#' tagList(
|
||||
#' textOutput(ns("txt")),
|
||||
#' actionButton(ns("multiply_it"), "Multiply It")
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' multModuleServer <- function(id, multiplier = 2) {
|
||||
#' moduleServer(id, function(input, output, session) {
|
||||
#' myreactive <- reactive({
|
||||
#' input$x * multiplier
|
||||
#' the_value <- reactive({
|
||||
#' max(input$multiply_it * multiplier, 1)
|
||||
#' })
|
||||
#' output$txt <- renderText({
|
||||
#' paste0(prefix, myreactive())
|
||||
#' paste("The value is", the_value())
|
||||
#' })
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' testServer(myModuleServer, args = list(multiplier = 2), {
|
||||
#' session$setInputs(x = 1)
|
||||
#' # You're also free to use third-party
|
||||
#' # testing packages like testthat:
|
||||
#' # expect_equal(myreactive(), 2)
|
||||
#' stopifnot(myreactive() == 2)
|
||||
#' stopifnot(output$txt == "I am 2")
|
||||
#' testServer(multModuleServer, args = list(multiplier = 2), {
|
||||
#' # Set the initial button value to 0
|
||||
#' session$setInputs(multiply_it = 0)
|
||||
#' stopifnot(the_value() == 1)
|
||||
#' stopifnot(output$txt == "The value is 1")
|
||||
#'
|
||||
#' session$setInputs(x = 2)
|
||||
#' stopifnot(myreactive() == 4)
|
||||
#' stopifnot(output$txt == "I am 4")
|
||||
#' # Any additional arguments, below, are passed along to the module.
|
||||
#' # Simulate two button clicks
|
||||
#' session$setInputs(multiply_it = 2)
|
||||
#' stopifnot(the_value() == 4)
|
||||
#' stopifnot(output$txt == "The value is 4")
|
||||
#'
|
||||
#' # Note: you're also free to use third-party
|
||||
#' # testing packages like testthat:
|
||||
#' # expect_equal(myreactive(), 1)
|
||||
#' })
|
||||
#' @export
|
||||
testServer <- function(app = NULL, expr, args = list(), session = MockShinySession$new()) {
|
||||
|
||||
@@ -423,6 +423,23 @@ updateSliderInput <- function(session = getDefaultReactiveDomain(), inputId, lab
|
||||
{
|
||||
validate_session_object(session)
|
||||
|
||||
if (!is.null(value)) {
|
||||
if (!is.null(min) && !is.null(max)) {
|
||||
# Validate value/min/max together if all three are provided
|
||||
tryCatch(
|
||||
validate_slider_value(min, max, value, "updateSliderInput"),
|
||||
error = function(err) warning(conditionMessage(err), call. = FALSE)
|
||||
)
|
||||
} else if (length(value) < 1 || length(value) > 2 || any(is.na(value))) {
|
||||
# Otherwise ensure basic assumptions about value are met
|
||||
warning(
|
||||
"In updateSliderInput(): value must be a single value or a length-2 ",
|
||||
"vector and cannot contain NA values.",
|
||||
call. = FALSE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
# If no min/max/value is provided, we won't know the
|
||||
# type, and this will return an empty string
|
||||
dataType <- getSliderType(min, max, value)
|
||||
|
||||
@@ -4,7 +4,7 @@ NULL
|
||||
|
||||
# @staticimports pkg:staticimports
|
||||
# is_installed get_package_version system_file
|
||||
# s3_register register_upgrade_message
|
||||
# s3_register
|
||||
# any_named any_unnamed
|
||||
|
||||
#' Make a random number generator repeatable
|
||||
|
||||
@@ -16,7 +16,7 @@ Easily build rich and productive interactive web apps in R — no HTML/CSS/J
|
||||
* A prebuilt set of highly sophisticated, customizable, and easy-to-use widgets (e.g., plots, tables, sliders, dropdowns, date pickers, and more).
|
||||
* An attractive default look based on [Bootstrap](https://getbootstrap.com/) which can also be easily customized with the [bslib](https://github.com/rstudio/bslib) package or avoided entirely with more direct R bindings to HTML/CSS/JavaScript.
|
||||
* Seamless integration with [R Markdown](https://shiny.rstudio.com/articles/interactive-docs.html), making it easy to embed numerous applications natively within a larger dynamic document.
|
||||
* Tools for improving and monitoring performance, including native support for [async programming](https://www.rstudio.com/blog/shiny-1-1-0/), [caching](https://talks.cpsievert.me/20201117), [load testing](https://rstudio.github.io/shinyloadtest/), and more.
|
||||
* Tools for improving and monitoring performance, including native support for [async programming](https://posit.co/blog/shiny-1-1-0/), [caching](https://talks.cpsievert.me/20201117), [load testing](https://rstudio.github.io/shinyloadtest/), and more.
|
||||
* [Modules](https://shiny.rstudio.com/articles/modules.html): a framework for reducing code duplication and complexity.
|
||||
* An ability to [bookmark application state](https://shiny.rstudio.com/articles/bookmarking-state.html) and/or [generate code to reproduce output(s)](https://github.com/rstudio/shinymeta).
|
||||
* A rich ecosystem of extension packages for more [custom widgets](http://www.htmlwidgets.org/), [input validation](https://github.com/rstudio/shinyvalidate), [unit testing](https://github.com/rstudio/shinytest), and more.
|
||||
@@ -45,6 +45,10 @@ For more examples and inspiration, check out the [Shiny User Gallery](https://sh
|
||||
|
||||
For help with learning fundamental Shiny programming concepts, check out the [Mastering Shiny](https://mastering-shiny.org/) book and the [Shiny Tutorial](https://shiny.rstudio.com/tutorial/). The former is currently more up-to-date with modern Shiny features, whereas the latter takes a deeper, more visual, dive into fundamental concepts.
|
||||
|
||||
## Join the conversation
|
||||
|
||||
If you want to chat about Shiny, meet other developers, or help us decide what to work on next, [join us on Discord](https://discord.gg/yMGCamUMnS).
|
||||
|
||||
## Getting Help
|
||||
|
||||
To ask a question about Shiny, please use the [RStudio Community website](https://community.rstudio.com/new-topic?category=shiny&tags=shiny).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
2
inst/www/shared/shiny.min.js
vendored
2
inst/www/shared/shiny.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -166,8 +166,8 @@ instead of the default 200 MB:
|
||||
}\if{html}{\out{</div>}}
|
||||
|
||||
To use different settings for a session-scoped cache, you can set
|
||||
\code{self$cache} at the top of your server function. By default, it will create
|
||||
a 200 MB memory cache for each session, but you can replace it with
|
||||
\code{session$cache} at the top of your server function. By default, it will
|
||||
create a 200 MB memory cache for each session, but you can replace it with
|
||||
something different. To use the session-scoped cache, you must also call
|
||||
\code{bindCache()} with \code{cache="session"}. This will create a 100 MB cache for
|
||||
the session:
|
||||
|
||||
@@ -36,19 +36,25 @@ function.
|
||||
\examples{
|
||||
\dontrun{
|
||||
ui <- fluidPage(
|
||||
p("Choose a dataset to download."),
|
||||
selectInput("dataset", "Dataset", choices = c("mtcars", "airquality")),
|
||||
downloadButton("downloadData", "Download")
|
||||
)
|
||||
|
||||
server <- function(input, output) {
|
||||
# Our dataset
|
||||
data <- mtcars
|
||||
# The requested dataset
|
||||
data <- reactive({
|
||||
get(input$dataset)
|
||||
})
|
||||
|
||||
output$downloadData <- downloadHandler(
|
||||
filename = function() {
|
||||
paste("data-", Sys.Date(), ".csv", sep="")
|
||||
# Use the selected dataset as the suggested file name
|
||||
paste0(input$dataset, ".csv")
|
||||
},
|
||||
content = function(file) {
|
||||
write.csv(data, file)
|
||||
# Write the dataset to the `file` that will be downloaded
|
||||
write.csv(data(), file)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -58,8 +58,7 @@ tabPanels}
|
||||
navigation bar}
|
||||
|
||||
\item{collapsible}{\code{TRUE} to automatically collapse the navigation
|
||||
elements into a menu when the width of the browser is less than 940 pixels
|
||||
(useful for viewing on smaller touchscreen device)}
|
||||
elements into an expandable menu on mobile devices or narrow window widths.}
|
||||
|
||||
\item{fluid}{\code{TRUE} to use a fluid layout. \code{FALSE} to use a fixed
|
||||
layout.}
|
||||
|
||||
@@ -86,8 +86,8 @@ Positive, negative, and zero values are allowed.}
|
||||
automatically destroyed when its domain (if any) ends.}
|
||||
|
||||
\item{ignoreNULL}{Whether the action should be triggered (or value
|
||||
calculated, in the case of \code{eventReactive}) when the input is
|
||||
\code{NULL}. See Details.}
|
||||
calculated, in the case of \code{eventReactive}) when the input event expression
|
||||
is \code{NULL}. See Details.}
|
||||
|
||||
\item{ignoreInit}{If \code{TRUE}, then, when this \code{observeEvent} is
|
||||
first created/initialized, ignore the \code{handlerExpr} (the second
|
||||
|
||||
@@ -48,7 +48,7 @@ but when \code{size} is set, it will be a box instead.}
|
||||
|
||||
\item{...}{Arguments passed to \code{selectInput()}.}
|
||||
|
||||
\item{options}{A list of options. See the documentation of \pkg{selectize.js}
|
||||
\item{options}{A list of options. See the documentation of \pkg{selectize.js}(\url{https://selectize.dev/docs/usage})
|
||||
for possible options (character option values inside \code{\link[base:AsIs]{base::I()}} will
|
||||
be treated as literal JavaScript code; see \code{\link[=renderDataTable]{renderDataTable()}}
|
||||
for details).}
|
||||
@@ -62,7 +62,7 @@ from a list of values.
|
||||
}
|
||||
\details{
|
||||
By default, \code{selectInput()} and \code{selectizeInput()} use the JavaScript library
|
||||
\pkg{selectize.js} (\url{https://github.com/selectize/selectize.js}) instead of
|
||||
\pkg{selectize.js} (<https://selectize.dev/) instead of
|
||||
the basic select input element. To use the standard HTML select input
|
||||
element, use \code{selectInput()} with \code{selectize=FALSE}.
|
||||
|
||||
|
||||
@@ -47,28 +47,42 @@ testServer(server, {
|
||||
|
||||
|
||||
# Testing a module --------------------------------------------------------
|
||||
myModuleServer <- function(id, multiplier = 2, prefix = "I am ") {
|
||||
# Testing the server function doesn't require a UI, but we've included it
|
||||
# here for completeness. In this simple app, a user clicks a button to
|
||||
# multiply a value by the module's multiplier argument. In the tests below,
|
||||
# we'll make sure the value is 1, 2, 4, etc. with each button click.
|
||||
multModuleUI <- function(id) {
|
||||
ns <- NS(id)
|
||||
tagList(
|
||||
textOutput(ns("txt")),
|
||||
actionButton(ns("multiply_it"), "Multiply It")
|
||||
)
|
||||
}
|
||||
|
||||
multModuleServer <- function(id, multiplier = 2) {
|
||||
moduleServer(id, function(input, output, session) {
|
||||
myreactive <- reactive({
|
||||
input$x * multiplier
|
||||
the_value <- reactive({
|
||||
max(input$multiply_it * multiplier, 1)
|
||||
})
|
||||
output$txt <- renderText({
|
||||
paste0(prefix, myreactive())
|
||||
paste("The value is", the_value())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
testServer(myModuleServer, args = list(multiplier = 2), {
|
||||
session$setInputs(x = 1)
|
||||
# You're also free to use third-party
|
||||
# testing packages like testthat:
|
||||
# expect_equal(myreactive(), 2)
|
||||
stopifnot(myreactive() == 2)
|
||||
stopifnot(output$txt == "I am 2")
|
||||
testServer(multModuleServer, args = list(multiplier = 2), {
|
||||
# Set the initial button value to 0
|
||||
session$setInputs(multiply_it = 0)
|
||||
stopifnot(the_value() == 1)
|
||||
stopifnot(output$txt == "The value is 1")
|
||||
|
||||
session$setInputs(x = 2)
|
||||
stopifnot(myreactive() == 4)
|
||||
stopifnot(output$txt == "I am 4")
|
||||
# Any additional arguments, below, are passed along to the module.
|
||||
# Simulate two button clicks
|
||||
session$setInputs(multiply_it = 2)
|
||||
stopifnot(the_value() == 4)
|
||||
stopifnot(output$txt == "The value is 4")
|
||||
|
||||
# Note: you're also free to use third-party
|
||||
# testing packages like testthat:
|
||||
# expect_equal(myreactive(), 1)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ the example section for a small demo of this feature.}
|
||||
\item{selected}{The initially selected value (or multiple values if \code{multiple = TRUE}). If not specified then defaults to the first value for
|
||||
single-select lists and no values for multiple select lists.}
|
||||
|
||||
\item{options}{A list of options. See the documentation of \pkg{selectize.js}
|
||||
\item{options}{A list of options. See the documentation of \pkg{selectize.js}(\url{https://selectize.dev/docs/usage})
|
||||
for possible options (character option values inside \code{\link[base:AsIs]{base::I()}} will
|
||||
be treated as literal JavaScript code; see \code{\link[=renderDataTable]{renderDataTable()}}
|
||||
for details).}
|
||||
|
||||
@@ -42,7 +42,7 @@ but when \code{size} is set, it will be a box instead.}
|
||||
|
||||
\item{...}{Arguments passed to \code{varSelectInput()}.}
|
||||
|
||||
\item{options}{A list of options. See the documentation of \pkg{selectize.js}
|
||||
\item{options}{A list of options. See the documentation of \pkg{selectize.js}(\url{https://selectize.dev/docs/usage})
|
||||
for possible options (character option values inside \code{\link[base:AsIs]{base::I()}} will
|
||||
be treated as literal JavaScript code; see \code{\link[=renderDataTable]{renderDataTable()}}
|
||||
for details).}
|
||||
@@ -57,7 +57,7 @@ from the column names of a data frame.
|
||||
\details{
|
||||
By default, \code{varSelectInput()} and \code{selectizeInput()} use the
|
||||
JavaScript library \pkg{selectize.js}
|
||||
(\url{https://github.com/selectize/selectize.js}) to instead of the basic
|
||||
(\url{https://selectize.dev/}) to instead of the basic
|
||||
select input element. To use the standard HTML select input element, use
|
||||
\code{selectInput()} with \code{selectize=FALSE}.
|
||||
}
|
||||
|
||||
@@ -197,7 +197,16 @@ class SliderInputBinding extends TextInputBindingBase {
|
||||
msg.to = data.value[1];
|
||||
} else {
|
||||
if (Array.isArray(data.value)) {
|
||||
throw "Slider only contains a single value and cannot be updated with an array";
|
||||
const errorReason = [
|
||||
"an empty array.",
|
||||
"a single-value array.",
|
||||
"an array with more than two values.",
|
||||
];
|
||||
throw (
|
||||
"Slider requires two values to update with an array, " +
|
||||
"but message value was " +
|
||||
errorReason[Math.min(data.value.length, 2)]
|
||||
);
|
||||
}
|
||||
msg.from = data.value;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { mergeSort } from "../utils";
|
||||
import { Callbacks } from "../utils/callbacks";
|
||||
|
||||
interface BindingBase {
|
||||
name: string;
|
||||
@@ -15,7 +14,6 @@ class BindingRegistry<Binding extends BindingBase> {
|
||||
name!: string;
|
||||
bindings: Array<BindingObj<Binding>> = [];
|
||||
bindingNames: { [key: string]: BindingObj<Binding> } = {};
|
||||
registerCallbacks: Callbacks = new Callbacks();
|
||||
|
||||
register(binding: Binding, bindingName: string, priority = 0): void {
|
||||
const bindingObj = { binding, priority };
|
||||
@@ -25,12 +23,6 @@ class BindingRegistry<Binding extends BindingBase> {
|
||||
this.bindingNames[bindingName] = bindingObj;
|
||||
binding.name = bindingName;
|
||||
}
|
||||
|
||||
this.registerCallbacks.invoke();
|
||||
}
|
||||
|
||||
onRegister(fn: () => void, once = true): void {
|
||||
this.registerCallbacks.register(fn, once);
|
||||
}
|
||||
|
||||
setPriority(bindingName: string, priority: number): void {
|
||||
|
||||
@@ -22,7 +22,7 @@ class InputBatchSender implements InputPolicy {
|
||||
if (opts.priority === "event") {
|
||||
this._sendNow();
|
||||
} else if (!this.sendIsEnqueued) {
|
||||
this.shinyapp.actionQueue.enqueue(() => {
|
||||
this.shinyapp.taskQueue.enqueue(() => {
|
||||
this.sendIsEnqueued = false;
|
||||
this._sendNow();
|
||||
});
|
||||
|
||||
@@ -150,24 +150,6 @@ function initShiny(windowShiny: Shiny): void {
|
||||
(x) => x.value
|
||||
);
|
||||
|
||||
// When future bindings are registered, rebind to the DOM once the current
|
||||
// event loop is done. This is necessary since the binding might be registered
|
||||
// after Shiny has already bound to the DOM (#3635)
|
||||
let enqueuedCount = 0;
|
||||
const enqueueRebind = () => {
|
||||
enqueuedCount++;
|
||||
windowShiny.shinyapp?.actionQueue.enqueue(() => {
|
||||
enqueuedCount--;
|
||||
// If this function is scheduled more than once in the queue, don't do anything.
|
||||
// Only do the bindAll when we get to the last instance of this function in the queue.
|
||||
if (enqueuedCount === 0) {
|
||||
windowShiny.bindAll?.(document.documentElement);
|
||||
}
|
||||
});
|
||||
};
|
||||
inputBindings.onRegister(enqueueRebind, false);
|
||||
outputBindings.onRegister(enqueueRebind, false);
|
||||
|
||||
// The server needs to know the size of each image and plot output element,
|
||||
// in case it is auto-sizing
|
||||
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
|
||||
|
||||
@@ -114,7 +114,7 @@ class ShinyApp {
|
||||
// without overlapping. This is used for handling incoming messages from the
|
||||
// server and scheduling outgoing messages to the server, and can be used for
|
||||
// other things tasks as well.
|
||||
actionQueue = new AsyncQueue<() => Promise<void> | void>();
|
||||
taskQueue = new AsyncQueue<() => Promise<void> | void>();
|
||||
|
||||
config: {
|
||||
workerId: string;
|
||||
@@ -240,7 +240,7 @@ class ShinyApp {
|
||||
this.startActionQueueLoop();
|
||||
};
|
||||
socket.onmessage = (e) => {
|
||||
this.actionQueue.enqueue(async () => await this.dispatchMessage(e.data));
|
||||
this.taskQueue.enqueue(async () => await this.dispatchMessage(e.data));
|
||||
};
|
||||
// Called when a successfully-opened websocket is closed, or when an
|
||||
// attempt to open a connection fails.
|
||||
@@ -266,7 +266,7 @@ class ShinyApp {
|
||||
async startActionQueueLoop(): Promise<void> {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const action = await this.actionQueue.dequeue();
|
||||
const action = await this.taskQueue.dequeue();
|
||||
|
||||
try {
|
||||
await action();
|
||||
@@ -725,8 +725,16 @@ class ShinyApp {
|
||||
evt.message = message[i].message;
|
||||
evt.binding = inputBinding;
|
||||
$(el).trigger(evt);
|
||||
if (!evt.isDefaultPrevented())
|
||||
inputBinding.receiveMessage(el, evt.message);
|
||||
if (!evt.isDefaultPrevented()) {
|
||||
try {
|
||||
inputBinding.receiveMessage(el, evt.message);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[shiny] Error in inputBinding.receiveMessage()",
|
||||
{ error, binding: inputBinding, message: evt.message }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
type Cb = {
|
||||
once: boolean;
|
||||
fn: () => void;
|
||||
};
|
||||
|
||||
type Cbs = {
|
||||
[key: string]: Cb;
|
||||
};
|
||||
|
||||
class Callbacks {
|
||||
callbacks: Cbs = {};
|
||||
id = 0;
|
||||
|
||||
register(fn: () => void, once = true): () => void {
|
||||
this.id += 1;
|
||||
const id = this.id;
|
||||
|
||||
this.callbacks[id] = { fn, once };
|
||||
return () => {
|
||||
delete this.callbacks[id];
|
||||
};
|
||||
}
|
||||
|
||||
invoke(): void {
|
||||
for (const id in this.callbacks) {
|
||||
const cb = this.callbacks[id];
|
||||
|
||||
try {
|
||||
cb.fn();
|
||||
} finally {
|
||||
if (cb.once) delete this.callbacks[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.callbacks = {};
|
||||
}
|
||||
|
||||
count(): number {
|
||||
return Object.keys(this.callbacks).length;
|
||||
}
|
||||
}
|
||||
|
||||
export { Callbacks };
|
||||
3
srcts/types/src/bindings/registry.d.ts
vendored
3
srcts/types/src/bindings/registry.d.ts
vendored
@@ -1,4 +1,3 @@
|
||||
import { Callbacks } from "../utils/callbacks";
|
||||
interface BindingBase {
|
||||
name: string;
|
||||
}
|
||||
@@ -13,9 +12,7 @@ declare class BindingRegistry<Binding extends BindingBase> {
|
||||
bindingNames: {
|
||||
[key: string]: BindingObj<Binding>;
|
||||
};
|
||||
registerCallbacks: Callbacks;
|
||||
register(binding: Binding, bindingName: string, priority?: number): void;
|
||||
onRegister(fn: () => void, once?: boolean): void;
|
||||
setPriority(bindingName: string, priority: number): void;
|
||||
getPriority(bindingName: string): number | false;
|
||||
getBindings(): Array<BindingObj<Binding>>;
|
||||
|
||||
2
srcts/types/src/shiny/shinyapp.d.ts
vendored
2
srcts/types/src/shiny/shinyapp.d.ts
vendored
@@ -20,7 +20,7 @@ type MessageValue = Parameters<WebSocket["send"]>[0];
|
||||
declare function addCustomMessageHandler(type: string, handler: Handler): void;
|
||||
declare class ShinyApp {
|
||||
$socket: ShinyWebSocket | null;
|
||||
actionQueue: AsyncQueue<() => Promise<void> | void>;
|
||||
taskQueue: AsyncQueue<() => Promise<void> | void>;
|
||||
config: {
|
||||
workerId: string;
|
||||
sessionId: string;
|
||||
|
||||
16
srcts/types/src/utils/callbacks.d.ts
vendored
16
srcts/types/src/utils/callbacks.d.ts
vendored
@@ -1,16 +0,0 @@
|
||||
type Cb = {
|
||||
once: boolean;
|
||||
fn: () => void;
|
||||
};
|
||||
type Cbs = {
|
||||
[key: string]: Cb;
|
||||
};
|
||||
declare class Callbacks {
|
||||
callbacks: Cbs;
|
||||
id: number;
|
||||
register(fn: () => void, once?: boolean): () => void;
|
||||
invoke(): void;
|
||||
clear(): void;
|
||||
count(): number;
|
||||
}
|
||||
export { Callbacks };
|
||||
Submodule tests/testthat/apps deleted from c471e6449e
@@ -268,3 +268,27 @@ test_that("quoToFunction handles nested quosures", {
|
||||
func <- quoToFunction(quo_outer, "foo")
|
||||
expect_identical(func(), 2)
|
||||
})
|
||||
|
||||
|
||||
|
||||
test_that("toJSON can set digits using options - default", {
|
||||
withr::local_options(list(shiny.json.digits = NULL))
|
||||
expect_equal(
|
||||
as.character(toJSON(pi)),
|
||||
"[3.141592653589793]"
|
||||
)
|
||||
})
|
||||
test_that("toJSON can set digits using options - number", {
|
||||
withr::local_options(list(shiny.json.digits = 4))
|
||||
expect_equal(
|
||||
as.character(toJSON(pi)),
|
||||
"[3.1416]"
|
||||
)
|
||||
})
|
||||
test_that("toJSON can set digits using options - asis number", {
|
||||
withr::local_options(list(shiny.json.digits = I(4)))
|
||||
expect_equal(
|
||||
as.character(toJSON(pi)),
|
||||
"[3.142]"
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user