Skip to contents

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

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

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

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:

focusedModuleUI <- function(id) {
    ns <- NS(id)
    tagList(
        h4("Simplified Scatter Plot"),
        # Only show essential controls
        dittoViz_ScatterPlotInputsUI(id, iris,
            hide.inputs = c("shape.by", "color.by")
        )
    )
}

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 or data types, 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.

See Also

  • The base modules (dittoViz_ScatterPlotInputsUI, dittoViz_ScatterPlotOutputUI, dittoViz_ScatterPlotServer) are documented in the package reference.