--- title: "Building Custom Modules" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Building Custom Modules} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ``` ## Introduction The modules in **VizModules** are designed to be composed and extended. You can build higher-level modules that add custom logic such as data filtering, transformations, or additional UI controls while reusing the full functionality of the base modules. This vignette demonstrates how to create a custom module by building on top of the `scatterPlot` module. ## The Pattern When building a custom module, you need to handle Shiny's namespacing correctly. The key insight is: 1. **Your module's custom inputs** need to be namespaced with `NS(id)`. 2. **The base module's UI and server functions** should receive the bare `id`, not a namespaced version. 3. **Data processing** that requires access to your module's inputs must happen inside a `moduleServer()` block. 4. **The base module's server** should be called *outside* that `moduleServer()` block to avoid double-namespacing issues. ## A Minimal Example Let's build a custom module that adds a simple filtering checkbox to the `scatterPlot` module. ### The UI ```{r module-ui} library(VizModules) minimalModuleUI <- function(id) { ns <- NS(id) tagList( h4("Minimal Module Controls"), # Custom input - uses the module's namespace checkboxInput(ns("filter_setosa"), "Start with Setosa Only", value = FALSE), hr(), # Base module UI - pass the bare 'id', not ns(id) dittoViz_scatterPlotInputsUI(id, iris) ) } minimalModuleOutput <- function(id) { # Simply delegate to the base module's output UI dittoViz_scatterPlotOutputUI(id) } ``` Notice that `checkboxInput()` uses `ns("filter_setosa")` to namespace the custom input, while `dittoViz_scatterPlotInputsUI()` receives the bare `id`. This ensures the base module creates its inputs in the correct namespace. ### The Server ```{r module-server} minimalModuleServer <- function(id, data_reactive) { # Step 1: Process data inside a moduleServer block # This gives us access to inputs namespaced to 'id' (our module's inputs) filtered_data <- moduleServer(id, function(input, output, session) { reactive({ req(data_reactive()) df <- data_reactive() # Input specific to this custom module if (isTRUE(input$filter_setosa)) { if ("Species" %in% names(df)) { df <- df[df$Species == "setosa", ] } } df }) }) # Step 2: Call the base module server OUTSIDE the moduleServer block # This is critical! If we called this inside the moduleServer above, # dittoViz_scatterPlotServer would look for inputs at id-id-inputName instead of id-inputName dittoViz_scatterPlotServer(id, filtered_data) } ``` **Why this pattern?** - `moduleServer(id, ...)` gives us access to `input$filter_setosa`, which is namespaced to our wrapper's `id`. - By calling `dittoViz_scatterPlotServer(id, filtered_data)` *outside* the `moduleServer()` closure, the base module attaches to the same namespace as our UI, not a nested one. - If we called `dittoViz_scatterPlotServer()` inside the `moduleServer()` block, it would create nested namespaces like `id-id-x_axis`, which wouldn't match the actual input IDs in the UI. ### Putting It Together ```{r full-app} ui <- fluidPage( titlePanel("Minimal Module Example"), sidebarLayout( sidebarPanel( minimalModuleUI("demo") ), mainPanel( minimalModuleOutput("demo") ) ) ) server <- function(input, output, session) { # Pass a reactive data source to the module minimalModuleServer("demo", reactive({ iris })) } shinyApp(ui, server) ``` ## Hiding Base Module Inputs If your module pre-sets certain parameters, you can hide those inputs from the user to keep them from being changed: ```{r hiding-inputs} focusedModuleUI <- function(id) { ns <- NS(id) tagList( h4("Simplified Scatter Plot"), # The UI takes no hide.* arguments -- see the server below. dittoViz_scatterPlotInputsUI(id, iris) ) } focusedModuleServer <- function(id, data_reactive) { # hide.inputs and hide.tabs are arguments of the *Server*, not the *InputsUI. dittoViz_scatterPlotServer(id, data_reactive, hide.inputs = c("shape.by", "color.by") ) } ``` ## Driving Base Module Inputs From Your Wrapper Hiding an input fixes it to one value. When a base module parameter instead needs to *follow* something your wrapper computes, pass a `reactive()` as that entry of `defaults` rather than reaching for `update*Input()`. The key must name an input the module actually reads -- `get_default()` falls back silently, so a `defaults` entry for an unexposed key does nothing at all. (`main` is the one to watch: no module exposes a plot title, so `defaults = list(main = ...)` has no effect.) ```{r reactive-defaults} colouredModuleServer <- function(id, data_reactive) { colour_col <- moduleServer(id, function(input, output, session) { reactive(if (isTRUE(input$filter_setosa)) "" else "Species") }) # The colour mapping tracks the checkbox, but the user can still change it. dittoViz_scatterPlotServer( id, data_reactive, defaults = list(color.by = colour_col) ) } ``` This is the recommended parent-to-child channel for parameter values. It resolves server-side, so the plot renders once per change instead of twice, and the control stays user-editable. See `vignette("defaults-and-hiding", package = "VizModules")` for the full semantics. ## Automatically Preserving Manual Title/Legend/Annotation Edits The base modules let users interactively drag and edit the plot title, legend, annotations, draggable axis titles, and continuous-colour legend (colorbar), and they re-apply those manual tweaks across re-renders. When you wrap a base module (as in the examples above), this persistence is inherited for free. If you build a brand-new `plotly` output from scratch inside a custom module (rather than delegating to a base module's server), you can add the same behaviour simply with 3 basic steps: ```{r manual-edits} customPlotServer <- function(id, data_reactive) { moduleServer(id, function(input, output, session) { # 1. A unique event source + the edit store, created once. plot_source <- session$ns("customplot") edit_store <- setup_manual_edits(input, session, plot_source) output$plot <- renderPlotly({ # 2. Create your plotly object with your plotting function fig <- build_my_plotly_figure(data_reactive(), input) # 3. Restore + capture edits on every render, then return the figure. finalize_manual_edits(fig, plot_source, edit_store, session) }) }) } ``` See the "Adding a New Module" vignette for the full description of these helpers, but this is pretty much all you need to do. ## Best Practices 1. **Keep wrapper logic focused**: Each wrapper should add a cohesive set of related functionality. 2. **Document the data requirements**: If your wrapper expects certain columns, a specific class or data structure, etc, document this clearly. 3. **Use reactive expressions**: Use reactive data inputs. 4. **Test the namespace**: If inputs aren't working, check that you're handling namespaces correctly. A common symptom of namespace issues is that inputs seem to have no effect. 5. **Consider composability**: Design your wrappers so they could potentially be wrapped by even higher-level modules. 6. **Prefer reactive `defaults` over `update*Input()`**: To drive a base module parameter from your wrapper's state, pass a `reactive()` in `defaults`. Pushing values with `update*Input()` costs an extra render on every change. When you must update one of your *own* inputs from the server, call `freezeReactiveValue()` on it first so readers pause instead of rendering with the stale value. Freezing does not cover an input you rebuild with `renderUI()`, though. In those cases, resolve the value server-side and have the plot read that instead, as `setup_group_colors()` does for a colour picker (see the "Updating Your Own Inputs From the Server" section in `vignette("adding-a-new-module", package = "VizModules")` for more info). See any of the existing modules for clear reference examples.