ddl alternative - #167
Conversation
| #' as well as a function (`on_submit`) that will create the desired data sets. | ||
| #' | ||
| #' One of the inputs must be an action button (or action link) called "submit". | ||
| #' When clicked, the `on_submit` function will be run. |
There was a problem hiding this comment.
| #' When clicked, the `on_submit` function will be run. | |
| #' When clicked, `on_submit` function will run. |
| #' `on_submit` must take one argument called `inputs`, | ||
| #' which will be a list of all input elements defined in the UI function except `"submit"`. |
There was a problem hiding this comment.
| #' `on_submit` must take one argument called `inputs`, | |
| #' which will be a list of all input elements defined in the UI function except `"submit"`. | |
| #' `on_submit` takes one argument called `inputs`, | |
| #' which is a list of all input elements defined in the UI function, except `"submit"`. |
| #' | ||
| #' `on_submit` must take one argument called `inputs`, | ||
| #' which will be a list of all input elements defined in the UI function except `"submit"`. | ||
| #' The function body must contain all code necessary to obtain the desired data sets and nothing else. |
There was a problem hiding this comment.
| #' The function body must contain all code necessary to obtain the desired data sets and nothing else. | |
| #' `on_submit` body should contain the code necessary to obtain the desired datasets and nothing else. |
| #' The obtained data sets will be packed into a `tdata` object. | ||
| #' The body of `on_submit` will be recorded in the resulting `tdata`. | ||
| #' | ||
| #' The `mask` argument can be used to mask input values used as arguments in the recorded code. |
There was a problem hiding this comment.
| #' The `mask` argument can be used to mask input values used as arguments in the recorded code. | |
| #' The `mask` argument can be used to mask input values used as arguments in the recorded code. So that sensitive parts of code (like password or username) are substituted with `mask`ed values. |
| #' | ||
| #' The `mask` argument can be used to mask input values used as arguments in the recorded code. | ||
| #' | ||
| #' Input elements will be put in a div of class `connector-input`. |
There was a problem hiding this comment.
| #' Input elements will be put in a div of class `connector-input`. | |
| #' Input elements will be put in a `connector-input` div. |
| #' | ||
| #' @param ... any number of `shiny.tag`s | ||
| #' @param on_submit function to run after clicking the "submit" button, see `Details` | ||
| #' @param mask optional list specifying how to mask the code run by `on_submit`, see `Details` |
There was a problem hiding this comment.
| #' @param mask optional list specifying how to mask the code run by `on_submit`, see `Details` | |
| #' @param mask an optional list specifying how to mask the code run by `on_submit`, see `Details` |
| #' @param ... any number of `shiny.tag`s | ||
| #' @param on_submit function to run after clicking the "submit" button, see `Details` | ||
| #' @param mask optional list specifying how to mask the code run by `on_submit`, see `Details` | ||
| #' @return A`reactive` expression returning a `tdata` object. |
There was a problem hiding this comment.
| #' @return A`reactive` expression returning a `tdata` object. | |
| #' @return A `reactive` expression returning a `tdata` object. |
| submit_class <- submit[grep("class$", names(submit))] | ||
| checkmate::assert_true( | ||
| grepl("action-button", submit_class), | ||
| .var.name = "The \"submit\" element has class \"action-button\"." |
There was a problem hiding this comment.
| .var.name = "The \"submit\" element has class \"action-button\"." | |
| .var.name = "The \"submit\" element class: \"action-button\"." |
|
Hey @chlebowa I can't give any feedback to the code, as it's extremely condensed, clean and contains a lot of advanced tricks that reduces the complexity of the implementation. As always, a solid thorough work! When it comes to the interface and user experience, I prefer this approach than #161 as I do see benefits of having everything in one place ( You have my axe! A small question: is there a way to use module <- input_template(
h5('Below is a consens statement, that you need to accept before using the app. Do you accept?')
actionButton("submit", "Accept"),
on_submit = function(input) {
# before on_submit data is NULL
data <- pull(input$data) # ??
}
) |
|
I just pushed a major overhaul of roxygen docs, which should have been there from the beginning but slipped my mind during the multiple rewrites this file has seen ober the last two weeks. Sorry for the inconvenience and thank you @m7pr for bringing it to my attention. |
| # Convert calls to strings and substitute argument references by bquote references. | ||
| code_strings <- vapply(code, deparse1, character(1L)) | ||
| code_strings <- gsub("(input\\$)(\\w+)", "\\.(\\2\\)", code_strings) | ||
| code_strings <- gsub("(input\\[\\[\")(\\w+)(\"\\]\\])", "\\.(\\2\\)", code_strings) |
There was a problem hiding this comment.
@chlebowa I would make sure those regexes work on all platforms. Looks like system specific?
There was a problem hiding this comment.
As far as I know this is just R-specific, it looks a little weird because every slash has to be escaped. \w and backtracing are widely recognized.
There was a problem hiding this comment.
There may be some edge cases where users use purrr::plucK(input, "something") or something else.
There was a problem hiding this comment.
True, get, for instance. I'm not sure we have to worry about it, considering people mosty use $.
Now that I think about it, some NSE tricks would probably go unnoticed 🤔
I think that issue is about a popup on the whole app, regardless of whether the data is remote or local, so |
|
Update: this version of I wonder what to do with errors raised by the user-provided code. Currently they are intercepted and passed to Requires |
| env <- new.env() | ||
| eval(as.expression(code_input), env) | ||
| # Create `tdata` with masked code. | ||
| new_tdata(as.list(env), code = as.expression(code_masked), keys = join_keys) |
There was a problem hiding this comment.
We need a way to change code_masked when creating tdata object
| new_tdata(as.list(env), code = as.expression(code_masked), keys = join_keys) | |
| new_tdata(as.list(env), code = as.expression(code_masked), keys = join_keys) |
There is a case where in the interactive session one can use login() function which opens a dialog, while for shiny session authentication is handled by the module and no login() function is used. It means that initial code_masked looks like this
data <- connection_package::read_data(...)and returned should look like:
connection_package::login()
data <- connection_package::read_data(...)| server <- function(id) { | ||
| moduleServer(id, function(input, output, session) { | ||
| result <- eventReactive(input[["submit"]], { | ||
| inputs <- sapply(setdiff(inputIds, "submit"), function(x) input[[x]], simplify = FALSE) | ||
| tryCatch( | ||
| do.call(tracked_request, list(inputs)), | ||
| error = function(e) validate(need(FALSE, sprintf("Error: %s", e$message))) | ||
| ) | ||
| }) | ||
| result | ||
| }) |
There was a problem hiding this comment.
We need a possibility to specify this server. For example to control hierarchical inputs.
| server <- function(id) { | |
| moduleServer(id, function(input, output, session) { | |
| result <- eventReactive(input[["submit"]], { | |
| inputs <- sapply(setdiff(inputIds, "submit"), function(x) input[[x]], simplify = FALSE) | |
| tryCatch( | |
| do.call(tracked_request, list(inputs)), | |
| error = function(e) validate(need(FALSE, sprintf("Error: %s", e$message))) | |
| ) | |
| }) | |
| result | |
| }) | |
| server <- function(id) { | |
| moduleServer(id, function(input, output, session) { | |
| result <- eventReactive(input[["submit"]], { | |
| inputs <- sapply(setdiff(inputIds, "submit"), function(x) input[[x]], simplify = FALSE) | |
| tryCatch( | |
| do.call(tracked_request, list(inputs)), | |
| error = function(e) validate(need(FALSE, sprintf("Error: %s", e$message))) | |
| ) | |
| }) | |
| result | |
| }) |
|
Having just |
companion to [this PR](insightsengineering/teal.data#167)
ddlimplementation alternative to #161 .Complemented by this PR.
In order to simplify the user (app dev) experience, I tried to streamline the logic.
In order to create a
ddlconnector module, one has to:input_templateto create the module: enumerate input widgetson_submit, to be run when"submit"button is clicked; function takes input values wrapped in a list calledinputand body refers to input values withinput$<value>orinput[["<value>"]]tdataobjectteal(I don't like it)teal; defaults to emptyteal.data::join_keys()When inputs are submitted,
on_submitis passed to a function that extracts the body, substitutesinputplaceholders with input values and evaluates the code to obtain data sets in a separate environment. Then it replaces the input values in the code with ones provided inmask(if any) and uses the environment and the masked code to createtdata.Much like in the solution proposed on branch
refactor, the user provides code to obtain data sets and replacements for input values, and data is created in separate environment, which is then used to createtdatawith masked code.Unlike that solution, the user specifies everything in one place, rather than having to define module ui, module server that runs a post-processing function, the post-processing function itself, etc. This is easier to understand for me.
Another difference is that the user provides code as code with
input$references, not text withgluesyntax ({ value }). This is done move focus to masking rather than have the user think about "online" and "offline" arguments. It also uses pure base R.MOCK DATABASE CONNECTION
MODULE DEFINITION
AN APP
A TEAL APP