Remove/add UI components based on number of existing UI components

Hi all,
I'm working on a shiny app with a dynamic UI. The user can add and remove input selectors using actionButtons, and then I need various aspects of the app behavior to depend on the number of selectors currently displayed.

I've run into a problem with removeUI that has been discussed before, most notably here. While removeUI works to remove the UI object from view, it does not actually nullify the inputs contained in that UI object, so from the server's perspective, they still exist. In my app, this means that when I try to condition behavior on whether certain selectors do or don't exist, is.null() tells me the input exists even if it has been removed by removeUI.

Here is an example that shows what I mean:

library(shiny)
ui <- fluidPage(
  actionButton("add", "Add a selector"),
  selectInput("selector1", "Selector 1", choices = c("a", "b", "c"), selected = "a"),
  uiOutput("selector2"),
  uiOutput("selector3"),
  actionButton("nSelectorsShow", "How many selectors?"),
  actionButton("resetButton", "Reset")
)

server <- function(input, output, session) {
  nSelectors <- reactive({ # counter that tells us how many selectors there are
    sum(!is.null(input$selector1), !is.null(input$selector2), !is.null(input$selector3))
  })
  
  # On button click, print to console how many selectors we have
  observeEvent(input$nSelectorsShow, {
    print(nSelectors())
  })
  
  # Add selectors on button click
  observeEvent(input$add, {
    if(nSelectors() == 1){
      output$selector2 <- renderUI(
        selectInput("selector2", "Selector 2", choices = c("a", "b", "c"), selected = "a")
      )
    }else if(nSelectors() == 2){
      output$selector3 <- renderUI(
        selectInput("selector3", "Selector 3", choices = c("a", "b", "c"), selected = "a")
      )
    }
  })
  
  # On reset button click, revert back to one selector by removing the second and third selectors
  observeEvent(input$resetButton, {
    ### remove selector 2, if it exists
    if(!is.null(input$selector2)){
      removeUI(
        selector = "#selector2"
      )
    }
    ### remove selector 3, if it exists
    if(!is.null(input$selector3)){
      removeUI(
        selector = "#selector3"
      )
    }
  })
  
}

shinyApp(ui, server)

Notice that I've created a reactive expression called nSelectors() equal to the number of non-null inputs. Which selectors are added/removed should depend on how many selectors are currently shown in the UI.

But if you click around a bit in that app (using the "How many selectors?" button to print the value of nSelectors() to the console at any time), you'll see that the removeUI command hasn't worked to change the inputs to null. So when I reset the selectors (so only one is showing), nSelectors() still thinks that there are three selectors. As a result, if you reset and then click "Add a selector" again, it will add Selector 3 after Selector 1, instead of adding Selector 2.

It seems like the team is considering a change to this behavior, but it hasn't been implemented yet.
In the Github issue I linked to above, @daattali suggested using Shiny.onInputChange("input", 'null') (or, as of more recent versions, Shiny.setInputValue("input", 'null') to set the inputs to null when they are removed. I tried doing that, and I don't know if it doesn't work or if I just didn't insert the Javascript code correctly (I don't know any js...)

(Example with js added):

library(shiny)
ui <- fluidPage(
  actionButton("add", "Add a selector"),
  selectInput("selector1", "Selector 1", choices = c("a", "b", "c"), selected = "a"),
  uiOutput("selector2"),
  uiOutput("selector3"),
  actionButton("nSelectorsShow", "How many selectors?"),
  actionButton("resetButton", "Reset")
)

server <- function(input, output, session) {
  nSelectors <- reactive({ # counter that tells us how many selectors there are
    sum(!is.null(input$selector1), !is.null(input$selector2), !is.null(input$selector3))
  })
  
  # On button click, print to console how many selectors we have
  observeEvent(input$nSelectorsShow, {
    print(nSelectors())
  })
  
  # Add selectors on button click
  observeEvent(input$add, {
    if(nSelectors() == 1){
      output$selector2 <- renderUI(
        selectInput("selector2", "Selector 2", choices = c("a", "b", "c"), selected = "a")
      )
    }else if(nSelectors() == 2){
      output$selector3 <- renderUI(
        selectInput("selector3", "Selector 3", choices = c("a", "b", "c"), selected = "a")
      )
    }
  })
  
  # On reset button click, revert back to one selector by removing the second and third selectors
  observeEvent(input$resetButton, {
    ### remove selector 2, if it exists
    if(!is.null(input$selector2)){
      removeUI(
        selector = "#selector2"
      )
      tags$script(HTML(
        "Shiny.setInputValue('selector2', 'null')"
      ))
    }
    
    ### remove selector 3, if it exists
    if(!is.null(input$selector3)){
      removeUI(
        selector = "#selector3"
      )
      tags$script(HTML(
        "Shiny.setInputValue('selector3', 'null')"
      ))
    }
  })
  
}

shinyApp(ui, server)

So, I guess there are two ways to solve my issue, and either would be fine.

  1. Can anyone help me find a way to nullify inputs once they're removed with removeUI?
    OR
  2. Maybe I'm going about this the wrong way and shouldn't be basing my code at all on whether certain inputs are or are not null. Is there a better way to condition app behavior based on whether or not a certain UI item currently exists in the app (or, based on how many such items are present)? Like, could I maybe surround my various UI components in html divs and use some function to condition on whether those divs exist?

Any ideas would be great! Thank you so much.

Here is a solution without javascript shenanigans, I just create my own object to keep track of what selectors should be considered active and which considered removed.

library(shiny)
ui <- fluidPage(
  actionButton("add", "Add a selector"),
  selectInput("selector1", "Selector 1", choices = c("a", "b", "c"), selected = "a"),
  uiOutput("selector2"),
  uiOutput("selector3"),
  actionButton("nSelectorsShow", "How many selectors?"),
  actionButton("resetButton", "Reset")
)

server <- function(input, output, session) {
  active_selectors <- reactiveValues(
    selector1 = TRUE,
    selector2 = FALSE,
    selector3 = FALSE
  )

  nSelectors <- reactive({ # counter that tells us how many selectors there are
    act_sel <- as.logical(reactiveValuesToList(active_selectors))
    sum(act_sel)
  })

  # On button click, print to console how many selectors we have
  observeEvent(input$nSelectorsShow, {
    print(nSelectors())
  })

  # Add selectors on button click
  observeEvent(input$add, {
    if (nSelectors() == 1) {
      active_selectors$selector2 <- TRUE
      output$selector2 <- renderUI(
        selectInput("selector2", "Selector 2", choices = c("a", "b", "c"), selected = "a")
      )
    } else if (nSelectors() == 2) {
      active_selectors$selector3 <- TRUE
      output$selector3 <- renderUI(
        selectInput("selector3", "Selector 3", choices = c("a", "b", "c"), selected = "a")
      )
    }
  })

  # On reset button click, revert back to one selector by removing the second and third selectors
  observeEvent(input$resetButton, {
    ### remove selector 2, if it exists
    if (!is.null(input$selector2)) {
      output$selector2 <- NULL
      active_selectors$selector2 <- FALSE
    }
    ### remove selector 3, if it exists
    if (!is.null(input$selector3)) {
      output$selector3 <- NULL
      active_selectors$selector3 <- FALSE
    }
  })
}

shinyApp(ui, server)

@nirgrahamuk that's a good idea! It hadn't occurred to me to keep track of each selector separately. I haven't tried your solution yet but I think it will work.

I would be curious though if there's a better way to access the information about whether or not a particular UI piece is displayed. Maybe I need conditionalPanels (though I'm not sure they would work for my particular use case). Seems like we need an is.active() function or something.

After reading this it feels like it shouldn't be necessary but, here is another solution avoiding removeUI, which requires less typing:

library(shiny)

ui <- fluidPage(
  column(12,
         actionButton("add", "Add a selector"),
         actionButton("resetButton", "Reset")
  ),
  column(12, uiOutput("selectors"))
)

server <- function(input, output, session) {
  nSelectors <- reactiveVal(1L)
  
  output$selectors <- renderUI({
    Map(
      selectInput,
      inputId = paste0("selector", seq_len(nSelectors())),
      label = paste("Selector", seq_len(nSelectors())),
      choices = list(c("a", "b", "c")),
      selected = "a"
    )
  })
  
  observeEvent(input$add, {
    nSelectors(nSelectors() + 1L)
    print(paste("nSelectors:", nSelectors()))
    # print(names(input))
  })
  
  observeEvent(input$resetButton, {
    nSelectors(1L)
    print(paste("nSelectors:", nSelectors()))
    # print(names(input))
  })
}

shinyApp(ui, server)
1 Like

I like that this can be extended to more selectors without adding a lot more typing. Thank you! Not 100% sure it will work for my particular case (the real-world app, not the minimal example I provided), but I'll give it a try.

Sure, in the end it's the same logic as @nirgrahamuk's answer, just generalized. Depending on the complexity of your real world app it might pay off to reduce redundant code. If you're stuck incorporating this approach, feel free to let me know. Maybe I can help. Cheers

Thanks so much! I may take you up on that. Appreciate the time you took to answer :slight_smile:

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

If you have a query related to it or one of the replies, start a new topic and refer back with a link.