From 2405a7491abcf462bb69ad722bc801e3c7b32fde Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Mon, 4 Nov 2019 02:39:00 -0500 Subject: [PATCH 01/23] :sparkles: support inline clientside callbacks --- R/dash.R | 33 +++++++++++++++++++++++++++++++++ R/utils.R | 17 +++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/R/dash.R b/R/dash.R index b13ea8d4..14d56607 100644 --- a/R/dash.R +++ b/R/dash.R @@ -599,6 +599,31 @@ Dash <- R6::R6Class( if (is.function(func)) { clientside_function <- NULL + } else if (is.character(func)) { + + # if namespace not provided, default to clientside + namespace <- "_dashprivate_clientside" + + # the following line assigns the name clientside_XX where XX is the count of + # existing clientside functions, which guarantees a unique name + fn_name <- paste0("_dashprivate_clientside", + sum(lengths( + vapply(private$callback_map, `[`, list(1), "clientside_function")) > 0) + 1) + + # hashing the name while keeping it a valid JS function name + fn_name <- paste0("_", digest(fn_name, + "md5", + serialize = FALSE)) + + clientside_function <- list(namespace = namespace, + function_name = fn_name) + + # register the function with its hashed name representation + private$js_map <- insertIntoJSMap(private$js_map, + func, + fn_name) + + func <- NULL } else { clientside_function <- func func <- NULL @@ -1227,6 +1252,13 @@ Dash <- R6::R6Class( "application/javascript", "var renderer = new DashRenderer();") + # if inline JS provided, include + if (!(is.null(private$js_map))) { + scripts_inline <- generate_js_inline(private$js_map) + } else { + scripts_inline <- NULL + } + # serving order of CSS and JS tags: package -> external -> assets css_tags <- paste(c(css_deps, css_external, @@ -1236,6 +1268,7 @@ Dash <- R6::R6Class( scripts_tags <- paste(c(scripts_deps, scripts_external, scripts_assets, + scripts_inline, scripts_invoke_renderer), collapse = "\n") diff --git a/R/utils.R b/R/utils.R index 595ef4d7..78a15b01 100644 --- a/R/utils.R +++ b/R/utils.R @@ -607,6 +607,18 @@ generate_js_dist_html <- function(href, } } +generate_js_inline <- function(js_map) { + inlineJSFormatted <- paste0("") +} + # This function takes the list object containing asset paths # for all stylesheets and scripts, as well as the URL path # to search, then returns the absolute local path (when @@ -1135,3 +1147,8 @@ dashLogger <- function(event = NULL, clientsideFunction <- function(namespace, function_name) { return(list(namespace=namespace, function_name=function_name)) } + +insertIntoJSMap <- function(map, func, name) { + map[[name]] <- func + return(map) +} From 94919330f8ca2da672afcdb928ee16fd9db481ff Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Mon, 4 Nov 2019 13:03:09 -0500 Subject: [PATCH 02/23] :hammer: improve handling of inline callbacks --- R/dash.R | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/R/dash.R b/R/dash.R index 14d56607..9dbe00c1 100644 --- a/R/dash.R +++ b/R/dash.R @@ -593,22 +593,18 @@ Dash <- R6::R6Class( # ------------------------------------------------------------------------ callback = function(output, params, func) { assert_valid_callbacks(output, params, func) - + inputs <- params[vapply(params, function(x) 'input' %in% attr(x, "class"), FUN.VALUE=logical(1))] state <- params[vapply(params, function(x) 'state' %in% attr(x, "class"), FUN.VALUE=logical(1))] - + if (is.function(func)) { clientside_function <- NULL } else if (is.character(func)) { - + # if namespace not provided, default to clientside namespace <- "_dashprivate_clientside" - # the following line assigns the name clientside_XX where XX is the count of - # existing clientside functions, which guarantees a unique name - fn_name <- paste0("_dashprivate_clientside", - sum(lengths( - vapply(private$callback_map, `[`, list(1), "clientside_function")) > 0) + 1) + fn_name <- paste0("_dashprivate_clientside_", createCallbackId(output)) # hashing the name while keeping it a valid JS function name fn_name <- paste0("_", digest(fn_name, @@ -619,16 +615,25 @@ Dash <- R6::R6Class( function_name = fn_name) # register the function with its hashed name representation - private$js_map <- insertIntoJSMap(private$js_map, - func, - fn_name) + js_map <- insertIntoJSMap(private$js_map, + func, + fn_name) + + # check that no entries in the inline JS map have names matching those in the + # callback map, and vice-versas + if (length(intersect(names(private$callback_map), + sub("_dashprivate_clientside_", "", names(private$js_map)))) > 0) { + stop(sprintf("One or more outputs are duplicated across callbacks. Please ensure that all ID and property combinations are unique."), call. = FALSE) + } else { + private$js_map <- js_map + } func <- NULL } else { clientside_function <- func func <- NULL } - + # register the callback_map private$callback_map <- insertIntoCallbackMap(private$callback_map, inputs, @@ -637,7 +642,7 @@ Dash <- R6::R6Class( func, clientside_function) }, - + # ------------------------------------------------------------------------ # request and return callback context # ------------------------------------------------------------------------ From 6704d8c036ffd2d76e52e65fec4f6f565a072167 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Mon, 6 Apr 2020 23:30:39 -0400 Subject: [PATCH 03/23] :hammer: refactor inline script handling --- R/dash.R | 60 ++++++++++++++++++++++--------------------------------- R/utils.R | 30 +++++++++++----------------- 2 files changed, 36 insertions(+), 54 deletions(-) diff --git a/R/dash.R b/R/dash.R index 23ec3cbe..1c748fd4 100644 --- a/R/dash.R +++ b/R/dash.R @@ -87,7 +87,9 @@ #' \describe{ #' \item{output}{a named list including a component `id` and `property`} #' \item{params}{an unnamed list of [input] and [state] statements, each with defined `id` and `property`} -#' \item{func}{any valid R function which generates [output] provided [input] and/or [state] arguments, or a call to [clientsideFunction] including `namespace` and `function_name` arguments for a locally served JavaScript function} +#' \item{func}{any valid R function which generates [output] provided [input] and/or [state] arguments, +#' a character string containing valid JavaScript, or a call to [clientsideFunction] including `namespace` +#' and `function_name` arguments for a locally served JavaScript function} #' } #' The `output` argument defines which layout component property should #' receive the results (via the [output] object). The events that @@ -779,34 +781,24 @@ Dash <- R6::R6Class( if (is.function(func)) { clientside_function <- NULL } else if (is.character(func)) { - - # if namespace not provided, default to clientside - namespace <- "_dashprivate_clientside" - - fn_name <- paste0("_dashprivate_clientside_", createCallbackId(output)) - - # hashing the name while keeping it a valid JS function name - fn_name <- paste0("_", digest(fn_name, - "md5", - serialize = FALSE)) - - clientside_function <- list(namespace = namespace, - function_name = fn_name) - - # register the function with its hashed name representation - js_map <- insertIntoJSMap(private$js_map, - func, - fn_name) - - # check that no entries in the inline JS map have names matching those in the - # callback map, and vice-versas - if (length(intersect(names(private$callback_map), - sub("_dashprivate_clientside_", "", names(private$js_map)))) > 0) { - stop(sprintf("One or more outputs are duplicated across callbacks. Please ensure that all ID and property combinations are unique."), call. = FALSE) - } else { - private$js_map <- js_map - } - + # update the scripts before generating tags, and remove exact + # duplicates from inline_scripts + fn_name <- paste0("_dashprivate_", output$id) + + func <- paste0('') + + private$inline_scripts <- unique(c(private$inline_scripts, func)) + + clientside_function <- clientsideFunction(namespace = fn_name, + function_name = output$property) + func <- NULL } else { clientside_function <- func @@ -1415,6 +1407,9 @@ Dash <- R6::R6Class( # the input/output mapping passed back-and-forth between the client & server callback_map = list(), + # the list of inline scripts passed as strings via (clientside) callbacks + inline_scripts = list(), + # akin to https://github.com/plotly/dash-renderer/blob/master/dash_renderer/__init__.py react_version_enabled= function() { version <- private$dependencies_internal$`react-prod`$version @@ -1584,13 +1579,6 @@ Dash <- R6::R6Class( "application/javascript", "var renderer = new DashRenderer();") - # if inline JS provided, include - if (!(is.null(private$js_map))) { - scripts_inline <- generate_js_inline(private$js_map) - } else { - scripts_inline <- NULL - } - # serving order of CSS and JS tags: package -> external -> assets css_tags <- paste(c(css_deps, css_external, diff --git a/R/utils.R b/R/utils.R index d47f8962..aebe13b3 100644 --- a/R/utils.R +++ b/R/utils.R @@ -592,18 +592,6 @@ generate_js_dist_html <- function(href, } } -generate_js_inline <- function(js_map) { - inlineJSFormatted <- paste0("") -} - generate_meta_tags <- function(metas) { has_ie_compat <- any(vapply(metas, function(x) x$name == "http-equiv" && x$content == "X-UA-Compatible", @@ -1130,6 +1118,8 @@ dashLogger <- function(event = NULL, #' Define a clientside callback #' #' Create a callback that updates the output by calling a clientside (JavaScript) function instead of an R function. +#' Note that it is also possible to specify JavaScript as a character string instead of passing `clientsideFunction`. +#' In this case Dash will inline your JavaScript automatically, without needing to save a script inside `assets`. #' #' @param namespace Character. Describes where the JavaScript function resides (Dash will look #' for the function at `window[namespace][function_name]`.) @@ -1159,16 +1149,20 @@ dashLogger <- function(event = NULL, #' namespace = 'my_clientside_library', #' function_name = 'my_function' #' ) -#' )} +#' ) +#' +#' # Passing JavaScript as a character string +#' app$callback( +#' output('output-clientside', 'children'), +#' params=list(input('input', 'value')), +#' "function (value) { +#' return 'Client says \"' + value + '\"'; +#' }" +#')} clientsideFunction <- function(namespace, function_name) { return(list(namespace=namespace, function_name=function_name)) } -insertIntoJSMap <- function(map, func, name) { - map[[name]] <- func - return(map) -} - buildFingerprint <- function(path, version, hash_value) { path <- file.path(path) filename <- getFileSansExt(path) From 3461fccaa5472ab860e2c9d674543ae24c8e1b40 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Tue, 7 Apr 2020 00:32:32 -0400 Subject: [PATCH 04/23] :books: update docs --- man/Dash.Rd | 38 ++++++++++++++++++++++++++++++++------ man/clientsideFunction.Rd | 13 ++++++++++++- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/man/Dash.Rd b/man/Dash.Rd index bf7e9aa4..5c386713 100644 --- a/man/Dash.Rd +++ b/man/Dash.Rd @@ -101,18 +101,21 @@ The \code{callback} method has three formal arguments: \describe{ \item{output}{a named list including a component \code{id} and \code{property}} \item{params}{an unnamed list of \link{input} and \link{state} statements, each with defined \code{id} and \code{property}} -\item{func}{any valid R function which generates \link{output} provided \link{input} and/or \link{state} arguments, or a call to \link{clientsideFunction} including \code{namespace} and \code{function_name} arguments for a locally served JavaScript function} +\item{func}{any valid R function which generates \link{output} provided \link{input} and/or \link{state} arguments, +a character string containing valid JavaScript, or a call to \link{clientsideFunction} including \code{namespace} +and \code{function_name} arguments for a locally served JavaScript function} } The \code{output} argument defines which layout component property should receive the results (via the \link{output} object). The events that trigger the callback are then described by the \link{input} (and/or \link{state}) object(s) (which should reference layout components), which become argument values for R callback handlers defined in \code{func}. Here \code{func} may -either be an anonymous R function, or a call to \code{clientsideFunction()}, which -describes a locally served JavaScript function instead. The latter defines a -"clientside callback", which updates components without passing data to and +either be an anonymous R function, a JavaScript function provided as a +character string, or a call to \code{clientsideFunction()}, which describes a +locally served JavaScript function instead. The latter two methods define +a "clientside callback", which updates components without passing data to and from the Dash backend. The latter may offer improved performance relative -to callbacks written in R. +to callbacks written purely in R. } \item{\code{title("dash")}}{ The title of the app. If no title is supplied, Dash for R will use 'dash'. @@ -131,6 +134,29 @@ but this is configurable via the \code{prefix} parameter. Note: this method will present a warning and return \code{NULL} if the Dash app was not loaded via \code{source()} if the \code{DASH_APP_PATH} environment variable is undefined. } +\item{\code{get_relative_path(path, requests_pathname_prefix)}}{ +The \code{get_relative_path} method simplifies the handling of URLs and pathnames for apps +running locally and on a deployment server such as Dash Enterprise. It handles the prefix +for requesting assets similar to the \code{get_asset_url} method, but can also be used for URL handling +in components such as \code{dccLink} or \code{dccLocation}. For example, \code{app$get_relative_url("/page/")} +would return \code{/app/page/} for an app running on a deployment server. The path must be prefixed with +a \code{/}. +\describe{ +\item{path}{Character. A path string prefixed with a leading \code{/} which directs at a path or asset directory.} +\item{requests_pathname_prefix}{Character. The pathname prefix for the app on a deployed application. Defaults to the environment variable set by the server, or \code{""} if run locally.} +} +} +\item{\code{strip_relative_path(path, requests_pathname_prefix)}}{ +The \code{strip_relative_path} method simplifies the handling of URLs and pathnames for apps +running locally and on a deployment server such as Dash Enterprise. It acts almost opposite the \code{get_relative_path} +method, by taking a \code{relative path} as an input, and returning the \code{path} stripped of the \code{requests_pathname_prefix}, +and any leading or trailing \code{/}. For example, a path string \code{/app/homepage/}, would be returned as +\code{homepage}. This is particularly useful for \code{dccLocation} URL routing. +\describe{ +\item{path}{Character. A path string prefixed with a leading \code{/} and \code{requests_pathname_prefix} which directs at a path or asset directory.} +\item{requests_pathname_prefix}{Character. The pathname prefix for the app on a deployed application. Defaults to the environment variable set by the server, or \code{""} if run locally.} +} +} \item{\code{index_string(string)}}{ The \code{index_string} method allows the specification of a custom index by changing the default \code{HTML} template that is generated by the Dash UI. Meta tags, CSS, Javascript, @@ -194,7 +220,7 @@ The \code{run_server} method has 13 formal arguments, several of which are optio \item{port}{Integer. Specifies the port number on which the server should listen (default is \code{8050}). Environment variable: \code{PORT}.} \item{block}{Logical. Start the server while blocking console input? Default is \code{TRUE}.} \item{showcase}{Logical. Load the Dash application into the default web browser when server starts? Default is \code{FALSE}.} -\item{use_viewer}{Logical. Load the Dash application into RStudio's viewer pane? Requires that \code{host} is either \code{127.0.0.1} or \code{localhost}, and that Dash application is started within RStudio; if \code{use_viewer = TRUE} and these conditions are not satsified, the user is warned and the app opens in the default browser instead. Default is \code{FALSE}.} +\item{use_viewer}{Logical. Load the Dash application into RStudio's viewer pane? Requires that \code{host} is either \code{127.0.0.1} or \code{localhost}, and that Dash application is started within RStudio; if \code{use_viewer = TRUE} and these conditions are not satisfied, the user is warned and the app opens in the default browser instead. Default is \code{FALSE}.} \item{debug}{Logical. Enable/disable all the dev tools unless overridden by the arguments or environment variables. Default is \code{FALSE} when called via \code{run_server}. Environment variable: \code{DASH_DEBUG}.} \item{dev_tools_ui}{Logical. Show Dash's dev tools UI? Default is \code{TRUE} if \code{debug == TRUE}, \code{FALSE} otherwise. Environment variable: \code{DASH_UI}.} \item{dev_tools_hot_reload}{Logical. Activate hot reloading when app, assets, and component files change? Default is \code{TRUE} if \code{debug == TRUE}, \code{FALSE} otherwise. Requires that the Dash application is loaded using \code{source()}, so that \code{srcref} attributes are available for executed code. Environment variable: \code{DASH_HOT_RELOAD}.} diff --git a/man/clientsideFunction.Rd b/man/clientsideFunction.Rd index a82438db..47d6cb80 100644 --- a/man/clientsideFunction.Rd +++ b/man/clientsideFunction.Rd @@ -14,6 +14,8 @@ for the function at \code{window[namespace][function_name]}.)} } \description{ Create a callback that updates the output by calling a clientside (JavaScript) function instead of an R function. +Note that it is also possible to specify JavaScript as a character string instead of passing \code{clientsideFunction}. +In this case Dash will inline your JavaScript automatically, without needing to save a script inside \code{assets}. } \details{ With this signature, Dash's front-end will call \code{window.my_clientside_library.my_function} with the current @@ -39,5 +41,14 @@ app$callback( namespace = 'my_clientside_library', function_name = 'my_function' ) -)} +) + +# Passing JavaScript as a character string +app$callback( + output('output-clientside', 'children'), + params=list(input('input', 'value')), + "function (value) { + return 'Client says \\"' + value + '\\"'; + }" +)} } From 123463e08e9db85b1532158809d64e84cf9a86be Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Tue, 7 Apr 2020 00:33:20 -0400 Subject: [PATCH 05/23] update roxygen lines in dash.R --- R/dash.R | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/R/dash.R b/R/dash.R index 1c748fd4..e6e46197 100644 --- a/R/dash.R +++ b/R/dash.R @@ -96,11 +96,12 @@ #' trigger the callback are then described by the [input] (and/or [state]) #' object(s) (which should reference layout components), which become #' argument values for R callback handlers defined in `func`. Here `func` may -#' either be an anonymous R function, or a call to `clientsideFunction()`, which -#' describes a locally served JavaScript function instead. The latter defines a -#' "clientside callback", which updates components without passing data to and +#' either be an anonymous R function, a JavaScript function provided as a +#' character string, or a call to `clientsideFunction()`, which describes a +#' locally served JavaScript function instead. The latter two methods define +#' a "clientside callback", which updates components without passing data to and #' from the Dash backend. The latter may offer improved performance relative -#' to callbacks written in R. +#' to callbacks written purely in R. #' } #' \item{`title("dash")`}{ #' The title of the app. If no title is supplied, Dash for R will use 'dash'. From ae9395ee82ccc3b934d65288e1a90a8c3ef39353 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Tue, 7 Apr 2020 00:39:35 -0400 Subject: [PATCH 06/23] fix EOL --- NAMESPACE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NAMESPACE b/NAMESPACE index 6daedb64..e5c7d9ec 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -27,4 +27,4 @@ importFrom(routr,RouteStack) importFrom(routr,ressource_route) importFrom(stats,setNames) importFrom(tools,file_ext) -importFrom(utils,getFromNamespace) \ No newline at end of file +importFrom(utils,getFromNamespace) From a5054ee659fa7915e1df4b6820850300116983f5 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Tue, 7 Apr 2020 00:56:05 -0400 Subject: [PATCH 07/23] :rotating_light: add inline clientside test --- .../clientside/test_clientside_inline.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tests/integration/clientside/test_clientside_inline.py diff --git a/tests/integration/clientside/test_clientside_inline.py b/tests/integration/clientside/test_clientside_inline.py new file mode 100644 index 00000000..1c4437b8 --- /dev/null +++ b/tests/integration/clientside/test_clientside_inline.py @@ -0,0 +1,73 @@ +from selenium.webdriver.support.select import Select +import time, os + +app = """ +library(dash) +library(dashCoreComponents) +library(dashHtmlComponents) + +app <- Dash$new() + +app$layout(htmlDiv(list( + dccInput(id='input'), + htmlDiv(id='output-clientside'), + htmlDiv(id='output-serverside') + ) + ) +) + +app$callback( + output(id = "output-serverside", property = "children"), + params = list( + input(id = "input", property = "value") + ), + function(value) { + sprintf("Server says %s", value) + } +) + +app$callback( + output('output-clientside', 'children'), + params=list(input('input', 'value')), + " + inline: function (value) { + return 'Client says \"' + value + '\"'; + }" +) + +app$run_server() +""" + + +def test_rscc001_clientside(dashr): + os.chdir(os.path.dirname(__file__)) + dashr.start_server(app) + dashr.wait_for_text_to_equal( + '#output-clientside', + 'Client says "undefined"' + ) + dashr.wait_for_text_to_equal( + "#output-serverside", + "Server says NULL" + ) + input1 = dashr.find_element("#input") + dashr.clear_input(input1) + input1.send_keys("Clientside") + dashr.wait_for_text_to_equal( + '#output-clientside', + 'Client says "Clientside"' + ) + dashr.wait_for_text_to_equal( + "#output-serverside", + "Server says Clientside" + ) + dashr.clear_input(input1) + input1.send_keys("Callbacks") + dashr.wait_for_text_to_equal( + '#output-clientside', + 'Client says "Callbacks"' + ) + dashr.wait_for_text_to_equal( + "#output-serverside", + "Server says Callbacks" + ) From f80ba67a4d5bcb485e6fbc3e2bf813f34f330865 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Tue, 7 Apr 2020 01:05:03 -0400 Subject: [PATCH 08/23] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a4e546..3dda2e1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Change Log for Dash for R All notable changes to this project will be documented in this file. +## Unreleased +### Added +- Support for inline clientside callbacks in JavaScript [#140](https://github.com/plotly/dashR/pull/140) ## [0.3.0] - 2020-02-12 ### Added From 78456517e7ab83b94557cb65ff54672047136b2e Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Wed, 8 Apr 2020 18:36:52 -0400 Subject: [PATCH 09/23] :hocho: leading whitespace in clientside --- R/dash.R | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/R/dash.R b/R/dash.R index e6e46197..e130b771 100644 --- a/R/dash.R +++ b/R/dash.R @@ -787,13 +787,12 @@ Dash <- R6::R6Class( fn_name <- paste0("_dashprivate_", output$id) func <- paste0('') + '') private$inline_scripts <- unique(c(private$inline_scripts, func)) From 09c73a2d511f23a4c2bca3bd34370616d7b32ba7 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Wed, 8 Apr 2020 18:37:36 -0400 Subject: [PATCH 10/23] :necktie: remove trailing whitespace --- R/dash.R | 84 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/R/dash.R b/R/dash.R index e130b771..3f80890e 100644 --- a/R/dash.R +++ b/R/dash.R @@ -97,8 +97,8 @@ #' object(s) (which should reference layout components), which become #' argument values for R callback handlers defined in `func`. Here `func` may #' either be an anonymous R function, a JavaScript function provided as a -#' character string, or a call to `clientsideFunction()`, which describes a -#' locally served JavaScript function instead. The latter two methods define +#' character string, or a call to `clientsideFunction()`, which describes a +#' locally served JavaScript function instead. The latter two methods define #' a "clientside callback", which updates components without passing data to and #' from the Dash backend. The latter may offer improved performance relative #' to callbacks written purely in R. @@ -122,8 +122,8 @@ #' } #' \item{`get_relative_path(path, requests_pathname_prefix)`}{ #' The `get_relative_path` method simplifies the handling of URLs and pathnames for apps -#' running locally and on a deployment server such as Dash Enterprise. It handles the prefix -#' for requesting assets similar to the `get_asset_url` method, but can also be used for URL handling +#' running locally and on a deployment server such as Dash Enterprise. It handles the prefix +#' for requesting assets similar to the `get_asset_url` method, but can also be used for URL handling #' in components such as `dccLink` or `dccLocation`. For example, `app$get_relative_url("/page/")` #' would return `/app/page/` for an app running on a deployment server. The path must be prefixed with #' a `/`. @@ -135,8 +135,8 @@ #' The `strip_relative_path` method simplifies the handling of URLs and pathnames for apps #' running locally and on a deployment server such as Dash Enterprise. It acts almost opposite the `get_relative_path` #' method, by taking a `relative path` as an input, and returning the `path` stripped of the `requests_pathname_prefix`, -#' and any leading or trailing `/`. For example, a path string `/app/homepage/`, would be returned as -#' `homepage`. This is particularly useful for `dccLocation` URL routing. +#' and any leading or trailing `/`. For example, a path string `/app/homepage/`, would be returned as +#' `homepage`. This is particularly useful for `dccLocation` URL routing. #' \describe{ #' \item{path}{Character. A path string prefixed with a leading `/` and `requests_pathname_prefix` which directs at a path or asset directory.} #' \item{requests_pathname_prefix}{Character. The pathname prefix for the app on a deployed application. Defaults to the environment variable set by the server, or `""` if run locally.} @@ -186,9 +186,9 @@ #' but offers the ability to change the default components of the Dash index as seen in the example below: #' \preformatted{ #' app$interpolate_index( -#' template_index, -#' metas = "", -#' renderer = renderer, +#' template_index, +#' metas = "", +#' renderer = renderer, #' config = config) #' } #' \describe{ @@ -196,7 +196,7 @@ #' \item{...}{Named List. The unnamed arguments can be passed as individual named lists corresponding to the components #' of the Dash html index. These include the same arguments as those found in the `index_string()` template.} #' } -#' } +#' } #' \item{`run_server(host = Sys.getenv('HOST', "127.0.0.1"), #' port = Sys.getenv('PORT', 8050), block = TRUE, showcase = FALSE, ...)`}{ #' The `run_server` method has 13 formal arguments, several of which are optional: @@ -775,10 +775,10 @@ Dash <- R6::R6Class( # ------------------------------------------------------------------------ callback = function(output, params, func) { assert_valid_callbacks(output, params, func) - + inputs <- params[vapply(params, function(x) 'input' %in% attr(x, "class"), FUN.VALUE=logical(1))] state <- params[vapply(params, function(x) 'state' %in% attr(x, "class"), FUN.VALUE=logical(1))] - + if (is.function(func)) { clientside_function <- NULL } else if (is.character(func)) { @@ -804,7 +804,7 @@ Dash <- R6::R6Class( clientside_function <- func func <- NULL } - + # register the callback_map private$callback_map <- insertIntoCallbackMap(private$callback_map, inputs, @@ -813,7 +813,7 @@ Dash <- R6::R6Class( func, clientside_function) }, - + # ------------------------------------------------------------------------ # request and return callback context # ------------------------------------------------------------------------ @@ -823,13 +823,13 @@ Dash <- R6::R6Class( } private$callback_context_ }, - + # ------------------------------------------------------------------------ # return asset URLs # ------------------------------------------------------------------------ get_asset_url = function(asset_path, prefix = self$config$requests_pathname_prefix) { app_root_path <- Sys.getenv("DASH_APP_PATH") - + if (app_root_path == "" && getAppPath() != FALSE) { # app loaded via source(), root path is known app_root_path <- dirname(private$app_root_path) @@ -838,14 +838,14 @@ Dash <- R6::R6Class( warning("application not started via source(), and DASH_APP_PATH environment variable is undefined. get_asset_url returns NULL since root path cannot be reliably identified.") return(NULL) } - - asset <- lapply(private$asset_map, + + asset <- lapply(private$asset_map, function(x) { # asset_path should be prepended with the full app root & assets path # if leading slash(es) present in asset_path, remove them before # assembling full asset path asset_path <- file.path(app_root_path, - private$assets_folder, + private$assets_folder, sub(pattern="^/+", replacement="", asset_path)) @@ -853,37 +853,37 @@ Dash <- R6::R6Class( } ) asset <- unlist(asset, use.names = FALSE) - + if (length(asset) == 0) stop(sprintf("the asset path '%s' is not valid; please verify that this path exists within the '%s' directory.", asset_path, private$assets_folder)) - + # strip multiple slashes if present, since we'll # introduce one when we concatenate the prefix and # asset path & prepend the asset name with route prefix return(gsub(pattern="/+", replacement="/", - paste(prefix, - private$assets_url_path, - asset, + paste(prefix, + private$assets_url_path, + asset, sep="/"))) }, - + # ------------------------------------------------------------------------ # return relative asset URLs # ------------------------------------------------------------------------ - + get_relative_path = function(path, requests_pathname_prefix = self$config$requests_pathname_prefix) { asset = get_relative_path(requests_pathname = requests_pathname_prefix, path = path) return(asset) }, - - + + # ------------------------------------------------------------------------ # return relative asset URLs # ------------------------------------------------------------------------ - + strip_relative_path = function(path, requests_pathname_prefix = self$config$requests_pathname_prefix) { asset = strip_relative_path(requests_pathname = requests_pathname_prefix, path = path) return(asset) @@ -894,24 +894,24 @@ Dash <- R6::R6Class( index_string = function(string) { private$custom_index <- validate_keys(string) }, - + # ------------------------------------------------------------------------ - # modify the templated variables by using the `interpolate_index` method. + # modify the templated variables by using the `interpolate_index` method. # ------------------------------------------------------------------------ interpolate_index = function(template_index = private$template_index[[1]], ...) { template = template_index kwargs <- list(...) - + for (name in names(kwargs)) { key = paste0('\\{\\%', name, '\\%\\}') template = sub(key, kwargs[[name]], template) - } - + } + invisible(validate_keys(names(kwargs))) - + private$template_index <- template }, - + # ------------------------------------------------------------------------ # specify a custom title # ------------------------------------------------------------------------ @@ -919,7 +919,7 @@ Dash <- R6::R6Class( assertthat::assert_that(is.character(string)) private$name <- string }, - + # ------------------------------------------------------------------------ # convenient fiery wrappers # ------------------------------------------------------------------------ @@ -1480,7 +1480,7 @@ Dash <- R6::R6Class( depsAll <- compact(c( private$react_deps()[private$react_versions() %in% private$react_version_enabled()], private$dependencies_internal[grepl(pattern = "prop-types", x = private$dependencies_internal)], - private$dependencies_internal[grepl(pattern = "polyfill", x = private$dependencies_internal)], + private$dependencies_internal[grepl(pattern = "polyfill", x = private$dependencies_internal)], private$dependencies, private$dependencies_user, private$dependencies_internal[grepl(pattern = "dash-renderer", x = private$dependencies_internal)] @@ -1616,7 +1616,7 @@ Dash <- R6::R6Class( # insert meta tags if present meta_tags <- all_tags[["meta_tags"]] - + # define the react-entry-point app_entry <- "
Loading...
" # define the dash default config key @@ -1624,13 +1624,13 @@ Dash <- R6::R6Class( if (is.null(private$name)) private$name <- 'dash' - + if (!is.null(private$custom_index)) { string_index <- glue::glue(private$custom_index, .open = "{%", .close = "%}") - + private$.index <- string_index } - + else if (length(private$template_index) == 1) { private$.index <- private$template_index } From 989643a8ff68829b8729211416c0d471a5df7a93 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Wed, 8 Apr 2020 18:38:22 -0400 Subject: [PATCH 11/23] :hocho: trailing whitespace in utils.R --- R/utils.R | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/R/utils.R b/R/utils.R index aebe13b3..d380c4f3 100644 --- a/R/utils.R +++ b/R/utils.R @@ -467,7 +467,7 @@ resolvePrefix <- function(prefix, environment_var, base_pathname) { prefix_env <- Sys.getenv(environment_var) env_base_pathname <- Sys.getenv("DASH_URL_BASE_PATHNAME") app_name <- Sys.getenv("DASH_APP_NAME") - + if (prefix_env != "") return(prefix_env) else if (app_name != "") @@ -1150,7 +1150,7 @@ dashLogger <- function(event = NULL, #' function_name = 'my_function' #' ) #' ) -#' +#' #' # Passing JavaScript as a character string #' app$callback( #' output('output-clientside', 'children'), @@ -1158,7 +1158,7 @@ dashLogger <- function(event = NULL, #' "function (value) { #' return 'Client says \"' + value + '\"'; #' }" -#')} +#')} clientsideFunction <- function(namespace, function_name) { return(list(namespace=namespace, function_name=function_name)) } @@ -1285,8 +1285,8 @@ tryCompress <- function(request, response) { get_relative_path <- function(requests_pathname, path) { # Returns a path with the config setting 'requests_pathname_prefix' prefixed to # it. This is particularly useful for apps deployed on Dash Enterprise, which makes - # it easier to serve apps under both URL prefixes and localhost. - + # it easier to serve apps under both URL prefixes and localhost. + if (requests_pathname == "/" && path == "") { return("/") } @@ -1306,7 +1306,7 @@ get_relative_path <- function(requests_pathname, path) { strip_relative_path <- function(requests_pathname, path) { # Returns a relative path with the `requests_pathname_prefix` and leadings and trailing # slashes stripped from it. This function is particularly relevant to dccLocation pathname routing. - + if (is.null(path)) { return(NULL) } @@ -1327,27 +1327,27 @@ strip_relative_path <- function(requests_pathname, path) { interpolate_str <- function(index_template, ...) { # This function takes an index string, along with # user specified keys for the html keys of the index - # and sets the default values of the keys to the + # and sets the default values of the keys to the # ones specified by the keys themselves, returning - # the custom index template. - template = index_template + # the custom index template. + template = index_template kwargs <- list(...) - + for (name in names(kwargs)) { key = paste0('\\{', name, '\\}') - + template = sub(key, kwargs[[name]], template) - } + } return(template) } validate_keys <- function(string) { required_keys <- c("app_entry", "config", "scripts") - + keys_present <- vapply(required_keys, function(x) grepl(x, string), logical(1)) - + if (!all(keys_present)) { - stop(sprintf("Did you forget to include %s in your index string?", + stop(sprintf("Did you forget to include %s in your index string?", paste(names(keys_present[keys_present==FALSE]), collapse = ", "))) } else { return(string) From e73b7e4951f6cd0bb1166c04aa9451c9429151a7 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Thu, 9 Apr 2020 13:32:11 -0400 Subject: [PATCH 12/23] Revert "fix EOL", see if test passes This reverts commit ae9395ee82ccc3b934d65288e1a90a8c3ef39353. --- NAMESPACE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NAMESPACE b/NAMESPACE index e5c7d9ec..6daedb64 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -27,4 +27,4 @@ importFrom(routr,RouteStack) importFrom(routr,ressource_route) importFrom(stats,setNames) importFrom(tools,file_ext) -importFrom(utils,getFromNamespace) +importFrom(utils,getFromNamespace) \ No newline at end of file From 3b4dc7e69fd99d1486e36db7e5f081be606ed397 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Tue, 21 Apr 2020 00:38:30 -0400 Subject: [PATCH 13/23] try to fix CI error --- R/dash.R | 2 +- man/Dash.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/dash.R b/R/dash.R index 3f80890e..b1926b79 100644 --- a/R/dash.R +++ b/R/dash.R @@ -23,7 +23,7 @@ #' #' @section Arguments: #' \tabular{lll}{ -#' `name` \tab \tab Character. The name of the Dash application (placed in the `` +#' `name` \tab \tab Character. The name of the Dash application (placed in the title #' of the HTML page). DEPRECATED; please use `index_string()` or `interpolate_index()` instead.\cr #' `server` \tab \tab The web server used to power the application. #' Must be a [fiery::Fire] object.\cr diff --git a/man/Dash.Rd b/man/Dash.Rd index 5c386713..f827b75b 100644 --- a/man/Dash.Rd +++ b/man/Dash.Rd @@ -33,7 +33,7 @@ suppress_callback_exceptions = FALSE \section{Arguments}{ \tabular{lll}{ -\code{name} \tab \tab Character. The name of the Dash application (placed in the \code{<title>} +\code{name} \tab \tab Character. The name of the Dash application (placed in the title of the HTML page). DEPRECATED; please use \code{index_string()} or \code{interpolate_index()} instead.\cr \code{server} \tab \tab The web server used to power the application. Must be a \link[fiery:Fire]{fiery::Fire} object.\cr From 83e746269b8f4ad97458f98aa49dcc5e82e7c0f5 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle <ryan@plot.ly> Date: Tue, 21 Apr 2020 00:57:01 -0400 Subject: [PATCH 14/23] try to pass CI --- R/dash.R | 108 +++++++++++++++++++++---------------------------------- 1 file changed, 41 insertions(+), 67 deletions(-) diff --git a/R/dash.R b/R/dash.R index b1926b79..479f6910 100644 --- a/R/dash.R +++ b/R/dash.R @@ -87,21 +87,18 @@ #' \describe{ #' \item{output}{a named list including a component `id` and `property`} #' \item{params}{an unnamed list of [input] and [state] statements, each with defined `id` and `property`} -#' \item{func}{any valid R function which generates [output] provided [input] and/or [state] arguments, -#' a character string containing valid JavaScript, or a call to [clientsideFunction] including `namespace` -#' and `function_name` arguments for a locally served JavaScript function} +#' \item{func}{any valid R function which generates [output] provided [input] and/or [state] arguments, or a call to [clientsideFunction] including `namespace` and `function_name` arguments for a locally served JavaScript function} #' } #' The `output` argument defines which layout component property should #' receive the results (via the [output] object). The events that #' trigger the callback are then described by the [input] (and/or [state]) #' object(s) (which should reference layout components), which become #' argument values for R callback handlers defined in `func`. Here `func` may -#' either be an anonymous R function, a JavaScript function provided as a -#' character string, or a call to `clientsideFunction()`, which describes a -#' locally served JavaScript function instead. The latter two methods define -#' a "clientside callback", which updates components without passing data to and +#' either be an anonymous R function, or a call to `clientsideFunction()`, which +#' describes a locally served JavaScript function instead. The latter defines a +#' "clientside callback", which updates components without passing data to and #' from the Dash backend. The latter may offer improved performance relative -#' to callbacks written purely in R. +#' to callbacks written in R. #' } #' \item{`title("dash")`}{ #' The title of the app. If no title is supplied, Dash for R will use 'dash'. @@ -122,8 +119,8 @@ #' } #' \item{`get_relative_path(path, requests_pathname_prefix)`}{ #' The `get_relative_path` method simplifies the handling of URLs and pathnames for apps -#' running locally and on a deployment server such as Dash Enterprise. It handles the prefix -#' for requesting assets similar to the `get_asset_url` method, but can also be used for URL handling +#' running locally and on a deployment server such as Dash Enterprise. It handles the prefix +#' for requesting assets similar to the `get_asset_url` method, but can also be used for URL handling #' in components such as `dccLink` or `dccLocation`. For example, `app$get_relative_url("/page/")` #' would return `/app/page/` for an app running on a deployment server. The path must be prefixed with #' a `/`. @@ -135,8 +132,8 @@ #' The `strip_relative_path` method simplifies the handling of URLs and pathnames for apps #' running locally and on a deployment server such as Dash Enterprise. It acts almost opposite the `get_relative_path` #' method, by taking a `relative path` as an input, and returning the `path` stripped of the `requests_pathname_prefix`, -#' and any leading or trailing `/`. For example, a path string `/app/homepage/`, would be returned as -#' `homepage`. This is particularly useful for `dccLocation` URL routing. +#' and any leading or trailing `/`. For example, a path string `/app/homepage/`, would be returned as +#' `homepage`. This is particularly useful for `dccLocation` URL routing. #' \describe{ #' \item{path}{Character. A path string prefixed with a leading `/` and `requests_pathname_prefix` which directs at a path or asset directory.} #' \item{requests_pathname_prefix}{Character. The pathname prefix for the app on a deployed application. Defaults to the environment variable set by the server, or `""` if run locally.} @@ -186,9 +183,9 @@ #' but offers the ability to change the default components of the Dash index as seen in the example below: #' \preformatted{ #' app$interpolate_index( -#' template_index, -#' metas = "<meta_charset='UTF-8'/>", -#' renderer = renderer, +#' template_index, +#' metas = "<meta_charset='UTF-8'/>", +#' renderer = renderer, #' config = config) #' } #' \describe{ @@ -196,7 +193,7 @@ #' \item{...}{Named List. The unnamed arguments can be passed as individual named lists corresponding to the components #' of the Dash html index. These include the same arguments as those found in the `index_string()` template.} #' } -#' } +#' } #' \item{`run_server(host = Sys.getenv('HOST', "127.0.0.1"), #' port = Sys.getenv('PORT', 8050), block = TRUE, showcase = FALSE, ...)`}{ #' The `run_server` method has 13 formal arguments, several of which are optional: @@ -781,25 +778,6 @@ Dash <- R6::R6Class( if (is.function(func)) { clientside_function <- NULL - } else if (is.character(func)) { - # update the scripts before generating tags, and remove exact - # duplicates from inline_scripts - fn_name <- paste0("_dashprivate_", output$id) - - func <- paste0('<script>\n', - 'var clientside = window.dash_clientside = window.dash_clientside || {};\n', - 'var ns = clientside["', fn_name, '"] = clientside["', fn_name, '"] || {}\n', - 'ns["', output$property, '"] =\n', - func, - '\n;', - '</script>') - - private$inline_scripts <- unique(c(private$inline_scripts, func)) - - clientside_function <- clientsideFunction(namespace = fn_name, - function_name = output$property) - - func <- NULL } else { clientside_function <- func func <- NULL @@ -823,13 +801,13 @@ Dash <- R6::R6Class( } private$callback_context_ }, - + # ------------------------------------------------------------------------ # return asset URLs # ------------------------------------------------------------------------ get_asset_url = function(asset_path, prefix = self$config$requests_pathname_prefix) { app_root_path <- Sys.getenv("DASH_APP_PATH") - + if (app_root_path == "" && getAppPath() != FALSE) { # app loaded via source(), root path is known app_root_path <- dirname(private$app_root_path) @@ -838,14 +816,14 @@ Dash <- R6::R6Class( warning("application not started via source(), and DASH_APP_PATH environment variable is undefined. get_asset_url returns NULL since root path cannot be reliably identified.") return(NULL) } - - asset <- lapply(private$asset_map, + + asset <- lapply(private$asset_map, function(x) { # asset_path should be prepended with the full app root & assets path # if leading slash(es) present in asset_path, remove them before # assembling full asset path asset_path <- file.path(app_root_path, - private$assets_folder, + private$assets_folder, sub(pattern="^/+", replacement="", asset_path)) @@ -853,37 +831,37 @@ Dash <- R6::R6Class( } ) asset <- unlist(asset, use.names = FALSE) - + if (length(asset) == 0) stop(sprintf("the asset path '%s' is not valid; please verify that this path exists within the '%s' directory.", asset_path, private$assets_folder)) - + # strip multiple slashes if present, since we'll # introduce one when we concatenate the prefix and # asset path & prepend the asset name with route prefix return(gsub(pattern="/+", replacement="/", - paste(prefix, - private$assets_url_path, - asset, + paste(prefix, + private$assets_url_path, + asset, sep="/"))) }, - + # ------------------------------------------------------------------------ # return relative asset URLs # ------------------------------------------------------------------------ - + get_relative_path = function(path, requests_pathname_prefix = self$config$requests_pathname_prefix) { asset = get_relative_path(requests_pathname = requests_pathname_prefix, path = path) return(asset) }, - - + + # ------------------------------------------------------------------------ # return relative asset URLs # ------------------------------------------------------------------------ - + strip_relative_path = function(path, requests_pathname_prefix = self$config$requests_pathname_prefix) { asset = strip_relative_path(requests_pathname = requests_pathname_prefix, path = path) return(asset) @@ -894,24 +872,24 @@ Dash <- R6::R6Class( index_string = function(string) { private$custom_index <- validate_keys(string) }, - + # ------------------------------------------------------------------------ - # modify the templated variables by using the `interpolate_index` method. + # modify the templated variables by using the `interpolate_index` method. # ------------------------------------------------------------------------ interpolate_index = function(template_index = private$template_index[[1]], ...) { template = template_index kwargs <- list(...) - + for (name in names(kwargs)) { key = paste0('\\{\\%', name, '\\%\\}') template = sub(key, kwargs[[name]], template) - } - + } + invisible(validate_keys(names(kwargs))) - + private$template_index <- template }, - + # ------------------------------------------------------------------------ # specify a custom title # ------------------------------------------------------------------------ @@ -919,7 +897,7 @@ Dash <- R6::R6Class( assertthat::assert_that(is.character(string)) private$name <- string }, - + # ------------------------------------------------------------------------ # convenient fiery wrappers # ------------------------------------------------------------------------ @@ -1407,9 +1385,6 @@ Dash <- R6::R6Class( # the input/output mapping passed back-and-forth between the client & server callback_map = list(), - # the list of inline scripts passed as strings via (clientside) callbacks - inline_scripts = list(), - # akin to https://github.com/plotly/dash-renderer/blob/master/dash_renderer/__init__.py react_version_enabled= function() { version <- private$dependencies_internal$`react-prod`$version @@ -1480,7 +1455,7 @@ Dash <- R6::R6Class( depsAll <- compact(c( private$react_deps()[private$react_versions() %in% private$react_version_enabled()], private$dependencies_internal[grepl(pattern = "prop-types", x = private$dependencies_internal)], - private$dependencies_internal[grepl(pattern = "polyfill", x = private$dependencies_internal)], + private$dependencies_internal[grepl(pattern = "polyfill", x = private$dependencies_internal)], private$dependencies, private$dependencies_user, private$dependencies_internal[grepl(pattern = "dash-renderer", x = private$dependencies_internal)] @@ -1588,7 +1563,6 @@ Dash <- R6::R6Class( scripts_tags <- paste(c(scripts_deps, scripts_external, scripts_assets, - scripts_inline, scripts_invoke_renderer), collapse = "\n ") @@ -1616,7 +1590,7 @@ Dash <- R6::R6Class( # insert meta tags if present meta_tags <- all_tags[["meta_tags"]] - + # define the react-entry-point app_entry <- "<div id='react-entry-point'><div class='_dash-loading'>Loading...</div></div>" # define the dash default config key @@ -1624,13 +1598,13 @@ Dash <- R6::R6Class( if (is.null(private$name)) private$name <- 'dash' - + if (!is.null(private$custom_index)) { string_index <- glue::glue(private$custom_index, .open = "{%", .close = "%}") - + private$.index <- string_index } - + else if (length(private$template_index) == 1) { private$.index <- private$template_index } From adba18b0f36a172fe3c7e7a1e00164e8e6a035a5 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle <ryan@plot.ly> Date: Tue, 21 Apr 2020 02:03:56 -0400 Subject: [PATCH 15/23] try editing documentation only first --- R/dash.R | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/R/dash.R b/R/dash.R index 479f6910..d2aa8473 100644 --- a/R/dash.R +++ b/R/dash.R @@ -23,7 +23,7 @@ #' #' @section Arguments: #' \tabular{lll}{ -#' `name` \tab \tab Character. The name of the Dash application (placed in the title +#' `name` \tab \tab Character. The name of the Dash application (placed in the `<title>` #' of the HTML page). DEPRECATED; please use `index_string()` or `interpolate_index()` instead.\cr #' `server` \tab \tab The web server used to power the application. #' Must be a [fiery::Fire] object.\cr @@ -87,18 +87,21 @@ #' \describe{ #' \item{output}{a named list including a component `id` and `property`} #' \item{params}{an unnamed list of [input] and [state] statements, each with defined `id` and `property`} -#' \item{func}{any valid R function which generates [output] provided [input] and/or [state] arguments, or a call to [clientsideFunction] including `namespace` and `function_name` arguments for a locally served JavaScript function} +#' \item{func}{any valid R function which generates [output] provided [input] and/or [state] arguments, +#' a character string containing valid JavaScript, or a call to [clientsideFunction] including `namespace` +#' and `function_name` arguments for a locally served JavaScript function} #' } #' The `output` argument defines which layout component property should #' receive the results (via the [output] object). The events that #' trigger the callback are then described by the [input] (and/or [state]) #' object(s) (which should reference layout components), which become #' argument values for R callback handlers defined in `func`. Here `func` may -#' either be an anonymous R function, or a call to `clientsideFunction()`, which -#' describes a locally served JavaScript function instead. The latter defines a -#' "clientside callback", which updates components without passing data to and +#' either be an anonymous R function, a JavaScript function provided as a +#' character string, or a call to `clientsideFunction()`, which describes a +#' locally served JavaScript function instead. The latter two methods define +#' a "clientside callback", which updates components without passing data to and #' from the Dash backend. The latter may offer improved performance relative -#' to callbacks written in R. +#' to callbacks written purely in R. #' } #' \item{`title("dash")`}{ #' The title of the app. If no title is supplied, Dash for R will use 'dash'. @@ -119,8 +122,8 @@ #' } #' \item{`get_relative_path(path, requests_pathname_prefix)`}{ #' The `get_relative_path` method simplifies the handling of URLs and pathnames for apps -#' running locally and on a deployment server such as Dash Enterprise. It handles the prefix -#' for requesting assets similar to the `get_asset_url` method, but can also be used for URL handling +#' running locally and on a deployment server such as Dash Enterprise. It handles the prefix +#' for requesting assets similar to the `get_asset_url` method, but can also be used for URL handling #' in components such as `dccLink` or `dccLocation`. For example, `app$get_relative_url("/page/")` #' would return `/app/page/` for an app running on a deployment server. The path must be prefixed with #' a `/`. @@ -132,8 +135,8 @@ #' The `strip_relative_path` method simplifies the handling of URLs and pathnames for apps #' running locally and on a deployment server such as Dash Enterprise. It acts almost opposite the `get_relative_path` #' method, by taking a `relative path` as an input, and returning the `path` stripped of the `requests_pathname_prefix`, -#' and any leading or trailing `/`. For example, a path string `/app/homepage/`, would be returned as -#' `homepage`. This is particularly useful for `dccLocation` URL routing. +#' and any leading or trailing `/`. For example, a path string `/app/homepage/`, would be returned as +#' `homepage`. This is particularly useful for `dccLocation` URL routing. #' \describe{ #' \item{path}{Character. A path string prefixed with a leading `/` and `requests_pathname_prefix` which directs at a path or asset directory.} #' \item{requests_pathname_prefix}{Character. The pathname prefix for the app on a deployed application. Defaults to the environment variable set by the server, or `""` if run locally.} @@ -183,9 +186,9 @@ #' but offers the ability to change the default components of the Dash index as seen in the example below: #' \preformatted{ #' app$interpolate_index( -#' template_index, -#' metas = "<meta_charset='UTF-8'/>", -#' renderer = renderer, +#' template_index, +#' metas = "<meta_charset='UTF-8'/>", +#' renderer = renderer, #' config = config) #' } #' \describe{ @@ -193,7 +196,7 @@ #' \item{...}{Named List. The unnamed arguments can be passed as individual named lists corresponding to the components #' of the Dash html index. These include the same arguments as those found in the `index_string()` template.} #' } -#' } +#' } #' \item{`run_server(host = Sys.getenv('HOST', "127.0.0.1"), #' port = Sys.getenv('PORT', 8050), block = TRUE, showcase = FALSE, ...)`}{ #' The `run_server` method has 13 formal arguments, several of which are optional: From 975121cb120ea6e10350eabd64fe38aebfb1dc3f Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle <ryan@plot.ly> Date: Tue, 21 Apr 2020 02:10:40 -0400 Subject: [PATCH 16/23] callback registration --- R/dash.R | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/R/dash.R b/R/dash.R index d2aa8473..440f440e 100644 --- a/R/dash.R +++ b/R/dash.R @@ -781,6 +781,25 @@ Dash <- R6::R6Class( if (is.function(func)) { clientside_function <- NULL + } else if (is.character(func)) { + # update the scripts before generating tags, and remove exact + # duplicates from inline_scripts + fn_name <- paste0("_dashprivate_", output$id) + + func <- paste0('<script>\n', + 'var clientside = window.dash_clientside = window.dash_clientside || {};\n', + 'var ns = clientside["', fn_name, '"] = clientside["', fn_name, '"] || {}\n', + 'ns["', output$property, '"] =\n', + func, + '\n;', + '</script>') + + private$inline_scripts <- unique(c(private$inline_scripts, func)) + + clientside_function <- clientsideFunction(namespace = fn_name, + function_name = output$property) + + func <- NULL } else { clientside_function <- func func <- NULL @@ -1388,6 +1407,9 @@ Dash <- R6::R6Class( # the input/output mapping passed back-and-forth between the client & server callback_map = list(), + # the list of line scripts passed as strings via clientside callbacks + inline_scripts = list(), + # akin to https://github.com/plotly/dash-renderer/blob/master/dash_renderer/__init__.py react_version_enabled= function() { version <- private$dependencies_internal$`react-prod`$version @@ -1458,7 +1480,7 @@ Dash <- R6::R6Class( depsAll <- compact(c( private$react_deps()[private$react_versions() %in% private$react_version_enabled()], private$dependencies_internal[grepl(pattern = "prop-types", x = private$dependencies_internal)], - private$dependencies_internal[grepl(pattern = "polyfill", x = private$dependencies_internal)], + private$dependencies_internal[grepl(pattern = "polyfill", x = private$dependencies_internal)], private$dependencies, private$dependencies_user, private$dependencies_internal[grepl(pattern = "dash-renderer", x = private$dependencies_internal)] @@ -1566,6 +1588,7 @@ Dash <- R6::R6Class( scripts_tags <- paste(c(scripts_deps, scripts_external, scripts_assets, + scripts_inline, scripts_invoke_renderer), collapse = "\n ") From a40c6159a7a748b8dd601755b74573e7e28c454c Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle <ryan@plot.ly> Date: Tue, 21 Apr 2020 13:35:30 -0400 Subject: [PATCH 17/23] :hammer: try to resolve JS issue --- R/dash.R | 78 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/R/dash.R b/R/dash.R index 440f440e..c0bb8073 100644 --- a/R/dash.R +++ b/R/dash.R @@ -23,7 +23,7 @@ #' #' @section Arguments: #' \tabular{lll}{ -#' `name` \tab \tab Character. The name of the Dash application (placed in the `<title>` +#' `name` \tab \tab Character. The name of the Dash application (placed in the title #' of the HTML page). DEPRECATED; please use `index_string()` or `interpolate_index()` instead.\cr #' `server` \tab \tab The web server used to power the application. #' Must be a [fiery::Fire] object.\cr @@ -87,21 +87,18 @@ #' \describe{ #' \item{output}{a named list including a component `id` and `property`} #' \item{params}{an unnamed list of [input] and [state] statements, each with defined `id` and `property`} -#' \item{func}{any valid R function which generates [output] provided [input] and/or [state] arguments, -#' a character string containing valid JavaScript, or a call to [clientsideFunction] including `namespace` -#' and `function_name` arguments for a locally served JavaScript function} +#' \item{func}{any valid R function which generates [output] provided [input] and/or [state] arguments, or a call to [clientsideFunction] including `namespace` and `function_name` arguments for a locally served JavaScript function} #' } #' The `output` argument defines which layout component property should #' receive the results (via the [output] object). The events that #' trigger the callback are then described by the [input] (and/or [state]) #' object(s) (which should reference layout components), which become #' argument values for R callback handlers defined in `func`. Here `func` may -#' either be an anonymous R function, a JavaScript function provided as a -#' character string, or a call to `clientsideFunction()`, which describes a -#' locally served JavaScript function instead. The latter two methods define -#' a "clientside callback", which updates components without passing data to and +#' either be an anonymous R function, or a call to `clientsideFunction()`, which +#' describes a locally served JavaScript function instead. The latter defines a +#' "clientside callback", which updates components without passing data to and #' from the Dash backend. The latter may offer improved performance relative -#' to callbacks written purely in R. +#' to callbacks written in R. #' } #' \item{`title("dash")`}{ #' The title of the app. If no title is supplied, Dash for R will use 'dash'. @@ -789,9 +786,9 @@ Dash <- R6::R6Class( func <- paste0('<script>\n', 'var clientside = window.dash_clientside = window.dash_clientside || {};\n', 'var ns = clientside["', fn_name, '"] = clientside["', fn_name, '"] || {}\n', - 'ns["', output$property, '"] =\n', + 'ns["', output$property, '"] = {\n', func, - '\n;', + '\n};', '</script>') private$inline_scripts <- unique(c(private$inline_scripts, func)) @@ -823,13 +820,13 @@ Dash <- R6::R6Class( } private$callback_context_ }, - + # ------------------------------------------------------------------------ # return asset URLs # ------------------------------------------------------------------------ get_asset_url = function(asset_path, prefix = self$config$requests_pathname_prefix) { app_root_path <- Sys.getenv("DASH_APP_PATH") - + if (app_root_path == "" && getAppPath() != FALSE) { # app loaded via source(), root path is known app_root_path <- dirname(private$app_root_path) @@ -838,14 +835,14 @@ Dash <- R6::R6Class( warning("application not started via source(), and DASH_APP_PATH environment variable is undefined. get_asset_url returns NULL since root path cannot be reliably identified.") return(NULL) } - - asset <- lapply(private$asset_map, + + asset <- lapply(private$asset_map, function(x) { # asset_path should be prepended with the full app root & assets path # if leading slash(es) present in asset_path, remove them before # assembling full asset path asset_path <- file.path(app_root_path, - private$assets_folder, + private$assets_folder, sub(pattern="^/+", replacement="", asset_path)) @@ -853,37 +850,37 @@ Dash <- R6::R6Class( } ) asset <- unlist(asset, use.names = FALSE) - + if (length(asset) == 0) stop(sprintf("the asset path '%s' is not valid; please verify that this path exists within the '%s' directory.", asset_path, private$assets_folder)) - + # strip multiple slashes if present, since we'll # introduce one when we concatenate the prefix and # asset path & prepend the asset name with route prefix return(gsub(pattern="/+", replacement="/", - paste(prefix, - private$assets_url_path, - asset, + paste(prefix, + private$assets_url_path, + asset, sep="/"))) }, - + # ------------------------------------------------------------------------ # return relative asset URLs # ------------------------------------------------------------------------ - + get_relative_path = function(path, requests_pathname_prefix = self$config$requests_pathname_prefix) { asset = get_relative_path(requests_pathname = requests_pathname_prefix, path = path) return(asset) }, - - + + # ------------------------------------------------------------------------ # return relative asset URLs # ------------------------------------------------------------------------ - + strip_relative_path = function(path, requests_pathname_prefix = self$config$requests_pathname_prefix) { asset = strip_relative_path(requests_pathname = requests_pathname_prefix, path = path) return(asset) @@ -894,24 +891,24 @@ Dash <- R6::R6Class( index_string = function(string) { private$custom_index <- validate_keys(string) }, - + # ------------------------------------------------------------------------ - # modify the templated variables by using the `interpolate_index` method. + # modify the templated variables by using the `interpolate_index` method. # ------------------------------------------------------------------------ interpolate_index = function(template_index = private$template_index[[1]], ...) { template = template_index kwargs <- list(...) - + for (name in names(kwargs)) { key = paste0('\\{\\%', name, '\\%\\}') template = sub(key, kwargs[[name]], template) - } - + } + invisible(validate_keys(names(kwargs))) - + private$template_index <- template }, - + # ------------------------------------------------------------------------ # specify a custom title # ------------------------------------------------------------------------ @@ -919,7 +916,7 @@ Dash <- R6::R6Class( assertthat::assert_that(is.character(string)) private$name <- string }, - + # ------------------------------------------------------------------------ # convenient fiery wrappers # ------------------------------------------------------------------------ @@ -1407,7 +1404,7 @@ Dash <- R6::R6Class( # the input/output mapping passed back-and-forth between the client & server callback_map = list(), - # the list of line scripts passed as strings via clientside callbacks + # the list of inline scripts passed as strings via (clientside) callbacks inline_scripts = list(), # akin to https://github.com/plotly/dash-renderer/blob/master/dash_renderer/__init__.py @@ -1579,6 +1576,9 @@ Dash <- R6::R6Class( "application/javascript", "var renderer = new DashRenderer();") + # add inline tags + scripts_inline <- private$inline_scripts + # serving order of CSS and JS tags: package -> external -> assets css_tags <- paste(c(css_deps, css_external, @@ -1616,7 +1616,7 @@ Dash <- R6::R6Class( # insert meta tags if present meta_tags <- all_tags[["meta_tags"]] - + # define the react-entry-point app_entry <- "<div id='react-entry-point'><div class='_dash-loading'>Loading...</div></div>" # define the dash default config key @@ -1624,13 +1624,13 @@ Dash <- R6::R6Class( if (is.null(private$name)) private$name <- 'dash' - + if (!is.null(private$custom_index)) { string_index <- glue::glue(private$custom_index, .open = "{%", .close = "%}") - + private$.index <- string_index } - + else if (length(private$template_index) == 1) { private$.index <- private$template_index } From b2e578f0be1ff2f81c919b294bccd3dee5809e27 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle <ryan@plot.ly> Date: Tue, 21 Apr 2020 18:14:20 -0400 Subject: [PATCH 18/23] fix clientside --- R/dash.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/dash.R b/R/dash.R index c0bb8073..e1bc0181 100644 --- a/R/dash.R +++ b/R/dash.R @@ -785,10 +785,10 @@ Dash <- R6::R6Class( func <- paste0('<script>\n', 'var clientside = window.dash_clientside = window.dash_clientside || {};\n', - 'var ns = clientside["', fn_name, '"] = clientside["', fn_name, '"] || {}\n', - 'ns["', output$property, '"] = {\n', + 'var ns = clientside["', fn_name, '"] = clientside["', fn_name, '"] || {};\n', + 'ns["', output$property, '"] = \n', func, - '\n};', + '\n;', '</script>') private$inline_scripts <- unique(c(private$inline_scripts, func)) From b7e9c8f0ddf215de9f500cc898704468aa7f7ac9 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle <ryan@plot.ly> Date: Tue, 21 Apr 2020 18:29:17 -0400 Subject: [PATCH 19/23] fix test --- tests/integration/clientside/test_clientside_inline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/clientside/test_clientside_inline.py b/tests/integration/clientside/test_clientside_inline.py index 1c4437b8..07d15ca4 100644 --- a/tests/integration/clientside/test_clientside_inline.py +++ b/tests/integration/clientside/test_clientside_inline.py @@ -30,7 +30,7 @@ output('output-clientside', 'children'), params=list(input('input', 'value')), " - inline: function (value) { + function (value) { return 'Client says \"' + value + '\"'; }" ) From 4f3c036c1ee4a33e684a72b0bb4b493851ea3183 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle <ryan@plot.ly> Date: Tue, 21 Apr 2020 19:19:07 -0400 Subject: [PATCH 20/23] fix test --- tests/integration/clientside/test_clientside_inline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/clientside/test_clientside_inline.py b/tests/integration/clientside/test_clientside_inline.py index 07d15ca4..d2cb30ed 100644 --- a/tests/integration/clientside/test_clientside_inline.py +++ b/tests/integration/clientside/test_clientside_inline.py @@ -40,7 +40,6 @@ def test_rscc001_clientside(dashr): - os.chdir(os.path.dirname(__file__)) dashr.start_server(app) dashr.wait_for_text_to_equal( '#output-clientside', From 91b919afc4c05eee3cfd264f21ffc3c273730f52 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle <ryan@plot.ly> Date: Tue, 21 Apr 2020 20:01:57 -0400 Subject: [PATCH 21/23] try delay --- tests/integration/clientside/test_clientside_inline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/clientside/test_clientside_inline.py b/tests/integration/clientside/test_clientside_inline.py index d2cb30ed..d61d4a8b 100644 --- a/tests/integration/clientside/test_clientside_inline.py +++ b/tests/integration/clientside/test_clientside_inline.py @@ -41,6 +41,7 @@ def test_rscc001_clientside(dashr): dashr.start_server(app) + time.sleep(2) dashr.wait_for_text_to_equal( '#output-clientside', 'Client says "undefined"' From 3469368a133bb31d6c2aef01db66b7771939c1f7 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle <ryan@plot.ly> Date: Tue, 21 Apr 2020 20:48:23 -0400 Subject: [PATCH 22/23] restore docstrings --- R/dash.R | 13 ++++++++----- .../clientside/test_clientside_inline.py | 1 - 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/R/dash.R b/R/dash.R index e1bc0181..10451912 100644 --- a/R/dash.R +++ b/R/dash.R @@ -87,18 +87,21 @@ #' \describe{ #' \item{output}{a named list including a component `id` and `property`} #' \item{params}{an unnamed list of [input] and [state] statements, each with defined `id` and `property`} -#' \item{func}{any valid R function which generates [output] provided [input] and/or [state] arguments, or a call to [clientsideFunction] including `namespace` and `function_name` arguments for a locally served JavaScript function} +#' \item{func}{any valid R function which generates [output] provided [input] and/or [state] arguments, +#' a character string containing valid JavaScript, or a call to [clientsideFunction] including `namespace` +#' and `function_name` arguments for a locally served JavaScript function} #' } #' The `output` argument defines which layout component property should #' receive the results (via the [output] object). The events that #' trigger the callback are then described by the [input] (and/or [state]) #' object(s) (which should reference layout components), which become #' argument values for R callback handlers defined in `func`. Here `func` may -#' either be an anonymous R function, or a call to `clientsideFunction()`, which -#' describes a locally served JavaScript function instead. The latter defines a -#' "clientside callback", which updates components without passing data to and +#' either be an anonymous R function, a JavaScript function provided as a +#' character string, or a call to `clientsideFunction()`, which describes a +#' locally served JavaScript function instead. The latter two methods define +#' a "clientside callback", which updates components without passing data to and #' from the Dash backend. The latter may offer improved performance relative -#' to callbacks written in R. +#' to callbacks written purely in R. #' } #' \item{`title("dash")`}{ #' The title of the app. If no title is supplied, Dash for R will use 'dash'. diff --git a/tests/integration/clientside/test_clientside_inline.py b/tests/integration/clientside/test_clientside_inline.py index d61d4a8b..d2cb30ed 100644 --- a/tests/integration/clientside/test_clientside_inline.py +++ b/tests/integration/clientside/test_clientside_inline.py @@ -41,7 +41,6 @@ def test_rscc001_clientside(dashr): dashr.start_server(app) - time.sleep(2) dashr.wait_for_text_to_equal( '#output-clientside', 'Client says "undefined"' From 209c8f4dd6138cd6a9f5c1510ff8c50ea538aced Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle <ryan@plot.ly> Date: Tue, 21 Apr 2020 22:39:58 -0400 Subject: [PATCH 23/23] :see_no_evil: fixed test finally --- tests/integration/clientside/test_clientside_inline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/clientside/test_clientside_inline.py b/tests/integration/clientside/test_clientside_inline.py index d2cb30ed..86edd808 100644 --- a/tests/integration/clientside/test_clientside_inline.py +++ b/tests/integration/clientside/test_clientside_inline.py @@ -31,7 +31,7 @@ params=list(input('input', 'value')), " function (value) { - return 'Client says \"' + value + '\"'; + return 'Client says \\"' + value + '\\"'; }" )