Modules are how larger Shiny apps stay maintainable, and glasstabs
widgets follow the same namespacing rule as every other input: apply
NS(id) to the widget IDs in the module UI, and pass the
module’s session plus bare input IDs everywhere else. The one mistake to
avoid is namespacing twice — never pass an already-namespaced ID to an
update function or a server helper.
ns <- shiny::NS("filters")
stopifnot(ns("region") == "filters-region")
stopifnot(ns("status") == "filters-status")NS("filters")("region") is
"filters-region". The module UI creates widgets under those
namespaced IDs; the module server talks about "region" and
"status" with its own session, and Shiny resolves them to
the same inputs.
Both select controls, their update functions, and a value shared with the parent:
filterUI <- function(id) {
ns <- NS(id)
tagList(
glassSelect(
ns("region"),
c(North = "north", South = "south"),
label = "Region",
selected = "north"
),
glassMultiSelect(
ns("status"),
c(Active = "active", Closed = "closed"),
label = "Status",
selected = "active",
show_style_switcher = FALSE
),
actionButton(ns("clear"), "Clear filters")
)
}
filterServer <- function(id) {
moduleServer(id, function(input, output, session) {
observeEvent(input$clear, {
updateGlassSelect(
session, "region",
selected = character(0)
)
updateGlassMultiSelect(
session, "status",
selected = character(0)
)
})
reactive({
list(
region = input$region,
status = input$status
)
})
})
}
ui <- fluidPage(
useGlassTabs(),
filterUI("filters"),
verbatimTextOutput("current")
)
server <- function(input, output, session) {
filters <- filterServer("filters")
output$current <- renderPrint(filters())
}
if (interactive()) shinyApp(ui, server)Three things to notice. First, the update calls take the module
session and the bare IDs ("region", "status"):
passing ns("region") here would address a nonexistent
"filters-filters-region" input. Second,
character(0) clears both widgets — for the single-select it
means “no selection” (the value becomes NULL), for the
multi-select it means an empty selection. Third, the parent never
touches input$filters-region directly; it reads the
module’s returned reactive, which keeps the module’s inputs an
implementation detail.
The same rule covers glassMultiSelectServer() and
glassSelectServer(): register them inside
moduleServer with the bare ID and the module session, and
the helper searches the module’s input:
filterServerWithSearch <- function(id, choices) {
moduleServer(id, function(input, output, session) {
glassMultiSelectServer("status", choices, session = session)
reactive({
input$status
})
})
}The widget itself still takes the namespaced ID in the module UI
(glassMultiSelect(ns("status"), ..., server = TRUE)); only
the server side uses the bare ID. The same holds for controls created by
renderUI() inside a module: register the server helper once
in the module server for that input ID, and the newly rendered control
receives search results through Shiny.
Because every ID is namespaced at the UI boundary, the same module can be instantiated twice with no collision:
ui <- fluidPage(
useGlassTabs(),
filterUI("left"),
filterUI("right")
)
server <- function(input, output, session) {
left <- filterServer("left")
right <- filterServer("right")
}Each instance keeps its own selection, its own server registration,
and its own update scope. If an update in one module ever seems to reach
the other, the cause is almost always a hardcoded full ID somewhere in
the UI — check that every widget ID goes through ns().