Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically add and remove uiOutput elements based on index using actionButtons

Tags:

r

shiny

I'm trying to add and remove uiOutput elements using an index to keep track of each individual element. There is an actionButton for adding and element to the list, and an x button for each element that removes the selected item, as in the image below:

Shiny dynamic elements example

I'm using a single .Rmd file that includes both ui and server code. My current solution (with which I cannot produce the desired functionality shown above---it basically does nothing) is the following:

actionButton("addFilter", "Add filter", icon=icon("plus", class=NULL, lib="font-awesome"))

i <- 0

observeEvent(input$addFilter, {
  i <<- i + 1
  uiOutput(paste("filterPage",i,sep=""))

  output[[paste("filterPage",i,sep="")]] = renderUI({
    fluidPage(
      fluidRow(
        column(6, selectInput(paste("filteringFactor",i,sep=""), "Choose factor to filter by:",
                              choices=c("factor A", "factor B", "factor C"), selected="factor B",
                              width="100%")),
        column(6, actionButton(paste("removeFactor",i,sep=""), "",
                               icon=icon("times", class = NULL, lib = "font-awesome")))
      )
    )
  })

  observeEvent(input[[paste("removeFactor",i,sep="")]], {
    output[[paste("filterPage",i,sep="")]] = renderUI({})
  })

})

When I put uiOutput and the remove-button observeEvent outside of the add-button observeEvent the code works, but I need to have a separate statement per index, as follows:

uiOutput(paste("filterPage",1,sep=""))
uiOutput(paste("filterPage",2,sep=""))
uiOutput(paste("filterPage",3,sep=""))
uiOutput(paste("filterPage",4,sep=""))

actionButton("addFilter", "Add filter", icon=icon("plus", class=NULL, lib="font-awesome"))

i <- 0

observeEvent(input$addFilter, {
  i <<- i + 1
  output[[paste("filterPage",i,sep="")]] = renderUI({
    fluidPage(
      fluidRow(
        column(6, selectInput(paste("filteringFactor",i,sep=""), "Choose factor to filter by:",
                              choices=c("factor A", "factor B", "factor C"), selected="factor B",
                              width="100%")),
        column(6, actionButton(paste("removeFactor",i,sep=""), "",
                               icon=icon("times", class = NULL, lib = "font-awesome")))
      )
    )
  })
})

observeEvent(input[[paste("removeFactor",1,sep="")]], {
  output[[paste("filterPage",1,sep="")]] = renderUI({})
})
observeEvent(input[[paste("removeFactor",2,sep="")]], {
  output[[paste("filterPage",2,sep="")]] = renderUI({})
})
observeEvent(input[[paste("removeFactor",3,sep="")]], {
  output[[paste("filterPage",3,sep="")]] = renderUI({})
})
observeEvent(input[[paste("removeFactor",4,sep="")]], {
  output[[paste("filterPage",4,sep="")]] = renderUI({})
})

I couldn't make a for loop or lapply call to work (it looks like a scoping problem I do not understand). As the number of elements is not known in advance hardcoding the values will not work for me. Does anybody know how to make this work? Thanks.

like image 935
Orestis Tsinalis Avatar asked May 29 '16 00:05

Orestis Tsinalis


1 Answers

I have a fix for you, that might not be beautiful on the backend, but makes life very easy. I'd suggest you nest all your elements within each other. Make the uiOutput number i contain the uiOutput for the next one. This way, you can add them successively without needing to worry about size or total number of elements. Performance-wise this will also be okay, especially since I don't think you are creating thousands of filters. See code for more details.

For deleting, you have already seen that it's tedious to keep track of all button input values. So I'd suggest you design one variable to observe, which tells you the element number, that has been clicked. We can achieve this with custom onclick function for our buttons, sending the button number to one single input variable. You might want to make yourself familiar with the JavaScript client function Shiny.onInputChange. Just briefly: it sends a value to the R backend under the given variable name.

Deleting the corresponding ui element is easy.

If you want to get more into this and design a more clear, more fine solution, try this page and related questions. There you can get input on how to design and add chunks of dynamic ui into a document without the commonly used single uiOutput wrapper.

Code below (I made a regular ui-server app):

library(shiny)

ui <- shinyUI(
  fluidPage(

    actionButton("addFilter", "Add filter", icon=icon("plus", class=NULL, lib="font-awesome")),

    uiOutput("filterPage1")
  )
)

server <- function(input, output){
  i <- 0

  observeEvent(input$addFilter, {
    i <<- i + 1
    output[[paste("filterPage",i,sep="")]] = renderUI({
      list(
        fluidPage(
          fluidRow(
            column(6, selectInput(paste("filteringFactor",i,sep=""), "Choose factor to filter by:",
                                choices=c("factor A", "factor B", "factor C"), selected="factor B",
                                width="100%")),
            column(6, actionButton(paste("removeFactor",i,sep=""), "",
                                 icon=icon("times", class = NULL, lib = "font-awesome"),
                                 onclick = paste0("Shiny.onInputChange('remove', ", i, ")")))
          )
        ),
        uiOutput(paste("filterPage",i + 1,sep=""))
      )
    })
  })

  observeEvent(input$remove, {
    i <- input$remove

    output[[paste("filterPage",i,sep="")]] <- renderUI({uiOutput(paste("filterPage",i + 1,sep=""))})
  })
}

shinyApp(ui, server)
like image 106
K. Rohde Avatar answered Oct 23 '22 05:10

K. Rohde