Skip to content

Commit e082bc4

Browse files
authored
Support inline clientside callbacks in Dash for R (#140)
* ✨ support inline clientside callbacks * 📚 update docs * 🚨 add inline clientside test * Update CHANGELOG.md
1 parent 9f7d33e commit e082bc4

File tree

6 files changed

+216
-64
lines changed

6 files changed

+216
-64
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Change Log for Dash for R
22
All notable changes to this project will be documented in this file.
33

4+
## Unreleased
5+
### Added
6+
- Support for inline clientside callbacks in JavaScript [#140](https://github.com/plotly/dashR/pull/140)
47

58
## [0.3.0] - 2020-02-12
69
### Added

R/dash.R

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
#'
2424
#' @section Arguments:
2525
#' \tabular{lll}{
26-
#' `name` \tab \tab Character. The name of the Dash application (placed in the `<title>`
26+
#' `name` \tab \tab Character. The name of the Dash application (placed in the title
2727
#' of the HTML page). DEPRECATED; please use `index_string()` or `interpolate_index()` instead.\cr
2828
#' `server` \tab \tab The web server used to power the application.
2929
#' Must be a [fiery::Fire] object.\cr
@@ -87,18 +87,21 @@
8787
#' \describe{
8888
#' \item{output}{a named list including a component `id` and `property`}
8989
#' \item{params}{an unnamed list of [input] and [state] statements, each with defined `id` and `property`}
90-
#' \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}
90+
#' \item{func}{any valid R function which generates [output] provided [input] and/or [state] arguments,
91+
#' a character string containing valid JavaScript, or a call to [clientsideFunction] including `namespace`
92+
#' and `function_name` arguments for a locally served JavaScript function}
9193
#' }
9294
#' The `output` argument defines which layout component property should
9395
#' receive the results (via the [output] object). The events that
9496
#' trigger the callback are then described by the [input] (and/or [state])
9597
#' object(s) (which should reference layout components), which become
9698
#' argument values for R callback handlers defined in `func`. Here `func` may
97-
#' either be an anonymous R function, or a call to `clientsideFunction()`, which
98-
#' describes a locally served JavaScript function instead. The latter defines a
99-
#' "clientside callback", which updates components without passing data to and
99+
#' either be an anonymous R function, a JavaScript function provided as a
100+
#' character string, or a call to `clientsideFunction()`, which describes a
101+
#' locally served JavaScript function instead. The latter two methods define
102+
#' a "clientside callback", which updates components without passing data to and
100103
#' from the Dash backend. The latter may offer improved performance relative
101-
#' to callbacks written in R.
104+
#' to callbacks written purely in R.
102105
#' }
103106
#' \item{`title("dash")`}{
104107
#' The title of the app. If no title is supplied, Dash for R will use 'dash'.
@@ -119,8 +122,8 @@
119122
#' }
120123
#' \item{`get_relative_path(path, requests_pathname_prefix)`}{
121124
#' The `get_relative_path` method simplifies the handling of URLs and pathnames for apps
122-
#' running locally and on a deployment server such as Dash Enterprise. It handles the prefix
123-
#' for requesting assets similar to the `get_asset_url` method, but can also be used for URL handling
125+
#' running locally and on a deployment server such as Dash Enterprise. It handles the prefix
126+
#' for requesting assets similar to the `get_asset_url` method, but can also be used for URL handling
124127
#' in components such as `dccLink` or `dccLocation`. For example, `app$get_relative_url("/page/")`
125128
#' would return `/app/page/` for an app running on a deployment server. The path must be prefixed with
126129
#' a `/`.
@@ -132,8 +135,8 @@
132135
#' The `strip_relative_path` method simplifies the handling of URLs and pathnames for apps
133136
#' running locally and on a deployment server such as Dash Enterprise. It acts almost opposite the `get_relative_path`
134137
#' method, by taking a `relative path` as an input, and returning the `path` stripped of the `requests_pathname_prefix`,
135-
#' and any leading or trailing `/`. For example, a path string `/app/homepage/`, would be returned as
136-
#' `homepage`. This is particularly useful for `dccLocation` URL routing.
138+
#' and any leading or trailing `/`. For example, a path string `/app/homepage/`, would be returned as
139+
#' `homepage`. This is particularly useful for `dccLocation` URL routing.
137140
#' \describe{
138141
#' \item{path}{Character. A path string prefixed with a leading `/` and `requests_pathname_prefix` which directs at a path or asset directory.}
139142
#' \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,17 +186,17 @@
183186
#' but offers the ability to change the default components of the Dash index as seen in the example below:
184187
#' \preformatted{
185188
#' app$interpolate_index(
186-
#' template_index,
187-
#' metas = "<meta_charset='UTF-8'/>",
188-
#' renderer = renderer,
189+
#' template_index,
190+
#' metas = "<meta_charset='UTF-8'/>",
191+
#' renderer = renderer,
189192
#' config = config)
190193
#' }
191194
#' \describe{
192195
#' \item{template_index}{Character. A formatted string with the HTML index string. Defaults to the initial template}
193196
#' \item{...}{Named List. The unnamed arguments can be passed as individual named lists corresponding to the components
194197
#' of the Dash html index. These include the same arguments as those found in the `index_string()` template.}
195198
#' }
196-
#' }
199+
#' }
197200
#' \item{`run_server(host = Sys.getenv('HOST', "127.0.0.1"),
198201
#' port = Sys.getenv('PORT', 8050), block = TRUE, showcase = FALSE, ...)`}{
199202
#' The `run_server` method has 13 formal arguments, several of which are optional:
@@ -778,6 +781,25 @@ Dash <- R6::R6Class(
778781

779782
if (is.function(func)) {
780783
clientside_function <- NULL
784+
} else if (is.character(func)) {
785+
# update the scripts before generating tags, and remove exact
786+
# duplicates from inline_scripts
787+
fn_name <- paste0("_dashprivate_", output$id)
788+
789+
func <- paste0('<script>\n',
790+
'var clientside = window.dash_clientside = window.dash_clientside || {};\n',
791+
'var ns = clientside["', fn_name, '"] = clientside["', fn_name, '"] || {};\n',
792+
'ns["', output$property, '"] = \n',
793+
func,
794+
'\n;',
795+
'</script>')
796+
797+
private$inline_scripts <- unique(c(private$inline_scripts, func))
798+
799+
clientside_function <- clientsideFunction(namespace = fn_name,
800+
function_name = output$property)
801+
802+
func <- NULL
781803
} else {
782804
clientside_function <- func
783805
func <- NULL
@@ -801,13 +823,13 @@ Dash <- R6::R6Class(
801823
}
802824
private$callback_context_
803825
},
804-
826+
805827
# ------------------------------------------------------------------------
806828
# return asset URLs
807829
# ------------------------------------------------------------------------
808830
get_asset_url = function(asset_path, prefix = self$config$requests_pathname_prefix) {
809831
app_root_path <- Sys.getenv("DASH_APP_PATH")
810-
832+
811833
if (app_root_path == "" && getAppPath() != FALSE) {
812834
# app loaded via source(), root path is known
813835
app_root_path <- dirname(private$app_root_path)
@@ -816,52 +838,52 @@ Dash <- R6::R6Class(
816838
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.")
817839
return(NULL)
818840
}
819-
820-
asset <- lapply(private$asset_map,
841+
842+
asset <- lapply(private$asset_map,
821843
function(x) {
822844
# asset_path should be prepended with the full app root & assets path
823845
# if leading slash(es) present in asset_path, remove them before
824846
# assembling full asset path
825847
asset_path <- file.path(app_root_path,
826-
private$assets_folder,
848+
private$assets_folder,
827849
sub(pattern="^/+",
828850
replacement="",
829851
asset_path))
830852
return(names(x[x == asset_path]))
831853
}
832854
)
833855
asset <- unlist(asset, use.names = FALSE)
834-
856+
835857
if (length(asset) == 0)
836858
stop(sprintf("the asset path '%s' is not valid; please verify that this path exists within the '%s' directory.",
837859
asset_path,
838860
private$assets_folder))
839-
861+
840862
# strip multiple slashes if present, since we'll
841863
# introduce one when we concatenate the prefix and
842864
# asset path & prepend the asset name with route prefix
843865
return(gsub(pattern="/+",
844866
replacement="/",
845-
paste(prefix,
846-
private$assets_url_path,
847-
asset,
867+
paste(prefix,
868+
private$assets_url_path,
869+
asset,
848870
sep="/")))
849871
},
850-
872+
851873
# ------------------------------------------------------------------------
852874
# return relative asset URLs
853875
# ------------------------------------------------------------------------
854-
876+
855877
get_relative_path = function(path, requests_pathname_prefix = self$config$requests_pathname_prefix) {
856878
asset = get_relative_path(requests_pathname = requests_pathname_prefix, path = path)
857879
return(asset)
858880
},
859-
860-
881+
882+
861883
# ------------------------------------------------------------------------
862884
# return relative asset URLs
863885
# ------------------------------------------------------------------------
864-
886+
865887
strip_relative_path = function(path, requests_pathname_prefix = self$config$requests_pathname_prefix) {
866888
asset = strip_relative_path(requests_pathname = requests_pathname_prefix, path = path)
867889
return(asset)
@@ -872,32 +894,32 @@ Dash <- R6::R6Class(
872894
index_string = function(string) {
873895
private$custom_index <- validate_keys(string)
874896
},
875-
897+
876898
# ------------------------------------------------------------------------
877-
# modify the templated variables by using the `interpolate_index` method.
899+
# modify the templated variables by using the `interpolate_index` method.
878900
# ------------------------------------------------------------------------
879901
interpolate_index = function(template_index = private$template_index[[1]], ...) {
880902
template = template_index
881903
kwargs <- list(...)
882-
904+
883905
for (name in names(kwargs)) {
884906
key = paste0('\\{\\%', name, '\\%\\}')
885907
template = sub(key, kwargs[[name]], template)
886-
}
887-
908+
}
909+
888910
invisible(validate_keys(names(kwargs)))
889-
911+
890912
private$template_index <- template
891913
},
892-
914+
893915
# ------------------------------------------------------------------------
894916
# specify a custom title
895917
# ------------------------------------------------------------------------
896918
title = function(string = "dash") {
897919
assertthat::assert_that(is.character(string))
898920
private$name <- string
899921
},
900-
922+
901923
# ------------------------------------------------------------------------
902924
# convenient fiery wrappers
903925
# ------------------------------------------------------------------------
@@ -1385,6 +1407,9 @@ Dash <- R6::R6Class(
13851407
# the input/output mapping passed back-and-forth between the client & server
13861408
callback_map = list(),
13871409

1410+
# the list of inline scripts passed as strings via (clientside) callbacks
1411+
inline_scripts = list(),
1412+
13881413
# akin to https://github.com/plotly/dash-renderer/blob/master/dash_renderer/__init__.py
13891414
react_version_enabled= function() {
13901415
version <- private$dependencies_internal$`react-prod`$version
@@ -1455,7 +1480,7 @@ Dash <- R6::R6Class(
14551480
depsAll <- compact(c(
14561481
private$react_deps()[private$react_versions() %in% private$react_version_enabled()],
14571482
private$dependencies_internal[grepl(pattern = "prop-types", x = private$dependencies_internal)],
1458-
private$dependencies_internal[grepl(pattern = "polyfill", x = private$dependencies_internal)],
1483+
private$dependencies_internal[grepl(pattern = "polyfill", x = private$dependencies_internal)],
14591484
private$dependencies,
14601485
private$dependencies_user,
14611486
private$dependencies_internal[grepl(pattern = "dash-renderer", x = private$dependencies_internal)]
@@ -1554,6 +1579,9 @@ Dash <- R6::R6Class(
15541579
"application/javascript",
15551580
"var renderer = new DashRenderer();")
15561581

1582+
# add inline tags
1583+
scripts_inline <- private$inline_scripts
1584+
15571585
# serving order of CSS and JS tags: package -> external -> assets
15581586
css_tags <- paste(c(css_deps,
15591587
css_external,
@@ -1563,6 +1591,7 @@ Dash <- R6::R6Class(
15631591
scripts_tags <- paste(c(scripts_deps,
15641592
scripts_external,
15651593
scripts_assets,
1594+
scripts_inline,
15661595
scripts_invoke_renderer),
15671596
collapse = "\n ")
15681597

@@ -1590,21 +1619,21 @@ Dash <- R6::R6Class(
15901619

15911620
# insert meta tags if present
15921621
meta_tags <- all_tags[["meta_tags"]]
1593-
1622+
15941623
# define the react-entry-point
15951624
app_entry <- "<div id='react-entry-point'><div class='_dash-loading'>Loading...</div></div>"
15961625
# define the dash default config key
15971626
config <- sprintf("<script id='_dash-config' type='application/json'> %s </script>", to_JSON(self$config))
15981627

15991628
if (is.null(private$name))
16001629
private$name <- 'dash'
1601-
1630+
16021631
if (!is.null(private$custom_index)) {
16031632
string_index <- glue::glue(private$custom_index, .open = "{%", .close = "%}")
1604-
1633+
16051634
private$.index <- string_index
16061635
}
1607-
1636+
16081637
else if (length(private$template_index) == 1) {
16091638
private$.index <- private$template_index
16101639
}

R/utils.R

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ resolvePrefix <- function(prefix, environment_var, base_pathname) {
467467
prefix_env <- Sys.getenv(environment_var)
468468
env_base_pathname <- Sys.getenv("DASH_URL_BASE_PATHNAME")
469469
app_name <- Sys.getenv("DASH_APP_NAME")
470-
470+
471471
if (prefix_env != "")
472472
return(prefix_env)
473473
else if (app_name != "")
@@ -1118,6 +1118,8 @@ dashLogger <- function(event = NULL,
11181118
#' Define a clientside callback
11191119
#'
11201120
#' Create a callback that updates the output by calling a clientside (JavaScript) function instead of an R function.
1121+
#' Note that it is also possible to specify JavaScript as a character string instead of passing `clientsideFunction`.
1122+
#' In this case Dash will inline your JavaScript automatically, without needing to save a script inside `assets`.
11211123
#'
11221124
#' @param namespace Character. Describes where the JavaScript function resides (Dash will look
11231125
#' for the function at `window[namespace][function_name]`.)
@@ -1147,7 +1149,16 @@ dashLogger <- function(event = NULL,
11471149
#' namespace = 'my_clientside_library',
11481150
#' function_name = 'my_function'
11491151
#' )
1150-
#' )}
1152+
#' )
1153+
#'
1154+
#' # Passing JavaScript as a character string
1155+
#' app$callback(
1156+
#' output('output-clientside', 'children'),
1157+
#' params=list(input('input', 'value')),
1158+
#' "function (value) {
1159+
#' return 'Client says \"' + value + '\"';
1160+
#' }"
1161+
#')}
11511162
clientsideFunction <- function(namespace, function_name) {
11521163
return(list(namespace=namespace, function_name=function_name))
11531164
}
@@ -1274,8 +1285,8 @@ tryCompress <- function(request, response) {
12741285
get_relative_path <- function(requests_pathname, path) {
12751286
# Returns a path with the config setting 'requests_pathname_prefix' prefixed to
12761287
# it. This is particularly useful for apps deployed on Dash Enterprise, which makes
1277-
# it easier to serve apps under both URL prefixes and localhost.
1278-
1288+
# it easier to serve apps under both URL prefixes and localhost.
1289+
12791290
if (requests_pathname == "/" && path == "") {
12801291
return("/")
12811292
}
@@ -1295,7 +1306,7 @@ get_relative_path <- function(requests_pathname, path) {
12951306
strip_relative_path <- function(requests_pathname, path) {
12961307
# Returns a relative path with the `requests_pathname_prefix` and leadings and trailing
12971308
# slashes stripped from it. This function is particularly relevant to dccLocation pathname routing.
1298-
1309+
12991310
if (is.null(path)) {
13001311
return(NULL)
13011312
}
@@ -1316,27 +1327,27 @@ strip_relative_path <- function(requests_pathname, path) {
13161327
interpolate_str <- function(index_template, ...) {
13171328
# This function takes an index string, along with
13181329
# user specified keys for the html keys of the index
1319-
# and sets the default values of the keys to the
1330+
# and sets the default values of the keys to the
13201331
# ones specified by the keys themselves, returning
1321-
# the custom index template.
1322-
template = index_template
1332+
# the custom index template.
1333+
template = index_template
13231334
kwargs <- list(...)
1324-
1335+
13251336
for (name in names(kwargs)) {
13261337
key = paste0('\\{', name, '\\}')
1327-
1338+
13281339
template = sub(key, kwargs[[name]], template)
1329-
}
1340+
}
13301341
return(template)
13311342
}
13321343

13331344
validate_keys <- function(string) {
13341345
required_keys <- c("app_entry", "config", "scripts")
1335-
1346+
13361347
keys_present <- vapply(required_keys, function(x) grepl(x, string), logical(1))
1337-
1348+
13381349
if (!all(keys_present)) {
1339-
stop(sprintf("Did you forget to include %s in your index string?",
1350+
stop(sprintf("Did you forget to include %s in your index string?",
13401351
paste(names(keys_present[keys_present==FALSE]), collapse = ", ")))
13411352
} else {
13421353
return(string)

0 commit comments

Comments
 (0)