teal icon indicating copy to clipboard operation
teal copied to clipboard

[Question]: Making easier to customize modules?

Open llrs-roche opened this issue 3 months ago • 2 comments

What is your question?

This issue documents discussions about how teal should work around modifications to modules. It grow up from #1597.

Need

App developers might want to change how some modules behave, both the UI and the server part. Examples observed:

  • Change the label of an input
  • Remove code blocks from the reporter for some or all modules
  • Modify output (customize plots, tables, ...)
  • Modify data input (Filter data, apply custom modifications, ...)
  • Disable sidebar: #1601

Current state

There are several functions to allow modification of existing teal_modules:

  • make_teal_transform_server: creates a new module with a reactive evaluating the expression provided It can modify the output of the module by modifying the code. It uses a moduleServer.
  • teal_transform_module: it only modifies the output (but accept and ui function), usage of make_teal_transform_server is suggested for the server argument. It uses a moduleServer.
  • modify_header, modify_title, modify_footer : These functions can modify specific text/labels of the UI.
  • after: Function that modifies the UI and the server (without restrictions)

There are also arguments in modules to modify them:

  • transformators: Modify data input
  • decorators (experimental): Modify data output (via teal_transform_module)

Comments

During the addition of decorators and transformators arguments, it was considered whether to add a parameter or as an external function.

Some effort should be done to avoid having too many functions exported. Especially when the names can be confusing (all with transform on them).

after function has arguments that accept what looks like module functions but they are not. This could confuse users of the function.

Using a function like after should be compatible with using a decorator and/or a transformator as arguments on the module.

Open questions

UI

Developers might want to update, add or remove UI elements: How to help app-developers avoiding problems like not returning the UI, duplicating it, removing a necessary input, ...? How should we give the freedom to place new UI elements? Using an argument like .cssSelector with some default or freely with unrestricted capabilities.

Server

Changes can be applied before module execution like transformators and/or after the module execution like decorators: How should we control that? Where to allow them:

  • As an expression evaluated on data?
  • Inside a reactive object?
  • Inside the server function module? If the expressions are allowed on multiple places, how could the package distinguish each case?

Code of Conduct

  • [x] I agree to follow this project's Code of Conduct.

Contribution Guidelines

  • [x] I agree to follow this project's Contribution Guidelines.

Security Policy

  • [x] I agree to follow this project's Security Policy.

llrs-roche avatar Sep 26 '25 09:09 llrs-roche

On #1597 the after() function is renamed to transform() and becomes a method. The main difference between transform() and teal_transform_module() is that the later generates a module. This , as far as I know, limits the modifications that can be done on the previous or next module.

`transform()` vs `decorator`

The transform() method is able to modify the module more profoundly than the decorators. Compare these two modules that are equivalent on the UI and functionality but compare

library("teal.reporter")

# UI+SRV after: transform vs decorators  ####

init(
  data = teal_data(IRIS = iris, MTCARS = mtcars),
  modules = example_module() |>
    transform(
      ui = function(id, elem) {
        ns <- NS(id)
        check_box <- checkboxInput(ns("src"), "Include R Code in the report", TRUE)
        htmltools::tagAppendChild(elem, check_box,
          .cssSelector = ".standard-layout .sidebar .sidebar-content"
        )
      },
      server = function(input, output, session, data) {
        teal_card(data) <- c(teal_card(data), teal_card("Modification"))
        if (!input$`wrapper-src`) {
          teal_card(data) <- Filter(function(x) !inherits(x, "code_chunk"), teal_card(data))
        }
        data
      },
    when = "after")
) |> runApp()

init(
  data = teal_data(IRIS = iris, MTCARS = mtcars),
  modules = example_module(decorators = list(teal_transform_module(
    ui = function(id) {
      ns <- NS(id)
      check_box <- checkboxInput(ns("src"), "Include R Code in the report", TRUE)
      tags$div(check_box)
      # htmltools::tagAppendChild(elem, check_box,
      #                           .cssSelector = ".standard-layout .sidebar .sidebar-content"
      # )
    },
    server = function(id, data) {
      moduleServer(id, function(input, output, session) {
        
        reactive({
          d <- data()
          teal_card(d) <- c(teal_card(d), teal_card("Modification"))
          if (!input$src) {
            teal_card(d) <- Filter(function(x) !inherits(x, "code_chunk"), teal_card(d))
          }
          d
        })
      })
      
    }
  )))
) |> runApp()
The decorator generates a report with one code block at the end. Image
While `transform()` doesn't Image
`transform()` vs `transformer`

Transformers are modules that are read from the argument name. Allowing transform() to add modifications before, as transformers as an attribute requires modifying the transformer feature to include them. However, as seen on the decorators the addition of a new module isn't equivalent as modifying the module itself.

Independent modification of module's UI and server

UI and server elements of a module are functions. To distinguish when transform needs to apply a Using dispatch on the arguments of the functions used caused an error. The dispatch on the server function didn't contain the session required for it to work. If needed we could provide some helpers using the proposed changes on https://github.com/insightsengineering/teal/pull/1597 to only modify the server or the UI: they would be some wrappers with ui = function(id, elem) elem and server = function(data) data respectively. This could have the benefit of keeping the when argument, otherwise to keep it would need to pass the responsibility of modifying the module at the right time to the respective transform_{ui,server}() function for the UI and server.

Thinking about the different functions and when we want to allow what I created this scheme:

Image

On the top part how the when parameter is supposed to work. The method receives a module which is modified before receiving data or after. On the central piece, a representation of what happens in each module: There is the shiny namespace, the module namespace and the qenv/teal_data/teal_card namespace. teal_transform_module() used by transformers and decorators generates a module that is added to the application. The module's server part can be generated with make_teal_transform_server(), which simplifies about ~9 lines of (template) code for small cases, but potentially one line for each extra input.

Comparison of an app with `make_teal_transform()` and one only with `teal_transform_module()`
# make_teal_transform_server vs transformator  ####
init(
  data = teal_data(iris = iris),
  modules = example_module(transformators = teal_transform_module(
    label = "Simplified interactive transformator for iris",
    datanames = "iris",
    ui = function(id) {
      ns <- NS(id)
      numericInput(ns("n_rows"), "Subset n rows", value = 6, min = 1, max = 150, step = 1)
    },
    server = make_teal_transform_server(expression(iris <- head(iris, n_rows)))
  ))
) |> runApp()


init(
  data = teal_data(iris = iris),
  modules = example_module(transformators = teal_transform_module(
    label = "Simplified interactive transformator for iris",
    datanames = "iris",
    ui = function(id) {
      ns <- NS(id)
      numericInput(ns("n_rows"), "Subset n rows", value = 6, min = 1, max = 150, step = 1)
    },
    server = function(id, data) {
      moduleServer(id, function(input, output, session) {
        reactive({
          within(data(), {
            iris <- head(iris, n_rows)
          },
          n_rows = input$n_rows)})
      })
    })
  )
) |> runApp()

On the last section of the scheme there is a representation of the tradeoff of rights given to the user and what responsibility they need to exercise then.
With teal_transform_module() there is a default placement of the UI, output and when is clearly defined on the module creation by the different argument names. With the transform() method the modifications can be on the data level and on the module level and users need to manage the placement of the UI or any modification on it and when the modification takes place.

llrs-roche avatar Sep 30 '25 10:09 llrs-roche

After today discussion some more comments:

transform() vs decorators: Decorators always (implicitly) print the object being decorated. So transform() and decorators are not 100% equal.

Several question arose:

  • when = "after" with no elem on ui() function: What should happen with the ui?

    In my opinion as it cannot modify UI and there is no default placement transform() should create one (Maybe a pop-up for any (new) ui could be shown)

  • when = "before" with elem on the ui function?

    This covers the need to update the labels of the UI. So we should allow to modify the UI.

  • transform is a generic word, but the transform() function has limitations like not being able to modify the module itself: How can cover most cases and avoid having users request more time on this?

    Perhaps cover the limitations through the documentation, examples and the vignette. Also by providing enough helpers that use the functionality so that most app-developers or even module-developers don't need to use it directly.

llrs-roche avatar Oct 01 '25 11:10 llrs-roche