Skip to content
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

Delays transform modules reactivity until tab is active #1373

Merged
merged 27 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
54818e6
add a note to teal_data_module constructor in its vignette about how …
m7pr Aug 30, 2024
fe4f707
move the note between files
m7pr Sep 2, 2024
fcc5d38
Merge branch 'main' into 1303_documentation@main
m7pr Sep 18, 2024
a7b3610
Merge branch 'main' into 1303_documentation@main
m7pr Oct 3, 2024
4a6bbb1
Merge branch 'main' into 1303_documentation@main
m7pr Oct 3, 2024
894910f
call_once_when
dependabot-preview[bot] Oct 7, 2024
a113824
comments
dependabot-preview[bot] Oct 7, 2024
47484e9
feat: uses reusable function to create observer that is executed once
averissimo Oct 9, 2024
ad504f8
fix: filter panel does not break with module_specific filters
averissimo Oct 9, 2024
fb986cb
Merge branch 'main' into 1303_postpone_transformer@main
averissimo Oct 9, 2024
c5a4c55
fix: module tab reactivity is only triggered after data is pulled
averissimo Oct 9, 2024
a503f90
feat: trigger filter manager in all modules at startup
averissimo Oct 10, 2024
c96d333
docs: adds warning that detects naïve case of eventReactive usage
averissimo Oct 10, 2024
6768e85
Merge remote-tracking branch 'origin/1303_documentation@main' into 13…
averissimo Oct 10, 2024
08ab298
docs: update message in warning and docs
averissimo Oct 10, 2024
ccc50a2
Update R/module_teal_data.R
averissimo Oct 11, 2024
6939d2c
docs: slight improvement on error message
averissimo Oct 11, 2024
07eb8f2
chore: correct lint errors and change argument name
averissimo Oct 11, 2024
5a60dfe
revert: argument name to condExpr as it needs to be a logical
averissimo Oct 11, 2024
126c547
chore: change argument name in line with observeEvent
averissimo Oct 11, 2024
5706d77
revert: removes protection against uninitialized filter manager modules
averissimo Oct 11, 2024
f4fdbd8
fix: typo
averissimo Oct 11, 2024
310bd07
fix: corrects test
averissimo Oct 11, 2024
69bf596
docs: add note to documentation of teal_transform_module
averissimo Oct 11, 2024
833f8f9
Update R/teal_data_module.R
averissimo Oct 14, 2024
31c5301
test: adds check if warning is thrown when teal_data_module returns e…
averissimo Oct 14, 2024
d885b10
tests: checks if modules are only called after teal_data_module is re…
averissimo Oct 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 108 additions & 66 deletions R/module_nested_tabs.R
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,11 @@ srv_teal_module.teal_modules <- function(id,
datasets = datasets,
slices_global = slices_global,
reporter = reporter,
is_active = reactive(is_active() && input$active_tab == module_id)
is_active = reactive(
is_active() &&
input$active_tab == module_id &&
identical(data_load_status(), "ok")
gogonzo marked this conversation as resolved.
Show resolved Hide resolved
)
)
},
simplify = FALSE
Expand All @@ -236,6 +240,8 @@ srv_teal_module.teal_module <- function(id,
is_active = reactive(TRUE)) {
logger::log_debug("srv_teal_module.teal_module initializing the module: { deparse1(modules$label) }.")
moduleServer(id = id, module = function(input, output, session) {
module_out <- reactiveVal()

active_datanames <- reactive({
.resolve_module_datanames(data = data_rv(), modules = modules)
})
Expand All @@ -253,77 +259,77 @@ srv_teal_module.teal_module <- function(id,
# Because available_teal_slices is used in FilteredData$srv_available_slices (via srv_filter_panel)
# and if it is not set, then it won't be available in the srv_filter_panel
srv_module_filter_manager(modules$label, module_fd = datasets, slices_global = slices_global)
filtered_teal_data <- srv_filter_data(
"filter_panel",
datasets = datasets,
active_datanames = active_datanames,
data_rv = data_rv,
is_active = is_active
)

is_transformer_failed <- reactiveValues()
transformed_teal_data <- srv_transform_data(
"data_transform",
data = filtered_teal_data,
transforms = modules$transformers,
modules = modules,
is_transformer_failed = is_transformer_failed
)
any_transformer_failed <- reactive({
any(unlist(reactiveValuesToList(is_transformer_failed)))
})
observeEvent(any_transformer_failed(), {
if (isTRUE(any_transformer_failed())) {
shinyjs::hide("teal_module_ui")
shinyjs::hide("validate_datanames")
shinyjs::show("transformer_failure_info")
} else {
shinyjs::show("teal_module_ui")
shinyjs::show("validate_datanames")
shinyjs::hide("transformer_failure_info")
}
})
call_once_when(is_active(), {
filtered_teal_data <- srv_filter_data(
"filter_panel",
datasets = datasets,
active_datanames = active_datanames,
data_rv = data_rv,
is_active = is_active
)
is_transformer_failed <- reactiveValues()
transformed_teal_data <- srv_transform_data(
"data_transform",
data = filtered_teal_data,
transforms = modules$transformers,
modules = modules,
is_transformer_failed = is_transformer_failed
)
any_transformer_failed <- reactive({
any(unlist(reactiveValuesToList(is_transformer_failed)))
})

module_teal_data <- reactive({
req(inherits(transformed_teal_data(), "teal_data"))
all_teal_data <- transformed_teal_data()
module_datanames <- .resolve_module_datanames(data = all_teal_data, modules = modules)
.subset_teal_data(all_teal_data, module_datanames)
})
observeEvent(any_transformer_failed(), {
if (isTRUE(any_transformer_failed())) {
shinyjs::hide("teal_module_ui")
shinyjs::hide("validate_datanames")
shinyjs::show("transformer_failure_info")
} else {
shinyjs::show("teal_module_ui")
shinyjs::show("validate_datanames")
shinyjs::hide("transformer_failure_info")
}
})

srv_validate_reactive_teal_data(
"validate_datanames",
data = module_teal_data,
modules = modules
)
module_teal_data <- reactive({
req(inherits(transformed_teal_data(), "teal_data"))
all_teal_data <- transformed_teal_data()
module_datanames <- .resolve_module_datanames(data = all_teal_data, modules = modules)
.subset_teal_data(all_teal_data, module_datanames)
})

summary_table <- srv_data_summary("data_summary", module_teal_data)

# Call modules.
module_out <- reactiveVal(NULL)
if (!inherits(modules, "teal_module_previewer")) {
obs_module <- observeEvent(
# wait for module_teal_data() to be not NULL but only once:
ignoreNULL = TRUE,
once = TRUE,
eventExpr = module_teal_data(),
handlerExpr = {
module_out(.call_teal_module(modules, datasets, module_teal_data, reporter))
}
srv_validate_reactive_teal_data(
"validate_datanames",
data = module_teal_data,
modules = modules
)
} else {
# Report previewer must be initiated on app start for report cards to be included in bookmarks.
# When previewer is delayed, cards are bookmarked only if previewer has been initiated (visited).
module_out(.call_teal_module(modules, datasets, module_teal_data, reporter))
}

# todo: (feature request) add a ReporterCard to the reporter as an output from the teal_module
# how to determine if module returns a ReporterCard so that reportPreviewer is needed?
# Should we insertUI of the ReportPreviewer then?
# What about attr(module, "reportable") - similar to attr(module, "bookmarkable")
if ("report" %in% names(module_out)) {
# (reactively) add card to the reporter
}
summary_table <- srv_data_summary("data_summary", module_teal_data)

# Call modules.
if (!inherits(modules, "teal_module_previewer")) {
obs_module <- call_once_when(
!is.null(module_teal_data()),
ignoreNULL = TRUE,
handlerExpr = {
module_out(.call_teal_module(modules, datasets, module_teal_data, reporter))
}
)
} else {
# Report previewer must be initiated on app start for report cards to be included in bookmarks.
# When previewer is delayed, cards are bookmarked only if previewer has been initiated (visited).
module_out(.call_teal_module(modules, datasets, module_teal_data, reporter))
}

# todo: (feature request) add a ReporterCard to the reporter as an output from the teal_module
# how to determine if module returns a ReporterCard so that reportPreviewer is needed?
# Should we insertUI of the ReportPreviewer then?
# What about attr(module, "reportable") - similar to attr(module, "bookmarkable")
if ("report" %in% names(module_out)) {
# (reactively) add card to the reporter
}
})

module_out
})
Expand Down Expand Up @@ -368,3 +374,39 @@ srv_teal_module.teal_module <- function(id,
)
}
}

#' Calls expression when condition is met
#'
#' Function postpones `handlerExpr` to the moment when `eventExpr` (condition) returns `TRUE`,
#' otherwise nothing happens.
#' @param eventExpr A (quoted or unquoted) logical expression that represents the event;
#' this can be a simple reactive value like input$click, a call to a reactive expression
#' like dataset(), or even a complex expression inside curly braces.
#' @param ... additional arguments passed to `observeEvent` with the exception of `eventExpr` that is not allowed.
#' @inheritParams shiny::observeEvent
#'
#' @return An observer.
#'
#' @keywords internal
call_once_when <- function(eventExpr, # nolint: object_name.
handlerExpr, # nolint: object_name.
event.env = parent.frame(), # nolint: object_name.
handler.env = parent.frame(), # nolint: object_name.
...) {
event_quo <- rlang::new_quosure(substitute(eventExpr), env = event.env)
handler_quo <- rlang::new_quosure(substitute(handlerExpr), env = handler.env)

# When `condExpr` is TRUE, then `handlerExpr` is evaluated once.
activator <- reactive({
if (isTRUE(rlang::eval_tidy(event_quo))) {
TRUE
}
})

observeEvent(
eventExpr = activator(),
once = TRUE,
handlerExpr = rlang::eval_tidy(handler_quo),
...
)
}
17 changes: 17 additions & 0 deletions R/teal_data_module.R
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ teal_data_module <- function(ui, server, label = "data module", once = TRUE) {
#' `shiny` module server function; that takes `id` and `data` argument,
#' where the `id` is the module id and `data` is the reactive `teal_data` input.
#' The server function must return reactive expression containing `teal_data` object.
#'
#' The server function definition should not use `eventReactive` as it may lead to
#' unexpected behavior.
#' See `vignettes("data-transform-as-shiny-module")` for more information.
#' @param datanames (`character`)
#' Names of the datasets that are relevant for the module. The
#' filter panel will only display filters for specified `datanames`. The keyword `"all"` will show
Expand Down Expand Up @@ -149,6 +153,19 @@ teal_transform_module <- function(ui = function(id) NULL,
ui = ui,
server = function(id, data) {
data_out <- server(id, data)

if (inherits(data_out, "reactive.event")) {
# This warning message partially detects when `eventReactive` is used in `data_module`.
warning(
paste(
"srv_teal_data",
"Using eventReactive in teal_transform module server code should be avoided as it",
"may lead to unexpected behavior. See the vignettes for more information ",
"(`vignette(\"data-transform-as-shiny-module\", package = \"teal\")`)."
)
averissimo marked this conversation as resolved.
Show resolved Hide resolved
)
}

decorate_err_msg(
assert_reactive(data_out),
pre = sprintf("From: 'teal_transform_module()':\nA 'teal_transform_module' with \"%s\" label:", label),
Expand Down
19 changes: 10 additions & 9 deletions inst/WORDLIST
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
Biomarker
CDISC
Forkers
Hoffmann
MAEs
ORCID
Reproducibility
TLG
UI
UX
bookmarkable
CDISC
cloneable
customizable
favicon
favicons
Forkers
funder
Hoffmann
lockfile
MAEs
omics
ORCID
pre
programmatically
quosure
reactively
repo
Reproducibility
reproducibility
summarization
tabset
themer
theming
TLG
UI
uncheck
UX
44 changes: 44 additions & 0 deletions man/call_once_when.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion man/teal_transform_module.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 27 additions & 15 deletions tests/testthat/test-module_teal.R
Original file line number Diff line number Diff line change
Expand Up @@ -1588,25 +1588,37 @@ testthat::describe("srv_teal teal_module(s) transformer", {
})

testthat::it("fails when transformer doesn't return reactive", {
testthat::expect_error(
testServer(
app = srv_teal,
args = list(
id = "test",
data = teal.data::teal_data(iris = iris),
modules = modules(
module(
server = function(id, data) data,
transformers = list(
teal_transform_module(
ui = function(id) NULL,
server = function(id, data) "whatever"
testthat::expect_warning(
averissimo marked this conversation as resolved.
Show resolved Hide resolved
# error decorator is mocked to avoid showing the trace error during the
# test.
# This tests works without the mocking, but it's more verbose.
testthat::with_mocked_bindings(
testServer(
app = srv_teal,
args = list(
id = "test",
data = teal.data::teal_data(iris = iris),
modules = modules(
module(
server = function(id, data) data,
transformers = list(
teal_transform_module(
ui = function(id) NULL,
server = function(id, data) "whatever"
)
)
)
)
)
),
expr = {
session$setInputs("teal_modules-active_tab" = "module")
session$flushReact()
}
),
expr = {}
decorate_err_msg = function(x, ...) {
testthat::expect_error(x, "Must be a reactive")
warning(tryCatch(x, error = function(e) e$message))
},
),
"Must be a reactive"
)
Expand Down
4 changes: 4 additions & 0 deletions vignettes/data-transform-as-shiny-module.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ if (interactive()) {
}
```

_Note_: It is recommended to return `reactive()` with `teal_data()` in `server` code of a `teal_transform_module` as this is more robust for maintaining the reactivity of Shiny.
If you are planning on using `eventReactive()` in the server, the event should include `data()` _(example `eventReactive(list(input$a, data()), {...})`)_.
More in [this discussion](https://github.com/insightsengineering/teal/issues/1303#issuecomment-2286239832).

### Multiple Transformers

Note that we can add multiple `teal` transformers by including `teal_transform_module` in a list.
Expand Down
Loading