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.
- Can anyone help me find a way to nullify inputs once they're removed with removeUI?
OR - 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.