When working on a larger app that has Shiny modules that include modules themselves, I've noticed that I can't put a downloadbutton in a renderUI call without it being disabled. Here's an example:
library(shiny)
# Module - top level
topLevel_UI <- function(id) {
ns <- NS(id)
tagList(
dl_UI(ns("my_module"))
)
}
topLevel_Server <- function(id) {
moduleServer(
id,
function(input, output, session) {
dl_Server("my_module")
}
)
}
# Module - lower level
dl_UI <- function(id) {
ns <- NS(id)
tagList(
uiOutput(ns("download_button"))
)
}
dl_Server <- function(id) {
moduleServer(
id,
function(input, output, session) {
ns <- NS(id)
output$download_button <- renderUI({
downloadButton(ns("download"), label = "Download file")
})
output$download <- downloadHandler(
filename = function() {"test.csv"},
content = function(file) {
write.csv(iris, file)
}
)
}
)
}
# Main app
ui <- fluidPage(
h1("Application"),
topLevel_UI("app")
# Including the lower level directly works
# , dl_UI("low")
)
server <- function(input, output, session) {
topLevel_Server("app")
# Including the lower level directly works
# , dl_Server("low")
}
shinyApp(ui, server)
When running the app, the button is disabled and cannot be clicked. If the module wasn't nested in topLevel module but called directly in main shiny app it works as intended (the comments in the code show this type of calling the module).
How can I fix it? It's probably due to namespacing issues.
I need the downloadButton to be in a renderUI call so I can have more logic before it.
You are correct. It's namespacing issue. In your dl_Server function, the local definition of ns should be
ns <- session$ns
not
ns <- NS(id)
With that change, your app works as expected. For further details see this page, specifically, the section Using renderUI within modules, on the Posit website.
Edit in response to OP's question in comment
You can't use NS and session$ns interchangably. They do different things - at least to an extent. NS("id") returns a function. session$ns("id") returns a string. [But, within a top level module with an id of "mod", NS("mod")("id"), session$ns("id") and NS("mod", "id") all return the same value: "mod-id".]
I think the problem has a number of causes. First, as I've demonstrated above, NS called with a single argument returns a function. When called with two arguments, it returns a string.
Further, Shiny documentation, examples and source code write code such as
modUI <- function(id) {
ns <- NS(id)
...
}
But the first parameter to NS is named not id but namespace:
> shiny::NS
function (namespace, id = NULL)
{
if (length(namespace) == 0)
ns_prefix <- character(0)
else ns_prefix <- paste(namespace, collapse = ns.sep)
f <- function(id) {
if (length(id) == 0)
return(ns_prefix)
if (length(ns_prefix) == 0)
return(id)
paste(ns_prefix, id, sep = ns.sep)
}
if (missing(id)) {
f
}
else {
f(id)
}
}
<bytecode: 0x130c46b00>
<environment: namespace:shiny>
Of course, a namespace is also an id, so the call is not incorrect, but it is misleading unless you have good understanding of what's going on under the hood.
So, in a top level module you could ignore either sessions$ns or NS, but not both. session$ns is a convenience that means you don't need to worry about the namespace within which the call is made. You don't have that luxury when using NS.
The subtlety comes when modules are nested. In a nested module NS(id) ignores the module hierarchy. session$ns does not. That explains why, in your example, you need session$ns.
For an extra level of detail, you can see how session$ns gets set by looking at the source of ShinySession$makeScope, which is called by shiny::callModule within shiny::moduleServer.