Accessing Parent Namespace Inside a Shiny Module

Accessing parent namespace inside a Shiny Module

Took me a while, but I found a way to get the sub-module to update the super-module.

Shiny is designed so that access to other modules must be done via module arguments or returned values. We can not pass the widget ID between modules, but we can pass the session information of the parent.

library(shiny)

moduleUI <- function(id) {
ns <- NS(id)
uiOutput(ns("my_link"))
}

module <- function(input, output, session, number, parent) {
output$my_link <- renderUI({
actionLink(session$ns("link"), paste0("This is a link to ", number))
})

observeEvent(input$link,{
updateSelectInput(session = parent,"selectInput",selected = number) ### use parent session
})
}

ui <- fluidPage(
selectInput("selectInput","Choose one option",choices=seq(1,10),selected=1),
moduleUI("5"),
moduleUI("10")
)

server <- function(session,input, output) {
callModule(module = module, id = "5", 5, parent = session) ### pass session information
callModule(module = module, id = "10", 10, parent = session) ### pass session information
}

shinyApp(ui = ui, server = server)

In particular note that:

  • we pass the current session information when the sub-module is called
  • we use the parent session when updating the input selector

Module inside module shiny

Change

callModule(serverForModal, ns("test2"))

to

callModule(serverForModal, "test2")

Using function inside module in Shiny - working with namespace

Do not use ns in the function:

tabpanel_content <- function(radio_id, plotly_id) {
div(
id = "div_1",
style = "",
div(
class = "inputs ",
style = "width: 100%",
radioGroupButtons(
inputId = radio_id,
label = "",
choices = c("Input_1", "Input_2"),
status = "success"
)
),
div(
style = "",
plotlyOutput(plotly_id, height = 200, width = "auto")
)
)
}

Use it in the module:

mod_1_UI <- function(id) {
ns <- NS(id)
tagList(
tabsetPanel(
tabPanel(
strong("TabPanel - Title"),
tabpanel_content(
radio_id = ns("radio_input_1"), plotly_id = ns("plotly_chart_1")
),
tabpanel_content(
radio_id = ns("radio_input_2"), plotly_id = ns("plotly_chart_2")
),
tabpanel_content(
radio_id = ns("radio_input_3"), plotly_id = ns("plotly_chart_3")
)
)
)
)
}

Is it possible to refer to namespace from another module?

As I mentioned in my comment above, the canonical way would be to capture the button push as an output of module moduleServer2 and use this as an input for test-btn where you perform the action.

However, if you want to mess around with the namespaces by yourself (not recommended), you can use the following solution. I had to adapt the leafletProxy function, because the normal implementation automatically adds the namespace of the calling module. This is what you don't want, because you want to use the namespace of a different module.

Now with code adapted to the edit:

library(shiny)
library(mapboxer)
library(dplyr)
library(sf)
library(leaflet)

leafletProxy2 <- function (mapId, session = shiny::getDefaultReactiveDomain(),
data = NULL, deferUntilFlush = TRUE)
{
if (is.null(session)) {
stop("leafletProxy must be called from the server function of a Shiny app")
}
structure(list(session = session, id = mapId, x = structure(list(),
leafletData = data), deferUntilFlush = deferUntilFlush,
dependencies = NULL), class = "leaflet_proxy")
}

# UI 1 #
mod_btn_UI1 <- function(id) {

ns <- NS(id)
tagList(
actionButton(ns("btn1"), "Button 1"),
mod_btn_UI2(ns("moduleServer2")),
leafletOutput(ns("map"))
)
}

# Server 1 #
mod_btn_server1 <- function(id){
moduleServer(id, function(input, output, session) {

coords <- quakes %>%
sf::st_as_sf(coords = c("long","lat"), crs = 4326)

mod_btn_server2("moduleServer2", coords) # here is nested module2

output$map <- leaflet::renderLeaflet({
leaflet::leaflet() %>%
leaflet::addTiles() %>%
leaflet::setView(172.972965,-35.377261, zoom = 4) %>%
leaflet::addCircleMarkers(
data = coords,
stroke = FALSE,
radius = 6)
})

observeEvent(input$btn1, {
leaflet::leafletProxy("map", data = coords) %>%
leaflet::addCircles()
})

})
}

# Module 2 - UI #
mod_btn_UI2 <- function(id){
ns <- NS(id)
actionButton(ns("btn2"), "Button 2")
}

# Module 2 - server #
mod_btn_server2 <- function(id, dataMod, btn){
moduleServer(id, function(input, output, session) {

# and here I refer to 'map' located in the first module
observeEvent(input$btn2, {
leafletProxy2("test-btn-map", data = dataMod) %>%
leaflet::addCircles(stroke = TRUE,
color = "red")
})

})
}

# App #

ui <- fluidPage(

tagList(
mod_btn_UI1("test-btn"))

)

server <- function(input, output, session) {

mod_btn_server1("test-btn")

}

shinyApp(ui = ui, server = server)

Here is a more canonical form that works with the correct input/output of modules and doesn't mess with namespaces:

library(shiny)
library(mapboxer)
library(dplyr)
library(sf)
library(leaflet)

# UI 1 #
mod_btn_UI1 <- function(id) {

ns <- NS(id)
tagList(
actionButton(ns("btn1"), "Button 1"),
mod_btn_UI2(ns("moduleServer2")),
leafletOutput(ns("map"))
)
}

# Server 1 #
mod_btn_server1 <- function(id){
moduleServer(id, function(input, output, session) {

ns <- NS(id)

coords <- quakes %>%
sf::st_as_sf(coords = c("long","lat"), crs = 4326)

external_btn <- mod_btn_server2("moduleServer2", coords) # here is nested module2

output$map <- leaflet::renderLeaflet({
leaflet::leaflet() %>%
leaflet::addTiles() %>%
leaflet::setView(172.972965,-35.377261, zoom = 4) %>%
leaflet::addCircleMarkers(
data = coords,
stroke = FALSE,
radius = 6)
})

observeEvent(input$btn1, {
leaflet::leafletProxy("map", data = coords) %>%
leaflet::addCircles()
})

observeEvent(external_btn(), {
leaflet::leafletProxy("map", data = coords) %>%
leaflet::addCircles(stroke = TRUE,
color = "red")
})

})
}

# Module 2 - UI #
mod_btn_UI2 <- function(id){
ns <- NS(id)
actionButton(ns("btn2"), "Button 2")
}

# Module 2 - server #
mod_btn_server2 <- function(id, dataMod){
moduleServer(id, function(input, output, session) {

return(reactive(input$btn2))

})
}

# App #

ui <- fluidPage(

tagList(
mod_btn_UI1("test-btn"))

)

server <- function(input, output, session) {

mod_btn_server1("test-btn")

}

shinyApp(ui = ui, server = server)

Inserting UI into R shiny moduleServer

Here is the working code. A lot you need to notice. Here are some key points.

  1. Remember to add ns for all modules.
  2. renderUI only returns one object, if you want to return more than one components, use tagList or div.
  3. insertUI selector inside module also needs to add ns.

Check the code yourself for minor bugs.

library(shiny)
library(shinydashboard)
library(shinydashboardPlus)
library(shinyWidgets)
library(dplyr)

ui_module2 = function(id) {
ns = NS(id)
uiOutput(ns("finalOut"))
}

server_module2 = function(id) {
moduleServer(id,
function(input, output, session) {
output$finalOut = renderUI({
ns = session$ns
div(
# not sure what you want with next line, commented, fix your own
# textOutput(p(style = "color: red", paste0("Package Num:", id, sep = "-"))),
sliderInput(ns("n"), "N", 1, 1000, 500),
textInput(ns("label"), "Label")
)
})
})
}

ui_module1 = function(id) {
ns = NS(id)
actionBttn(ns("actionbutton1"), "Press Button For New UI")
}

server_module1 = function(id) {
moduleServer(id,
function(input, output, session) {
ns <- session$ns
observeEvent(input$actionbutton1, {
i = sprintf('%04d', input$actionbutton1)
id = sprintf('static%s', i)

print(id)

insertUI(selector = paste0("#", ns("actionbutton1")),
where = "afterEnd",
immediate = TRUE,
ui = ui_module2(ns(id)))
server_module2(id)
})
})
}

ui = fluidPage(ui_module1("opt"))

server = function(input, output, session) {
server_module1("opt")
}

shinyApp(ui, server)

understand how it works

This case is an example of nested modules. To understand this, you need to know how ns namespace works in Shiny.
As Shiny's official document, they tell you that you need to add ns(id) on the module UI and you don't need to do so for the module server, but they didn't tell you why.

When you call the moduleServer(id, function(input, output, session) {...}), the session here is different than your top-level session. If you check the session here, it's a session_proxy class object, no longer the ShinySession class object. This is how shiny internally knows how expressions under moduleServer are inside the module. When you call input, output, renderXX, updateXX methods under the module scope, it will first query the session object, and if it is session_proxy, call the session$ns to attach namespace first. This is why you don't need to add ns() on the server, shiny did it for you.

Ok, let's go back to the nested module problem. Let's see what's namespace on both UI and server with this simple example. All it does is print out the namespace on UI and server.

library(shiny)

uiMod2 <- function(id) {
ns <- NS(id)
div(id = ns(""))
}

serverMod2 <- function(id) {
moduleServer(
id,
function(input, output, session) {
ns <- session$ns
print(ns(""))
})
}

uiMod1 <- function(id) {
ns <- NS(id)
tagList(
div(id = ns("")),
uiMod2(ns("level2"))
)

}

serverMod1 <- function(id) {
moduleServer(
id,
function(input, output, session) {
ns <- session$ns
print(ns(""))
serverMod2("level2")
})
}

ui <- fluidPage(uiMod1("level1"))
server <- function(input, output, session) {serverMod1("level1")}
shinyApp(ui, server)

Results UI

Sample Image

Server

[1] "level1-"
[1] "level1-level2-"

See the difference? I didn't call ns for second-level server, only serverMod2("level2"), but the namespace is automatically attached for me. However, shiny doesn't know to apply this to the UI, so I have to use uiMod2(ns("level2")).

The reason why shiny doesn't know is all about session. When you create a module, you need to pass function(input, output, session), and when you create nested modules, this session for the second level module is not the ShinySession, but a session_proxy, so parent namespace(s) will be appended automatically.

Things are different with UI. Normally When you create module UI like uiMod2 <- function(id) ..., you don't pass the session object, so it doesn't know what namespace to inherit.

Communicate from JS to Shiny in a module

I have to admit that I don't fully get the scope of your question, so please tell me if I misunderstood your intentions/reasons. I think what in your design makes problems is that you try to define a button that is the main server scope but is defined from within a module; this is not how the shiny module system is designed (additionally, the button ui has a different id than the shiny input).

If you respect the namespaces of the module system and use the same id for the button ui & shiny input, you can simplify your my_button function because the namespace is automatically added to the id:

library(shiny)

js_handler <- HTML("$(function() {
$(document).on('click', '.my-button', function() {
$me = $(this);
var bttn_id = $me.attr('id');
var id = Math.random();
Shiny.setInputValue(bttn_id, id);
});
})")

my_button <- function(id, label) {
tagList(
tags$button(id = id,
type = "button",
class = "btn btn-default my-button",
label),
tags$head(singleton(tags$script(js_handler)))
)
}

test_ui <- function(id) {
ns <- NS(id)
tagList(
my_button(ns("btn1"), "Send To R (readable only from module)"),
verbatimTextOutput(ns("output"))
)
}

test_server <- function(id) {
moduleServer(id, function(input, output, session) {
output$output <- renderPrint(req(input$btn1))
})}

shinyApp(ui = fluidPage(h4("Module"), test_ui("test"),
h4("Main"),
my_button("btn2", "Send To R (readable only at main)"),
verbatimTextOutput("output")),
server = function(input, output, session) {
test_server("test")
output$output <- renderPrint(req(input$btn2))
})

Would that work for you?

Call updateTabItems from inside Shiny module

You can find the answer in this post: Accessing parent namespace inside a shiny module

Basically, in updateTabItems() inside a moulde, you need to call the parent's session, not the session of the modul.

Thus, add a variable for your session to callModule() and call it in updateTabItems().



Related Topics



Leave a reply



Submit