Triggering initial insertUI event programmatically in a Shiny module

The app below contains a selectInput (input$month) and a button, Show Modal, that launches a modal window when clicked. The modal contains a button, Add UI, that inserts some text, Element x, where x is the value of a counter that increases by 1 each time Add UI is clicked.

Screenshot of app on startup with the modal window opened:

The app is modularised so that UI associated with the modal is rendered by function modUI and the corresponding server logic is defined in function modServer. modUI is rerendered each time input$month changes to overwrite previously inserted text elements.

I require the first block of text to be inserted programmatically, so that when the app loads OR the user changes input$month, Element 1 is already rendered in the modal. To do this, I tried to get the insertUI observer to fire if input$add_ui greater than or equal to 0 - i.e. observeEvent(if(req(input$add_ui) >= 0) TRUE else return(), { #insertUI expr }). However, this doesn't work and I don't understand why. Since observers are eagerly evaluated, shouldn't this observer fire after input$add_ui has finished initialising?

The counter must also be reset to 0 each time input$month changes so that inserted text elements start at Element 1. To do this, I included the following observer in modServer:

observe({
    req(input$add_ui == 0) #checking that modUI has finished rerendering
    print(paste('month changed to:', month, 'resetting counter')); 
    counter(0)
  })

Lastly, I am relatively new to modules and was wondering if someone could explain why counter() is not reset automatically whenever modServer is called via observe(callModule(modServer, 'hi', month = input$month))? Why does its value persist if the module has its own environment?

I would be grateful for any help as I have been stuck on this for a while.

Code to reproduce the above:

library(shiny)
library(shinyBS)

#MODULE UI ----
modUI <- function(id) {
  
  ns <- NS(id)
  
  tagList(
    actionButton(ns("show_modal"), "Show modal"),
    bsModal(
      id = ns('modal'),
      trigger = ns('show_modal'),
      
      actionButton(ns("add_ui"), "Add UI"),
      tags$div(id = ns("placeholder1"))
    )
  )
}

#MODULE SERVER ----
modServer <- function(input, output, session, month) {
  
  ns <- session$ns
  
  counter <- reactiveVal(0)
  
  # Observer to insert UI element
  observeEvent(if(req(input$add_ui) >= 0) TRUE else return(), {
    
    counter(counter() + 1)
    
    insertUI(
      selector = paste0("#", ns("placeholder1")),
      ui = tags$div(paste('Element', counter()))
    )
  })
  
  # Reset counter() if month is changed
  observe({
    print(paste('month changed to:', month(), 'resetting counter')); 
    counter(0)
  }, ignoreInit = T)
  
  # Print
  observe({ print(paste('input$add_ui:', input$add_ui, 'counter:', counter())) })
  
}

#MAIN UI ----
ui <- fluidPage(
  tagList(
    selectInput('month', 'Month', month.abb),
    uiOutput('modal_ui')
  )
)

#MAIN SERVER ----
server <- function(input, output, session) {
  
  #Call modUI if input$month is changed
  callModule(modServer, 'hi', month = reactive(input$month)) #previously observe(callModule(modServer, 'hi', month = input$month)), why isn't this allowed?
  
  #Rerender modUI if input$month is changed
  output$modal_ui <- renderUI({
    input$month
    modUI('hi')
  })
  
  observe(print(input$month))
}

shinyApp(ui = ui, server = server)
1 Like

Hi,

I'm not familiar with modules, but after playing with the code I noticed that the behavior of the input$month was odd (would not always render the expected response). I found that this might be because you're trying to pass a Shiny variable into a module, which is not allowed by design according to this post:

Could be a start...

PJ

Hi PJ,

Thanks for replying.

the behavior of the input$month was odd (would not always render the expected response)

Would it be possible to elaborate on what the expected response is?

I think passing inputs to a module is allowed as long as it is wrapped inside a reactive expression, this is also stated the link provided in the other post:

If a module needs to access an input that isn’t part of the module, the containing app should pass the input value wrapped in a reactive expression (i.e. reactive(...) ):

callModule(myModule, "myModule1", reactive(input$checkbox1))

I have modified my app's code to read callModule(modServer, 'hi', month = reactive(input$month)) intead of observe(callModule(modServer, 'hi', month = input$month)), but I am not exactly clear as to why the latter is not allowed.

I understand the differences between observe and reactive (eager vs lazy, side-effects vs return values), but in both cases modServer is re-executed with the most up-to-date value of input$month, so the difference is not obvious to me.

Hi,

First of all: that reactive wrapping of the month input did the trick and made it predictable again (it seemed before it would only react once, then stop)

More importantly, I found a solution for your issue, I'll try and explain the best I can. But first the code:

library(shiny)
library(shinyBS)

#MODULE UI ----
modUI <- function(id) {
  
  ns <- NS(id)
  
  tagList(
    actionButton(ns("show_modal"), "Show modal"),
    bsModal(
      id = ns('modal'),
      trigger = ns('show_modal'),
      
      actionButton(ns("add_ui"), "Add UI"),
      tags$div(id = ns("placeholder1"))
    )
  )
}

#MODULE SERVER ----
modServer <- function(input, output, session, month) {
  
  ns <- session$ns
  
  counter <- reactiveVal(0)
  
  # Observer to insert UI element
  observeEvent(c(input$add_ui, input$show_modal), {
    cat("add_ui ", input$add_ui, "- counter: ", counter(), "\n")
    req(input$add_ui == counter())
    
    counter(counter() + 1)
    
    insertUI(
      selector = paste0("#", ns("placeholder1")),
      ui = tags$div(paste('Element', counter()))
    )
  })
  
  # Reset counter() if month is changed
  observeEvent(month(),{
    print(paste('month changed to:', month(), 'resetting counter')); 
    counter(0)
  })
  
}

#MAIN UI ----
ui <- fluidPage(
  tagList(
    selectInput('month', 'Month', month.abb),
    uiOutput('modal_ui')
  )
)

#MAIN SERVER ----
server <- function(input, output, session) {
  
  #Call modUI if input$month is changed
  callModule(modServer, 'hi', month = reactive(input$month)) #previously observe(callModule(modServer, 'hi', month = input$month)), why isn't this allowed?
  
  #Rerender modUI if input$month is changed
  output$modal_ui <- renderUI({
    input$month
    modUI('hi')
  })

}

shinyApp(ui = ui, server = server)

EXPLANATION
The function to focus on (where my important changes are) is the following:

observeEvent(c(input$add_ui, input$show_modal), {
    cat("add_ui ", input$add_ui, "- counter: ", counter(), "\n")
    req(input$add_ui == counter())
    
    counter(counter() + 1)
    
    insertUI(
      selector = paste0("#", ns("placeholder1")),
      ui = tags$div(paste('Element', counter()))
    )
  })

As you can see, I created an observeEvent that listens to both the 'show modal' button (SM) and the 'add UI' (AUI) one. This is important because if you click the SM the first time, you like it to add the first element. BUT... you only want this the first time, then it should not do that anymore until you reset the whole thing by changing the month. Therefor, we require the counter value to be equal or greater than the AUI button counter (= times clicked). The first time you click the modal, the AUI button has not been clicked (i.a. = 0) and since the counter is 0 it will execute the code and add 1 to the counter. When you close out and click the SM again, the counter is 1, but the AUI still 0 and thus nothing happens. When you click the AUI the AUI value goes up and matches the counter and the function will execute. When you change the month, the whole modUI resets and thus AUI internal click counter too (plus the counter as in the code) and the whole thing starts again.

Ok I know it sounds convoluted an explanation, but run the code (and look at console output) and it'll hopefully make more sense!

Hope this helped,
PJ

2 Likes

This is awesome, thank you so much PJ!

Would you mind explaining why we have to listen for input$show_modal? The observer seems to work as expected without it.

Also, is the > in input$add_ui >= counter() necessary? I can't think of a case where input$add_ui would be greater than counter().

Hi,

The code was a result of a lot of experimentation and indeed upon taking your observations into account can be even simplified. You are correct that the >= can be ==, as there is no scenario where it can be greater.

But regarding the input$show_modal. It needs to be observed for one special scenario: where you change the month more than once before clicking the show modal button. If you don't observe the input$show_modal, the code won't run when the month get changed the second time and there will be no insert. Try it: remove the input$show_modal from the observeEvent, run the app, change the month multiple times before clicking the show modal and you'll see there is no insert. I don't fully understand myself why this is happening, but again I'm not very familiar with modules so it might be somewhere in that reactivity.

I updated the code.

Grtz,
PJ

I was able to reproduce that behaviour after changing the month 5 times and clicking input$add_ui 5+ times for each month. But when I relaunched the app and tried again, it worked fine every time and I must have changed input$month no less than 8 times. No idea what's causing this inconsistency.

Another strange thing is that when I try observeEvent(input$add, { ... }), the observer doesn't fire on startup but when I wrap input$add in c(), i.e. observeEvent(c(input$add), { ... }), it does. Here is a screenshot of the app on startup with just input$add_ui in the event expr:

Screenshot of app with c(input$add_ui) in the event expr:

I hope someone who is more familiar with modules could shed some light on why this is happening. In the meantime, thank you @pieterjanvc for all your help.

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.