renderUi which call himself

Hello all,
I am getting stuck for couple days now trying to create a dynamic structure in shiny. What I would like to do is a checkboxGroupInput (with by default all the choices selected) which when one or several choices are deselected, call another checkboxGroupInput, offering the deselected choices from the checkboxGroupInput t-1 to the new one. If you imagine n choices, then there is the possibility that n checkboxGroupInput must be created.
I coded it for 2 iterations using several uiOutput (even if the solution is not suitable) to show you better what I want to do.

library(shiny)

choices0 <- LETTERS[1:10]
ui <- fluidPage(
  tagList(
    checkboxGroupInput("cbGI_0", label = 0, inline = TRUE,
                       choices = choices0, 
                       selected = choices0),
    uiOutput("cbGI_ui_1")
  )
  
)

server <- function(input, output) {
  
  output$cbGI_ui_1 <- renderUI({
    choices <- choices0[!choices0 %in% input$cbGI_0]
    if (length(choices) > 0){
      tagList(
        checkboxGroupInput("cbGI_1", label = 1, inline = TRUE, 
                           choices = choices, 
                           selected = choices),
        uiOutput("cbGI_ui_2")
      )
    }
  })
  
  output$cbGI_ui_2 <- renderUI({
    choices <- choices0[!choices0 %in% c(input$cbGI_0, input$cbGI_1)]
    if (length(choices) > 0){
      tagList(
        checkboxGroupInput("cbGI_2", label = 2, inline = TRUE, 
                           choices = choices, 
                           selected = choices),
        uiOutput("cbGI_ui_3")
      )
    }
  })
  
  #and so on
  
}

shinyApp(ui = ui, server = server)

The structure of my problem make me think to a kind of recursive function, but the problem is that the reactive dependency between 2 ui coming from the same renderUI seems tricky in shiny. See what I tried for now (without succes)...

library(shiny)

choices0 <- LETTERS[1:10]
ui <- fluidPage(
  tagList(
    checkboxGroupInput("cbGI_0", label = 0, inline = TRUE,
                       choices = choices0, 
                       selected = choices0),
    uiOutput("cbGI_ui_1")
  )
)

server <- function(input, output) {
  
  n <- reactive({
    length(sapply(names(input), grepl, pattern = "cbGI_[0-9]*$"))
  })
  
  observe({
    for (i in seq_len(n())){
      output[[paste0("cbGI_ui_", i)]] <- renderUI({
        aready_selected <- NULL
        for (k in seq_len(i)){
          aready_selected <- c(aready_selected, input[[paste0("cbGI_", k-1)]])
        }

        choices <- choices0[!choices0 %in% aready_selected]
        if (length(choices) > 0){
          tagList(
            checkboxGroupInput(paste0("cbGI_", i), label = i, inline = TRUE, 
                               choices = choices, 
                               selected = choices),
            uiOutput(paste0("cbGI_ui_", i+1))
          )
        }
      })
    }
  }) 
}

# Run the application 
shinyApp(ui = ui, server = server)

Can you guys tell me if what I am trying to do is impossible in shiny or how to do it please ?

Thanks by advance !!

Hi @aconanec. I can't get your question. I guess you want to add checkboxGroupInput by the deselected choice. If yes, you can use insertUI to do so. First, place the first input in a named div container. Trigger the insertUI by the input and place the new input beforeEnd of the container.

library(shiny)

choices0 <- LETTERS[1:10]
ui <- fluidPage(
  tagList(
    tags$div(
      id = "cbGI_container",
      checkboxGroupInput("cbGI_0", label = 0, inline = TRUE,
                         choices = choices0, 
                         selected = choices0)
    )
  )
)

server <- function(input, output) {
  
  vals <- reactiveValues(prev = choices0, num = 1)
  
  observeEvent(
    input$cbGI_0,
    {
      desel <- setdiff(vals$prev, input$cbGI_0)
      insertUI("#cbGI_container", "beforeEnd", {
        checkboxGroupInput(paste0("cbGI_", vals$num), label = vals$num, choices = choices0, selected = desel, inline = TRUE)
      })
      vals$prev <- input$cbGI_0
      vals$num <- vals$num + 1
    },ignoreInit = TRUE, ignoreNULL = FALSE
  )

}

shinyApp(ui = ui, server = server)

Hi @raytong, thank you for your answer. Actually this not quite what I wanted. In your code, the insertUI react only when input$cbGI_0 change. What I want, it's to link each checkboxGroupInput to the one above him.
As I showed it in my first code, you start with a checkboxGroupInput (0) where all the choices are selected. If you deselect one or more, one and only one checkboxGroupInput (1) should appear having for choices only the one deselected above. Then if you deselect one or several choice of the checkboxGroupInput (1) then a third checkboxGroupInput (2) appears and so on...
Is it clearer ?

@aconanec. You have to write a factory function to generate all ui and observer at once when called. Please check the following code.

library(shiny)
library(shinyjs)

choices0 <- LETTERS[1:10]
ui <- fluidPage(
  useShinyjs(),
  tagList(
    tags$div(
      id = "cbGI_container",
      checkboxGroupInput("cbGI_0", label = 0, inline = TRUE,
                         choices = choices0, 
                         selected = choices0)
    )
  )
)

server <- function(input, output, session) {
  vals <- reactiveValues(choices0 = choices0)
  
  uiFactory <- function(num) {
    observeEvent(
      input[[paste0("cbGI_", num)]],
      {
        req(length(input[[paste0("cbGI_", num)]]) < length(vals[[paste0("choices", num)]]))
        if(!paste0("cbGI_", num + 1) %in% names(input)) {
          vals[[paste0("choices", num + 1)]] <- vals[[paste0("choices", num)]][!vals[[paste0("choices", num)]] %in% input[[paste0("cbGI_", num)]]]
          insertUI("#cbGI_container", "beforeEnd", {
            checkboxGroupInput(paste0("cbGI_", num + 1), label = num + 1, inline = TRUE,
                               choices = vals[[paste0("choices", num + 1)]],
                               selected = vals[[paste0("choices", num + 1)]]
            )
          })
        }
    })
    
    observe({
      vals[[paste0("choices", num + 1)]] <- vals[[paste0("choices", num)]][!vals[[paste0("choices", num)]] %in% input[[paste0("cbGI_", num)]]]
      updateCheckboxGroupInput(session, paste0("cbGI_", num + 1), 
                               choices = vals[[paste0("choices", num + 1)]], 
                               selected = vals[[paste0("choices", num + 1)]], inline = TRUE)
    })
    
    observe({
      if(length(vals[[paste0("choices", num + 1)]]) == 0) {
        hide(paste0("cbGI_", num + 1))
      } else {
        show(paste0("cbGI_", num + 1))
      }
    })
    
    observeEvent(
      input[[paste0("cbGI_", num + 1)]],
      {
        uiFactory(num + 1)
      }, ignoreInit = TRUE, once = TRUE
    )
  }
  
  
  observeEvent(
    input$cbGI_0,
    {
      uiFactory(0)
    }, ignoreInit = TRUE, once = TRUE
  )
}

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

Thank you very much !!! I am not sure I understood every piece of your code but it definitely solves the problem ! Thanks again :slight_smile:

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