How to clear dynamic UI elements?

Viewed 83

I have a shiny app that is made up of several select widgets. The number of the widgets depends on some data. In this example I use two. However, the user has the option to add or remove select widgets via action buttons.

At the moment it is not possible to clear those select widgets. For example, open the app and remove the second select widget. Afterwards, add another select widget. What happens is that the new select widget still has the old value. But what I want is that the new select widget is empty. I tried to achieve this with updateSelectInput() but it does not work.

I appreciate any help. If you have any suggestions on how to improve the app or make it simpler without losing the functionality I'd love to hear as well about that.

library(shiny)

data <- data.frame(A = c("a", "b"))

ui <- fluidPage(
  fluidRow(
    column(
      width = 6,
      uiOutput("ui_output_A")
    ),
  ),
  fluidRow(
    column(
      width = 6,
      uiOutput("ui_button_add"),
    ),
    column(
      width = 6,
      uiOutput("ui_button_remove")
    )
  )
)

server <- function(input, output, session) {
  widget_counter <- reactiveValues(value = 1)
  
  if (nrow(data) > 1) {
    widget_counter$value <- nrow(data)
  } else {
    widget_counter$value <- 1
  }
  
  output$ui_button_add <- renderUI({
    actionButton("button_add",
                 label = "Add")
  })
  
  output$ui_button_remove <- renderUI({
    actionButton("button_remove",
                 label = "Remove")
  })
  
  observeEvent(input$button_add, {
    widget_counter$value <- widget_counter$value + 1
  })
  
  observeEvent(input$button_remove, {
    if (widget_counter$value > 0) {
      updateSelectInput(
        session = session,
        inputId = paste0("selectt_A_",
                         widget_counter$value),
        selected = NA
      )
      widget_counter$value <- widget_counter$value - 1
    }
  })
  
  all_inputs <- reactive({
    x <- reactiveValuesToList(input)
  })
  
  reactive_A <- reactive({
    n <- widget_counter$value
    if (n > 0) {
      isolate({
        lapply(seq_len(n), function(i) {
          selectInput(inputId = paste0("select_A_", i),
                      label = paste0(i, ".) A:"),
                      choices = as.list(c("", "a", "b", "c", "d")),
                      selected =
                        if (isTRUE(i <= nrow(data))) {
                          data[i, "A"]
                        } else {
                          all_inputs()[[paste0("select_A_", i)]]
                        }
          )
        })
      })
    }
  })
  
  output$ui_output_A <- renderUI({reactive_A()})
}

shinyApp(ui, server)

EDIT

I simplified the example in the original post too much so here is an edit. The number of inital widgets depends on the row of a table a user selects. I tried to extend lz100's answer by moving some code from ui to server. However, I failed. See the following code.

library(shiny)
library(DT)
library(spsUtil)

data1 <- data.frame(id = c(1, 1, 2), 
                    value = c("a", "b", "a"))
data2 <- data.frame(id = 1:2)

ui <- fluidPage(
  fluidRow(
    DTOutput("table_output")
  ), 
  fluidRow(
    column(
      width = 6,
      uiOutput("ui_output")
    )
  ),
  fluidRow(
    column(
      width = 6,
      actionButton("button_add",
                   label = "Add"),
    ),
    column(
      width = 6,
      actionButton("button_remove",
                   label = "Remove")
    )
  )
)

server <- function(input, output, session) {
  counter <- reactiveValues(value = 1)
  
  output$table_output <- renderDT(datatable(data2))
  
  observeEvent(input$table_output_rows_selected, { 
    if (length(which(data1[, "id"] == data2[input$table_output_rows_selected, "id"])) > 1) {
      counter$value <- length(which(data1[, "id"] == data2[input$table_output_rows_selected, "id"]))
    } else {
      counter$value <- 1
    }
  })
  
  output$ui_output <- renderUI({
    div(
      id = "input_els",
      # define some default inputs, here 2 default inputs
      # when user wants to add more, we do it from server
      lapply(seq_len(counter$value), function(i) {
        div(
          id = paste0("select_container", i),
          selectInput(paste0("select_", i),
                      label = paste0(i, ".):"),
                      choices = as.list(c("", letters)),
                      selected =
                        if (isTRUE(i <= length(which(data1[, "id"] == data2[input$table_output_rows_selected, "id"])))) {
                          data1[i, "value"]
                        } else {
                          NA
                        }
          )
        )
        
      })
    )
  })
  
  observeEvent(input$button_add, {
    insertUI(
      "#input_els", 
      ui = div(
        id = paste0("select_container", counter$value + 1),
        selectInput(
          paste0("select_", counter$value + 1),
          paste0(counter$value + 1, ".) "),
          c("", "a", "b", "c", "d")
        )   
      )      
    )
    inc(counter$value)
  })
  observeEvent(input$button_remove, {
    req(counter$value > 0)
    removeUI(paste0("#select_container", counter$value))
    inc(counter$value, -1)
  })
}

shinyApp(ui, server)
1 Answers
library(shiny)
library(spsUtil)
data <- data.frame(A = c("a", "b"))
n <- 2
ui <- fluidPage(
    fluidRow(
        column(
            width = 6,
            div(
                id = "input_els",
                # define some default inputs, here 2 default inputs
                # when user wants to add more, we do it from server
                lapply(seq_len(n), function(i) {
                    div(
                        id = paste0("select_container", i),
                        selectInput(paste0("select_", i),
                                    label = paste0(i, ".):"),
                                    choices = as.list(c("", "a", "b", "c", "d")),
                                    selected =
                                        if (isTRUE(i <= nrow(data))) {
                                            data[i, "A"]
                                        } else {
                                            all_inputs()[[paste0("select_A_", i)]]
                                        }
                        )
                    )

                })
            )
        )
    ),
    fluidRow(
        column(
            width = 6,
            actionButton("button_add",
                         label = "Add"),
        ),
        column(
            width = 6,
                actionButton("button_remove",
                             label = "Remove")
        )
    )
)

server <- function(input, output, session) {
    counter <- reactiveValues(value = n)
    observeEvent(input$button_add, {
        insertUI(
            "#input_els", 
            ui = div(
                id = paste0("select_container", counter$value + 1),
                selectInput(
                    paste0("select_", counter$value + 1),
                    paste0(counter$value + 1, ".) "),
                    c("", "a", "b", "c", "d")
                )   
            )      
        )
        inc(counter$value)
    })
    observeEvent(input$button_remove, {
        req(counter$value > 0)
        removeUI(paste0("#select_container", counter$value))
        inc(counter$value, -1)
    })
}

shinyApp(ui, server)

I have simplified your code and made it more shiny-ish. See how clean the server code is.

  1. We make some default inputs directly from UI. Inputs you know they will be there on start.
  2. Use insertUI and removeUI to add/remove additional inputs dynamically.
  3. To have removeUI on input forms like selectInput, the easiest way is to wrap it inside another div.

Edits

library(shiny)
library(DT)
library(spsUtil)

data1 <- data.frame(id = c(1, 1, 2), 
                    value = c("a", "b", "a"))
data2 <- data.frame(id = 1:2)

ui <- fluidPage(
    fluidRow(
        DTOutput("table_output")
    ), 
    fluidRow(
        column(
            width = 6,
            uiOutput("ui_output")
        )
    ),
    fluidRow(
        column(
            width = 6,
            actionButton("button_add",
                         label = "Add"),
        ),
        column(
            width = 6,
            actionButton("button_remove",
                         label = "Remove")
        )
    )
)

server <- function(input, output, session) {
    counter <- reactiveValues(value = 1)
    
    output$table_output <- renderDT(datatable(data2))
    
    observeEvent(input$table_output_rows_selected, { 
        if (length(which(data1[, "id"] %in% data2[input$table_output_rows_selected, "id"])) > 1) {
            counter$value <- length(which(data1[, "id"] %in% data2[input$table_output_rows_selected, "id"]))
        } else {
            counter$value <- 1
        }
    })
    
    output$ui_output <- renderUI({
        div(
            id = "input_els",
            # define some default inputs, here 2 default inputs
            # when user wants to add more, we do it from server
            lapply(seq_len(isolate(counter$value)), function(i) {
                div(
                    id = paste0("select_container", i),
                    selectInput(paste0("select_", i),
                                label = paste0(i, ".):"),
                                choices = as.list(c("", letters)),
                                selected =
                                    if (isTRUE(i <= length(which(data1[, "id"] %in% data2[input$table_output_rows_selected, "id"])))) {
                                        data1[i, "value"]
                                    } else {
                                        NA
                                    }
                    )
                )
                
            })
        )
    })
    
    observeEvent(input$button_add, {
        insertUI(
            "#input_els", 
            ui = div(
                id = paste0("select_container", counter$value + 1),
                selectInput(
                    paste0("select_", counter$value + 1),
                    paste0(counter$value + 1, ".) "),
                    c("", "a", "b", "c", "d")
                )   
            )      
        )
        inc(counter$value)
    })
    observeEvent(input$button_remove, {
        req(counter$value > 0)
        removeUI(paste0("#select_container", counter$value))
        inc(counter$value, -1)
    })
}

shinyApp(ui, server)
Related