user51462
user51462

Reputation: 2022

R Shiny - Inserting dynamic UI inside a Shiny module

The app below contains a module that inserts a UI object each time the Add button is clicked. The UI object consists of two inputs:

  1. Input 1 is a selectInput with choices A and B.
  2. Input 2 is a textInput if the user chooses A and a numericInput if they choose B.

However, when I click Add, the inserted UI only contains Input 1 (the selectInput) - Input 2 is not rendered, as shown below:

enter image description here

Whereas the desired output looks like this:

enter image description here

I'm not sure if this is a namespacing issue or if there is a problem in the scoping of the module. Printing the IDs to the console checks out:

enter image description here

The app is as follows:

library(shiny)

# module UI function
modUI <- function(id){

  ns <- NS(id)

  tagList(
    actionButton(ns('add'), 'Add'),
    div(id = ns('placeholder'))
  )
}

# module server function
modServer <- function(input, output, session) {

  ns = session$ns

  ctn <- reactiveVal(0)

  Id <- reactive({
    function(id){
      ns(paste0(id, ctn()))
    }
  })

  observeEvent(input$add, {

    ctn(ctn() + 1)

    insertUI(
      selector = paste0('#', ns('placeholder')),
      ui = div(
        id = Id()('div'),
        selectInput(Id()('letter'), 'Letter:', LETTERS[1:2]),
        uiOutput(Id()('input'))
      )
    )
  })

  observeEvent(ctn(), {

    id <- Id()('input')
    selection <- Id()('letter')
    print(list(id = id, selection = selection))

    req(input[[selection]])

    output[[id]] <- renderUI({

      req(input[[selection]])

      switch(
        input[[selection]],
        'A' = textInput(Id()('text'), 'ENTER TEXT', ''),
        'B' = numericInput(Id()('numeric'), 'ENTER NUMBER', '') 
      )
    })

  }, ignoreInit = TRUE)
}

# main ui
ui <- fluidPage(
  modUI('mod1')
)

# main server
server <- function(input, output, session) {
  callModule(modServer, "mod1")
}

# run app
shinyApp(ui, server)

I tried splitting the module up into an inner and outer module. The inner mod creates Input 1 and Input 2 and the outer mod inserts them into the main app using insertUI. This gives me the same outcome as before though. The code for this can be viewed below:

library(shiny)


# INNER MOD ---------------------------------------------------------------

innermodUI <- function(id) {
  
  ns = NS(id)
  
  tagList(
    selectInput(ns('letter'), 'Letter:', LETTERS[1:2]),
    uiOutput(ns('names'))
  )
}

innermodServer <- function(input, output, session) {
  
  ns = session$ns
  
  output$names <- renderUI({
    
    selection = req(input$letter)
    
    switch(
      selection,
      'A' = textInput(ns('text'), 'ENTER TEXT', ''),
      'B' = numericInput(ns('numeric'), 'ENTER NUMBER', '')
    )
  })
}


# OUTER MOD ---------------------------------------------------------------

modUI <- function(id){
  
  ns <- NS(id)
  
  tagList(
    actionButton(ns('add'), 'Add'),
    div(id = ns('placeholder'))
  )
}

modServer <- function(input, output, session) {
  
  ns = session$ns
  
  ctn <- reactiveVal(0)
  
  Id <- reactive({
    function(id){
      ns(paste0(id, ctn()))
    }
  })
  
  observeEvent(input$add, {
    
    ctn(ctn() + 1)
    
    filterId = Id()('filter')
    
    insertUI(
      selector = paste0('#', ns('placeholder')),
      ui = innermodUI(filterId)
    )
    
    callModule(innermodServer, filterId)
    
  })
}


# MAIN --------------------------------------------------------------------

ui <- fluidPage(
  modUI('mod1')
)

server <- function(input, output, session) {
  callModule(modServer, "mod1")
}

shinyApp(ui, server)

I also tried wrapping the renderUI in a shinyjs::delay() to no avail. I would really appreciate any help on this since I'm not well-versed in Shiny modules and don't know what to try next.

Upvotes: 5

Views: 1656

Answers (1)

St&#233;phane Laurent
St&#233;phane Laurent

Reputation: 84519

I've managed to make it work by multiple trials-errors. As I understand (I'm still new in Shiny modules), you have to use session$ns only for the inputs created in the server.

library(shiny)

# module UI function
modUI <- function(id){

  ns <- NS(id)

  tagList(
    actionButton(ns('add'), 'Add'),
    div(id = ns('placeholder'))
  )
}

# module server function
modServer <- function(input, output, session) {

  ns = session$ns

  ctn <- reactiveVal(0)

  Id <- reactive({
    function(id){
      paste0(id, ctn())
    }
  })
  IdNS <- reactive({
    function(id){
      ns(paste0(id, ctn()))
    }
  })

  observeEvent(input$add, {

    ctn(ctn() + 1)

    insertUI(
      selector = paste0('#', ns('placeholder')),
      ui = div(
        id = Id()('div'),
        selectInput(IdNS()('letter'), 'Letter:', LETTERS[1:2]),
        uiOutput(IdNS()('input'))
      )
    )
  })

  observeEvent(ctn(), {

    id <- Id()('input')
    selection <- Id()('letter')

    output[[id]] <- renderUI({

      switch(
        input[[selection]],
        'A' = textInput(IdNS()('text'), 'ENTER TEXT', ''),
        'B' = numericInput(IdNS()('numeric'), 'ENTER NUMBER', '') 
      )

    })

  }, ignoreInit = TRUE)
}

# main ui
ui <- fluidPage(
  modUI('mod1')
)

# main server
server <- function(input, output, session) {
  callModule(modServer, "mod1")
}

# run app
shinyApp(ui, server)

Upvotes: 5

Related Questions