Dynamically add/remove the input in shiny

I am using flexdashboard in shiny to create my application. I have tried my best to add the input fields in shiny by allowing the user to click on a button. However, the problem I am having right now is everytime I add input fields in my shiny app the values in those input fields get reset. I haven't been able to implement a button and remove the input fields that I added in the app. So any input to efficiently add / remove input fields in shiny is highly appreciated. I want to add /remove three fields at once as shown in my application. May be there is a elegant may to perform what I am doing in my app.

The reproducible example is as follows:

---
title: "Shiny Flexboard Testing"
output:
  flexdashboard::flex_dashboard:
  orientation: rows
social: menu
source_code: embed
runtime: shiny
---

```{r global, include=FALSE}
# load data in 'global' chunk so it can be shared by all users of the dashboard
library(ggplot2)
library(mgcv)
library(magrittr)
library(plotly)
library(directlabels)
library(shinydashboard)
Column {.sidebar data-width=350}
-----------------------------------------------------------------------

### Plan View Controls
# Inline pricelow and pricehigh
div(style="display: inline-block;vertical-align:top; width: 70px;height:10px",
    shiny::textInput('pricelow', 'Min price', value = 0))
div(style="display: inline-block;vertical-align:top; width: 70px;",
    shiny::textInput('pricehigh', 'Max price', value = 30))

### Time Series Controls
fluidRow(column(4, textInput('num_ts','No of TS', value = 2)),
         column(4, shiny::textInput('ts_pricelow', 'Min price', value = 0)),
         column(4, shiny::textInput('ts_pricehigh', 'Max price', value = 30)))
fluidRow(column(3, actionButton("addInput","Add Input", style ='font-size:80%; padding:4px')),
         #column(2, actionButton("resetInput","Reset Input", style ='font-size:80%; padding:4px')),
         column(3, checkboxInput(inputId = 'plotpoints', "MapView", value = FALSE)),
         column(3, checkboxInput(inputId = 'ts_facet', "Facet", value = FALSE)),
         column(3, checkboxInput(inputId = 'facet_free', "Y Free", value = FALSE)))
         


uiOutput("inputs")
Column {.tabset}
------------------------------------------------------------------------
### Process addInput
```{r}
ids <- NULL
observeEvent(input$addInput,{
  print(ids)
  if (is.null(ids)){
    ids <<- 1
  }else{
    ids <<- c(ids, max(ids)+1)
  }
  output$inputs <- renderUI({
    tagList(
      lapply(1:length(ids),function(i){
      # Create a div that contains 3 sub divs
        div(
        # Display option to provide I of Cell
        div(style="display: inline-block;vertical-align:top; width: 80px",
            shiny::textInput(paste0('cell_i_',ids[i]), 'I index', value = 10)),
        # Option to provide J of cell
        div(style="display: inline-block;vertical-align:top; width: 80px",
            shiny::textInput(paste0("cell_j_",ids[i]), 'J index', value = 34)),
        # Option to provide K of cell
        div(style="display: inline-block;vertical-align:top; width: 80px",
            shiny::selectInput(paste0("cell_k_",ids[i]), 'Layer', c(1:10)))
        )
      })
    )
  })
})

So there are a few things that you are looking to accomplish, if I am understanding this correctly.

  1. you want to be able to add a new set of inputs every time you click the "Add Input" button while at the same time leaving the previously generated inputs the same. This can be accomplished by adding an if statement into your lapply function. Now the function will first to look if the input already exists. If it does, then it will use the current value as what is selected. The new lapply call looks like this:

       lapply(1:length(ids()),function(i){
         check_input_i <- paste0("cell_i_", ids()[i])
         check_input_j <- paste0("cell_j_", ids()[i])
         check_input_k <- paste0("cell_k_", ids()[i])
         if(is.null(input[[check_input_i]])){
           # Create a div that contains 3 new sub divs
           div(
             # Display option to provide I of Cell
             div(style="display: inline-block;vertical-align:top; width: 80px",
                 shiny::textInput(paste0('cell_i_',ids()[i]), 'I index', value = 10)),
             # Option to provide J of cell
             div(style="display: inline-block;vertical-align:top; width: 80px",
                 shiny::textInput(paste0("cell_j_",ids()[i]), 'J index', value = 34)),
             # Option to provide K of cell
             div(style="display: inline-block;vertical-align:top; width: 80px",
                 shiny::selectInput(paste0("cell_k_",ids()[i]), 'Layer', c(1:10)))
           )
         } else {
           # Create a div that contains 3 existing sub divs
           div(
             # Display option to provide I of Cell
             div(style="display: inline-block;vertical-align:top; width: 80px",
                 shiny::textInput(paste0('cell_i_',ids()[i]), 'I index', value = input[[check_input_i]])),
             # Option to provide J of cell
             div(style="display: inline-block;vertical-align:top; width: 80px",
                 shiny::textInput(paste0("cell_j_",ids()[i]), 'J index', value = input[[check_input_j]])),
             # Option to provide K of cell
             div(style="display: inline-block;vertical-align:top; width: 80px",
                 shiny::selectInput(paste0("cell_k_",ids()[i]), 'Layer', c(1:10), selected = input[[check_input_k]]))
           )
         }
       })
    
  2. Implement a button to remove the three generate fields. I have accomplished this by adding a new action button with an id = removeInput. This also leads me to another point. The way you have the renderUI nested inside of an observeEvent() has been discouraged by @jcheng as it can have unintended consequences. To avoid this, I have removed the renderUI function from your observeEvent() function call. I also rearranged how the ids vector is generated and made it a reactive expression. Now every time you click the "Add Input" button a new input element will be created, while the already existing values will keep their current values. Additionally, every time you click the "Remove Input" button, the last inputs will be removed. Here is the code for the entire app:

    ui <- shinyUI(
      fluidPage(
        div(style="display: inline-block;vertical-align:top; width: 70px;height:10px",
            shiny::textInput('pricelow', 'Min price', value = 0)),
        div(style="display: inline-block;vertical-align:top; width: 70px;",
            shiny::textInput('pricehigh', 'Max price', value = 30)),
    
        ### Time Series Controls
        fluidRow(column(4, textInput('num_ts','No of TS', value = 2)),
                 column(4, shiny::textInput('ts_pricelow', 'Min price', value = 0)),
                 column(4, shiny::textInput('ts_pricehigh', 'Max price', value = 30))),
        fluidRow(column(3, 
                        column(3, actionButton("addInput","Add Input", style ='font-size:80%; padding:4px')),
                        column(3, actionButton("removeInput","Remove Input", style ='font-size:80%; padding:4px'))),
         
                 #column(2, actionButton("resetInput","Reset Input", style ='font-size:80%; padding:4px')),
                 column(3, checkboxInput(inputId = 'plotpoints', "MapView", value = FALSE)),
                 column(3, checkboxInput(inputId = 'ts_facet', "Facet", value = FALSE)),
                 column(3, checkboxInput(inputId = 'facet_free', "Y Free", value = FALSE))),
    
    
    
        uiOutput("inputs")
      )
    )
    
    server <- function(input, output, session){
      ids <- reactive({
        if (input$addInput == 0) return(NULL)
    
        if (input$addInput == 1){
          output <- 1
        }else{
          if(input$addInput > input$removeInput) {
            output <- 1:(input$addInput-input$removeInput)
          } else return(NULL)
          
        }
        return(output)
      })
    
      output$inputs <- renderUI({
        if (is.null(ids())) return(NULL)
        tagList(
         lapply(1:length(ids()),function(i){
            check_input_i <- paste0("cell_i_", ids()[i])
            check_input_j <- paste0("cell_j_", ids()[i])
            check_input_k <- paste0("cell_k_", ids()[i])
            if(is.null(input[[check_input_i]])){
              # Create a div that contains 3 new sub divs
              div(
                # Display option to provide I of Cell
                div(style="display: inline-block;vertical-align:top; width: 80px",
                    shiny::textInput(paste0('cell_i_',ids()[i]), 'I index', value = 10)),
                # Option to provide J of cell
                div(style="display: inline-block;vertical-align:top; width: 80px",
                    shiny::textInput(paste0("cell_j_",ids()[i]), 'J index', value = 34)),
                # Option to provide K of cell
                div(style="display: inline-block;vertical-align:top; width: 80px",
                    shiny::selectInput(paste0("cell_k_",ids()[i]), 'Layer', c(1:10)))
              )
            } else {
              # Create a div that contains 3 existing sub divs
              div(
                # Display option to provide I of Cell
                div(style="display: inline-block;vertical-align:top; width: 80px",
                    shiny::textInput(paste0('cell_i_',ids()[i]), 'I index', value = input[[check_input_i]])),
                # Option to provide J of cell
                div(style="display: inline-block;vertical-align:top; width: 80px",
                    shiny::textInput(paste0("cell_j_",ids()[i]), 'J index', value = input[[check_input_j]])),
                # Option to provide K of cell
                div(style="display: inline-block;vertical-align:top; width: 80px",
                    shiny::selectInput(paste0("cell_k_",ids()[i]), 'Layer', c(1:10), selected = input[[check_input_k]]))
              )
            }
    
          })
        )
      })
    }
    
    shinyApp(ui = ui, server = server)
    

The one finicky thing about this implementation is that if you click the "Remove Input" button while there are no inputs, you will have to likewise click the "Add input" button the same number of times before new inputs actually start showing up. Hopefully this helps you. Let me know if you need any clarification

1 Like

@tbradley Thank you so much for your detailed reply explaining every details on what you have implemented. Everything works as intended. Just another additional question:
i. How can I update the text field no of Ts based on the click in add point. Each time user click add Input there will be one new input. So if the user clicks x times and removes y times, the number of points would be x-y. How can I update this info in no of ts fields ? In other words, can i add number of points based on the input specified in no of ts text field ?

I haven't tried your code, but from the description of the problem, I just wanted to make sure you're aware of the insertUI and removeUI functions. Here's the relevant part of our dynamic UI article. They seem more appropriate for this use case than renderUI.

2 Likes

If you decide to stick with the renderUI method then you could add this observe statement into your server code and it will update your num_ts ui element.

observe({
    updateTextInput(
      session = session,
      inputId = "num_ts",
      value = length(ids())
    )
  })

As a note, I am not sure how to call the session argument inside the flexdashboard layout, so you may have to play with that.

Also, I have never used the insertUI and removeUI functions that @barbara mentioned, but after reading the link she posted, they do seem like they may be what you are looking for in this situation. They may also handle the issue of input$addInput - input$removeInput being a negative number. So it is worth looking into that IMO.

1 Like

Thank you @tbradley and @barbara for your reply. Definitely the link provided by @barbara seems to be something I am looking for. I will look into that documentation more to understand the insertUI and removeUI functions.
Bradley, I am still new to flexdashboard layout but like the simplicity. I will try to figure out how to include the session information on flexdashboard. Thanks for showing how to update num_ts in ui. I learned a lot from these conversations.

1 Like