7

I've been trying to find a solution how to add and remove input fields with a button in shiny. I don't have a source code since I haven't made that much progress, but this jQuery example (http://www.mkyong.com/jquery/how-to-add-remove-textbox-dynamically-with-jquery/) gives a good idea on what I'm trying to accomplish. Is this possible in shiny or should I use shinyjs to do this? Thank you in advance!

Phil
  • 7,287
  • 3
  • 36
  • 66
samssan
  • 393
  • 3
  • 9

3 Answers3

9

EDIT: I read the jQuery example a bit more, and added a code snippet doing what I think you were looking for.

I don't know jQuery, so I couldn't make much out of the example link. I took a guess on what you wanted, but I think the key idea is the use of renderUI and uiOutput even if my suggestion here misses the point.

To toggle a ui element:

If you specifically don't want to use shinyjs, you could do something like this:

library(shiny)

ui <- shinyUI(fluidPage(

  actionButton("btn", "Toggle Textbox"),

  textOutput("btn_val"),
  uiOutput("textbox_ui")

))

server <- shinyServer(function(input, output, session) {

  output$btn_val <- renderPrint(print(input$btn))

  textboxToggle <- reactive({

    if (input$btn %% 2 == 1) {
      textInput("textin", "Write something:", value = "Hello World!")
    }

  })

  output$textbox_ui <- renderUI({ textboxToggle() })

})

shinyApp(ui, server)

To add and remove elements:

After reading a bit of the jQuery example, I think this is similar to what you were looking for:

library(shiny)

ui <- shinyUI(fluidPage(

  sidebarPanel(

      actionButton("add_btn", "Add Textbox"),
      actionButton("rm_btn", "Remove Textbox"),
      textOutput("counter")

    ),

  mainPanel(uiOutput("textbox_ui"))

))

server <- shinyServer(function(input, output, session) {

  # Track the number of input boxes to render
  counter <- reactiveValues(n = 0)

  observeEvent(input$add_btn, {counter$n <- counter$n + 1})
  observeEvent(input$rm_btn, {
    if (counter$n > 0) counter$n <- counter$n - 1
  })

  output$counter <- renderPrint(print(counter$n))

  textboxes <- reactive({

    n <- counter$n

    if (n > 0) {
      lapply(seq_len(n), function(i) {
        textInput(inputId = paste0("textin", i),
                  label = paste0("Textbox", i), value = "Hello World!")
      })
    }

  })

  output$textbox_ui <- renderUI({ textboxes() })

})

shinyApp(ui, server)

The problem with this approach is that each time you press the add or remove button, all of the input boxes get re-rendered. This means that any input you might have had on them disappears.

I think you could get around that by also saving the current input values of the input boxes into a reactiveValues object, and setting the values from the object as the starting values of the re-rendered input boxes by using the value option in textInput. I'll leave the implementation of that for now, though.

Mikko Marttila
  • 10,972
  • 18
  • 31
  • Can you kinld have a look at this query please? https://stackoverflow.com/questions/45867997/dynamic-form-in-rshiny – RanonKahn Aug 24 '17 at 19:22
6

Thank you @Mikko Marttila for your answer. I was able to use it for my purpose. Also, referring to the issue of all input boxes getting re-rendered here I found a solution worked from this answer. You can save all user inputs using reactiveValuesToList(), then call the reactive list accordingly to set every value to the corresponding user's input in the lapply() statement.

library(shiny)

ui <- shinyUI(fluidPage(

  sidebarPanel(

    actionButton("add_btn", "Add Textbox"),
    actionButton("rm_btn", "Remove Textbox"),
    textOutput("counter")

  ),

  mainPanel(uiOutput("textbox_ui"))

))

server <- shinyServer(function(input, output, session) {

  # Track the number of input boxes to render
  counter <- reactiveValues(n = 0)

  # Track all user inputs
  AllInputs <- reactive({
    x <- reactiveValuesToList(input)
  })

  observeEvent(input$add_btn, {counter$n <- counter$n + 1})
  observeEvent(input$rm_btn, {
    if (counter$n > 0) counter$n <- counter$n - 1
  })

  output$counter <- renderPrint(print(counter$n))

  textboxes <- reactive({

    n <- counter$n

    if (n > 0) {
      isolate({
        lapply(seq_len(n), function(i) {
          textInput(inputId = paste0("textin", i),
                    label = paste0("Textbox", i), 
                    value = AllInputs()[[paste0("textin", i)]])
        })
      })
    }

  })

  output$textbox_ui <- renderUI({ textboxes() })

})

shinyApp(ui, server)

EDIT: I wrapped the lapply() statement in isolate() because it gets annoying when boxes are being re-rendered as you're trying to type in the field

J.Moon
  • 120
  • 1
  • 7
2

Instead of re-rendering the entire list of inputs, try the following

I keep track of all the ids that are created, I remove the last one created, and I reuse the ids of the deleted ones. I start with an initial box (there is no real need for that, but I guess in a real work scenario you would expect at least 1 textBox to appear and increase thereafter). It is straightforward to start without the initial box.

Also, I keep track of the values of the textInput boxes that are currently active, in a reactive List. You would definitely need this

Lastly, I think for the two reactiveValues I have [inserted and counter], one of them is possibly redundant, but hey...

Hope it helps!

library(shiny)

ui <- fluidPage(

  actionButton("insertBtn", "Insert"),
  actionButton("deleteBtn", "Delete"),
  h4("My boxes"),
  # Initial box here to start with. Not needed but it is nice to have one :)
  div(id = "box-1", textInput(inputId =  "box-1", label = "box-1")),
  div(id = "placeholder"),
  h4('Box contents'),
  verbatimTextOutput("my_inputs")
)

server <- function(input, output, session) {
  
  ## keep track of elements inserted and a counter of the elements
  rv <- reactiveValues(
    inserted = c("box-1"),
    counter = 1
  )
  
  observeEvent(input$insertBtn, {
    
    rv$counter <- rv$counter+1
    serial <- rv$counter
    id <- paste0('box-', serial)
    rv$inserted <- c(rv$inserted, id)
    
    insertUI(
  
      selector = '#placeholder',      
      ## wrap element in a div with id for ease of removal
      ui = div(id = id,
               textInput(inputId =  id, label = paste0("box-", serial))
      )
    )
      
  })
  
  observeEvent(input$deleteBtn, { 
    req(rv$counter>0) 
    # removes the last one 
    id_to_remove <- rv$inserted[length(rv$inserted)]
    
    removeUI(
      ## pass it in as JQuery selector
      selector = paste0('#', id_to_remove)
    )
    
    rv$inserted <- rv$inserted[-length(rv$inserted)]
    
    rv$counter <- rv$counter - 1
    
  })
  
  my_inputs <- reactive({
    
    req(rv$inserted) # need to have some inputs
    
    l <- reactiveValuesToList(input)
    
    # regex of the union of all inputs. Note the starting input box-1
    ids_regex <- paste(c("box-1", rv$inserted), collapse = "|")
    
    l[grepl(ids_regex, names(l))]
    
  })
  
  output$my_inputs <- renderPrint({
    
    my_inputs()
    
  })
  
  
}

shinyApp(ui, server)

enter image description here

Many thanks to

this post and this and these SO posts one, two

Lefkios Paikousis
  • 462
  • 1
  • 6
  • 12