-
-
Notifications
You must be signed in to change notification settings - Fork 31
189 - Add Pattern Matching Callbacks for Dash R #228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
508f8f5
9be5ef9
7cb20ef
8c39812
0f47a6c
c4b6896
e860e0d
26372a3
aa8a9fc
a4a196a
d19ed4e
fd09358
4219296
6f2539b
be2e509
7ea4f56
18192cd
504233a
1944f6f
ba8f58c
fc58486
c94455f
f7723e0
939dc24
3c30a78
bcec380
28e0bf4
4af91cb
85e3052
77981ce
2d84a88
e8831dd
bf63f8a
fb34b11
84f7cdf
297c0e8
1ace13d
67166fb
aa89f81
5d8c12a
b2d9f30
6c01c78
7118828
ab327c4
dc5e2e3
e06dd57
d43a59b
1bc1cfc
e8a4fb1
c7fc099
28560cd
56f491a
577deb4
617e873
0250dbc
e745e2f
b9218b7
06b37d8
fc45ee7
0b6d543
1bec3c5
d777ae1
330eb9d
5969add
210d3c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -211,23 +211,37 @@ Dash <- R6::R6Class( | |
# | ||
# https://cran.r-project.org/doc/FAQ/R-FAQ.html#Others: | ||
callback_args <- list() | ||
|
||
for (input_element in request$body$inputs) { | ||
if(is.null(input_element$value)) | ||
if ("id.index" %in% names(unlist(input_element))) { | ||
unlisted_input <- unlist(input_element) | ||
values <- unname(unlisted_input[names(unlisted_input) == "value" | names(unlisted_input) == "value.index"]) | ||
callback_args <- c(callback_args, ifelse(length(values), list(values), list(NULL))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here it looks like we're checking if callback_args <- c(callback_args, list(values)) |
||
} | ||
else if(is.null(input_element$value)) { | ||
callback_args <- c(callback_args, list(list(NULL))) | ||
else | ||
} | ||
else { | ||
callback_args <- c(callback_args, list(input_element$value)) | ||
} | ||
} | ||
|
||
if (length(request$body$state)) { | ||
for (state_element in request$body$state) { | ||
if(is.null(state_element$value)) | ||
if ("id.index" %in% names(unlist(state_element))) { | ||
unlisted_state <- unlist(state_element) | ||
values <- unname(unlisted_state[names(unlisted_state) == "value" | names(unlisted_state) == "value.index"]) | ||
callback_args <- c(callback_args, ifelse(length(values), list(values), list(NULL))) | ||
} | ||
else if(is.null(state_element$value)) { | ||
callback_args <- c(callback_args, list(list(NULL))) | ||
else | ||
} | ||
else { | ||
callback_args <- c(callback_args, list(state_element$value)) | ||
} | ||
} | ||
} | ||
|
||
# set the callback context associated with this invocation of the callback | ||
private$callback_context_ <- setCallbackContext(request$body) | ||
|
||
|
@@ -639,10 +653,8 @@ Dash <- R6::R6Class( | |
#' JavaScript function. | ||
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)) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -890,29 +890,86 @@ removeHandlers <- function(fnList) { | |
} | ||
|
||
setCallbackContext <- function(callback_elements) { | ||
states <- lapply(callback_elements$states, function(x) { | ||
setNames(x$value, paste(x$id, x$property, sep=".")) | ||
}) | ||
# Set state elements for this callback | ||
|
||
if (length(callback_elements$state[[1]]) == 0) { | ||
states <- sapply(callback_elements$state, function(x) { | ||
setNames(list(x$value), paste(x$id, x$property, sep=".")) | ||
}) | ||
} else if (is.character(callback_elements$state[[1]][[1]])) { | ||
states <- sapply(callback_elements$state, function(x) { | ||
setNames(list(x$value), paste(x$id, x$property, sep=".")) | ||
}) | ||
} else { | ||
states <- sapply(callback_elements$state, function(x) { | ||
states_vector <- unlist(x) | ||
setNames(list(states_vector[names(states_vector) == "value" | names(states_vector) == "value.index"]), | ||
paste(as.character(jsonlite::toJSON(x[[1]])), x$property, sep=".")) | ||
}) | ||
} | ||
|
||
splitIdProp <- function(x) unlist(strsplit(x, split = "[.]")) | ||
|
||
triggered <- lapply(callback_elements$changedPropIds, | ||
function(x) { | ||
input_id <- splitIdProp(x)[1] | ||
prop <- splitIdProp(x)[2] | ||
|
||
id_match <- vapply(callback_elements$inputs, function(x) x$id %in% input_id, logical(1)) | ||
prop_match <- vapply(callback_elements$inputs, function(x) x$property %in% prop, logical(1)) | ||
|
||
value <- sapply(callback_elements$inputs[id_match & prop_match], `[[`, "value") | ||
|
||
list(`prop_id` = x, `value` = value) | ||
|
||
# The following conditionals check whether the callback is a pattern-matching callback and if it has been triggered. | ||
if (startsWith(input_id, "{")){ | ||
id_match <- vapply(callback_elements$inputs, function(x) { | ||
x <- unlist(x) | ||
any(x[names(x) == "id.index"] %in% jsonlite::fromJSON(input_id)[[1]]) | ||
}, logical(1))[[1]] | ||
} else { | ||
id_match <- vapply(callback_elements$inputs, function(x) x$id %in% input_id, logical(1)) | ||
} | ||
|
||
if (startsWith(input_id, "{")){ | ||
prop_match <- vapply(callback_elements$inputs, function(x) { | ||
x <- unlist(x) | ||
any(x[names(x) == "property"] %in% prop) | ||
}, logical(1))[[1]] | ||
} else { | ||
prop_match <- vapply(callback_elements$inputs, function(x) x$property %in% prop, logical(1)) | ||
} | ||
|
||
if (startsWith(input_id, "{")){ | ||
if (length(callback_elements$inputs) == 1) { | ||
value <- sapply(callback_elements$inputs[id_match & prop_match], `[[`, "value") | ||
} else { | ||
value <- sapply(callback_elements$inputs[id_match & prop_match][[1]], `[[`, "value") | ||
} | ||
} else { | ||
value <- sapply(callback_elements$inputs[id_match & prop_match], `[[`, "value") | ||
} | ||
|
||
if (startsWith(input_id, "{")){ | ||
return(list(`prop_id` = x, `value` = value)) | ||
} else { | ||
return(list(`prop_id` = x, `value` = value)) | ||
} | ||
} | ||
) | ||
|
||
inputs <- sapply(callback_elements$inputs, function(x) { | ||
setNames(list(x$value), paste(x$id, x$property, sep=".")) | ||
}) | ||
) | ||
if (length(callback_elements$inputs[[1]]) == 0) { | ||
inputs <- sapply(callback_elements$inputs, function(x) { | ||
setNames(list(x$value), paste(x$id, x$property, sep=".")) | ||
}) | ||
} else if (is.character(callback_elements$inputs[[1]][[1]])) { | ||
inputs <- sapply(callback_elements$inputs, function(x) { | ||
setNames(list(x$value), paste(x$id, x$property, sep=".")) | ||
}) | ||
} else if (length(callback_elements$inputs[[1]]) > 1) { | ||
inputs <- sapply(callback_elements$inputs, function(x) { | ||
inputs_vector <- unlist(x) | ||
setNames(list(inputs_vector[names(inputs_vector) == "value" | names(inputs_vector) == "value.index"]), paste(as.character(jsonlite::toJSON(x$id)), x$property, sep=".")) | ||
}) | ||
} else { | ||
inputs <- sapply(callback_elements$inputs, function(x) { | ||
inputs_vector <- unlist(x) | ||
setNames(list(inputs_vector[names(inputs_vector) == "value" | names(inputs_vector) == "value.index"]), paste(as.character(jsonlite::toJSON(x[[1]]$id)), x[[1]]$property, sep=".")) | ||
}) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suspect we could DRY this function up a bit as well. I was hoping to ask you about this block: if (startsWith(input_id, "{")){
return(list(`prop_id` = x, `value` = value))
} else {
return(list(`prop_id` = x, `value` = value))
} Is it just me or did we end up with some 🍝 in one of those two branches? I kept staring at it thinking I must have missed something. 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
As discussed offline, we're going to see if it's possible to DRY up the first part of the function, and see if there's a cleaner way to handle the pattern matching callbacks case within } else if (length(callback_elements$inputs[[1]]) > 1) {
inputs <- sapply(callback_elements$inputs, function(x) {
inputs_vector <- unlist(x)
setNames(list(inputs_vector[names(inputs_vector) == "value" | names(inputs_vector) == "value.index"]), paste(as.character(jsonlite::toJSON(x$id)), x$property, sep="."))
})
} else {
inputs <- sapply(callback_elements$inputs, function(x) {
inputs_vector <- unlist(x)
setNames(list(inputs_vector[names(inputs_vector) == "value" | names(inputs_vector) == "value.index"]), paste(as.character(jsonlite::toJSON(x[[1]]$id)), x[[1]]$property, sep="."))
})
} |
||
|
||
return(list(states=states, | ||
triggered=unlist(triggered, recursive=FALSE), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
# Simple Example with ALL | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice sample app! As far as tests go, I imagine we might want to consider
|
||
|
||
library(dash) | ||
library(dashCoreComponents) | ||
library(dashHtmlComponents) | ||
|
||
app <- Dash$new() | ||
|
||
app$layout(htmlDiv(list( | ||
htmlButton("Add Filter", id="add-filter", n_clicks=0), | ||
htmlDiv(id="dropdown-container", children=list()), | ||
htmlDiv(id="dropdown-container-output"), | ||
dccInput(id='my-id', value='initial value', type='text'), | ||
htmlDiv(id='my-div') | ||
))) | ||
|
||
|
||
app$callback( | ||
output(id="dropdown-container", property = "children"), | ||
params = list( | ||
input(id = "add-filter", property = "n_clicks"), | ||
state(id = "dropdown-container", property = "children") | ||
), | ||
display_dropdowns <- function(n_clicks, children){ | ||
new_dropdown = dccDropdown( | ||
id=list( | ||
"index" = n_clicks, | ||
"type" = "filter-dropdown" | ||
), | ||
options = lapply(1:4, function(x){ | ||
list("label" = x, "value" = x) | ||
}) | ||
) | ||
children[[n_clicks + 1]] <- new_dropdown | ||
return(children) | ||
} | ||
) | ||
|
||
|
||
app$callback( | ||
output(id="dropdown-container-output", property="children"), | ||
params = list( | ||
input(id=list("index" = "ALL", "type" = "filter-dropdown"), property= "value") | ||
), | ||
display_output <- function(test){ | ||
print(test) | ||
return(htmlDiv( | ||
lapply(1:length(test), function(x){ | ||
return(htmlDiv(sprintf("Dropdown %s = %s", x, test[[x]]))) | ||
}) | ||
)) | ||
} | ||
) | ||
|
||
|
||
app$callback( | ||
output=list(id='my-div', property='children'), | ||
params=list(input(id='my-id', property='value')), | ||
function(input_value) { | ||
sprintf("You've entered \"%s\"", input_value) | ||
}) | ||
|
||
|
||
app$run_server(debug=F, showcase = TRUE) | ||
|
||
# Standard Callback Example | ||
|
||
library(dash) | ||
library(dashCoreComponents) | ||
library(dashHtmlComponents) | ||
|
||
app <- Dash$new() | ||
|
||
app$layout( | ||
htmlDiv( | ||
list( | ||
dccInput(id='my-id', value='initial value', type='text'), | ||
htmlDiv(id='my-div') | ||
) | ||
) | ||
) | ||
|
||
app$callback( | ||
output=list(id='my-div', property='children'), | ||
params=list(input(id='my-id', property='value')), | ||
function(input_value) { | ||
sprintf("You've entered \"%s\"", input_value) | ||
}) | ||
|
||
app$run_server() | ||
|
||
|
||
# Simple Example with MATCH | ||
|
||
library(dash) | ||
library(dashCoreComponents) | ||
library(dashHtmlComponents) | ||
|
||
|
||
app <- Dash$new() | ||
|
||
app$layout(htmlDiv(list( | ||
htmlButton("Add Filter", id="dynamic-add-filter", n_clicks=0), | ||
htmlDiv(id="dynamic-dropdown-container", children=list()) | ||
))) | ||
|
||
|
||
app$callback( | ||
output(id="dynamic-dropdown-container", "children"), | ||
params = list( | ||
input("dynamic-add-filter", "n_clicks"), | ||
state("dynamic-dropdown-container", "children") | ||
), | ||
display_dropdown <- function(n_clicks, children){ | ||
new_element = htmlDiv(list( | ||
dccDropdown( | ||
id = list("type" = "dynamic-dropdown", "index" = sprintf("%s", n_clicks)), | ||
options = lapply(c("NYC", "MTL", "LA", "TOKYO"), function(x){ | ||
list("label" = x, "value" = x) | ||
}) | ||
), | ||
htmlDiv( | ||
id = list("type" = "dynamic-output", "index" = sprintf("%s", n_clicks)) | ||
) | ||
)) | ||
children <- c(children, list(new_element)) | ||
return(children) | ||
} | ||
) | ||
|
||
|
||
app$callback( | ||
output(list("type" = "dynamic-output", "index" = "MATCH"), "children"), | ||
params = list( | ||
input(list("type" = "dynamic-dropdown", "index" = "MATCH"), "value"), | ||
state(list("type" = "dynamic-dropdown", "index" = "MATCH"), "id") | ||
), | ||
display_output <- function(value, id){ | ||
print(id) | ||
print(value) | ||
return(htmlDiv(list( | ||
id, value | ||
))) | ||
} | ||
) | ||
|
||
app$run_server(showcase = T, debug = F) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could 🌴 this up a little by moving the
unlist(input_element)
outside of theif
.If what we're mostly interested in is
names(unlisted_input)
, we could actually assign that instead, and use it where needed below.Just an observation, but I think this may be a circumstance where using a
for
loop and a conditional causes a lot of housekeeping that could avoided by retaining the originallist
structure. For example, here:we have to call☺️
names
twice, even though we already extracted the names aboveOn the other hand, had we left it as a
list
, we could instead trywhich I think would give us what we want in a slightly easier-to-read/easier-to-follow way. I haven't tried it (and
input_list
is so compact it hardly matters), but I suspect this might be a bit more efficient for large lists as well.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit hesitant to change this part for a couple of reasons. For a normal callback,
request$body$inputs
is already a named list, and unlisting that would make handling regular callbacks more complicated as we then have to append a list of lists tocallback_args
for default callbacks.In the case of pattern matching callbacks, each
input_element
is a list of lists. By unlisting it we can access each of the wildcard inputs. I tried to use a vector to subset as you suggested, but while it works for some scenarios, on the initial call of the PMC we run into a subscript out of bounds error, and it might overcomplicate it to account for another condition. We'll also have to deal withNA
values that can come up if the input doesn't have avalue.index
.I'll try to figure out a way to simplify it and improve readability, but I think unlisting with a
for loop
in this case would have to be a necessary evil.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, trust your judgment (since I do as well). Though one remark I'd make is that the out of bounds error may be related to the current default values for callback inputs, which is
list(NULL)
. In that case, we could probably just ensure these values are handled properly, but don't worry about this for now.We can always revisit the implementation later if we have an epiphany about making it a little more concise, for the moment it looks OK as-is.