Mark Rothfuss Mark Rothfuss - 3 months ago 39
R Question

How do I make dynamically created inputs in R/Shiny flow like normal inputs in flowLayout?

I have a mix of dynamically created inputs and normally defined inputs. The dynamically created inputs behave like a large vertically stacked block that the other inputs flow around. How do I make them all flow together?

This reproduces the issue:

library(shinydashboard)
shinyApp(
ui = dashboardPage(
dashboardHeader(),
dashboardSidebar(),
dashboardBody(
flowLayout(
uiOutput('input_fields'),
textInput('fielda','Field A',''),
textInput('fieldb','Field B',''),
textInput('fieldc','Field C',''),
textInput('fieldd','Field D','')
)
)
),
server = function(input, output) {
output$input_fields <- renderUI({
lapply(1:4, function(i) {
textInput(paste0('field',i), paste('Field',i),'')
})
})
}
)


Current layout:



Desired layout:



Edit: Ignore the Field A,B,C,D widgets. They are just for showing how other items wrap but the uiOutput ones act as one block. Assume all inputs are dynamic.

I think I'm to use this code instead. It moves the renderUI inside the lapply to create multiple generic outputs (varies at runtime, up to 10) which I can reference with equally generic uiOutput statements. It doesn't even warn about the 5 outputs that don't exist. Not perfect but it'll do. Probably need to add in an observeEvent as I don't think it will be reactive on its own.

library(shinydashboard)
shinyApp(
ui = dashboardPage(
dashboardHeader(),
dashboardSidebar(),
dashboardBody(
wellPanel(
flowLayout(
uiOutput('field1'),
uiOutput('field2'),
uiOutput('field3'),
uiOutput('field4'),
uiOutput('field5'),
uiOutput('field6'),
uiOutput('field7'),
uiOutput('field8'),
uiOutput('field9'),
uiOutput('field10')
)
)
)
),
server = function(input, output) {
lapply(1:5, function(i) {
output[[paste0('field',i)]] <- renderUI({
textInput(paste0('field',i), paste('Field',i),'')
})
})
}
)

Answer

All four dynamic generated widgets are treated as one object because they are contained in a list. Therefore it is quite difficult to position them. The easiest solution I can think of is to use insertUI function from the newest shiny version which can be installed using devtools package:

devtools::install_github("rstudio/shiny")

(Here you can read/learn more about insertUI and removeUI)

We have to tell insertUI where it should add new widgets to the user interface, and we do it by specifying the parameter selector (it has to be a string that is accepted by jQuery selector) and the parameter where (there are four choices which which specify how widgets should go relative to the selector)

So if we want to add a new widget under Field 1 we have to set selector = "#fielda" (We use # selector since we are referring to the ID). Similarly, we add another widget under Field 2 by setting selector = "#fieldb" and so on. Following your naming convention we can generalize it to:

selector = paste0('#field', letters[i])

In all cases we also set where = "afterEnd. It will position new widgets below reference widgets. (There is no space to position them above reference widgets)

Now we can wrap insertUI into a for-loop

for (i in 1:4) { 
      insertUI(
        selector = paste0('#field', letters[i]), 
        where = "afterEnd",
        ui = textInput(paste0('field',i), paste('Field',i),'')
      )
    }

Note that we don't need to add any uiOutput on the client side.

The other solution would be to render whole dashboardBody on the server side creating dynamically distinct uiOutputs...which wouldn't be pleasant.


enter image description here

When we take a look at the picture we can see that I did it the other way around - capital letters in names in the first row and numbers in names in the second row. It shouldn't be a big deal I hope :)


Full example:

library(shiny)
library(shinydashboard)

shinyApp(
  ui = dashboardPage(
    dashboardHeader(),
    dashboardSidebar(),
    dashboardBody(
      flowLayout(
        textInput('fielda','Field A',''),
        textInput('fieldb','Field B',''),
        textInput('fieldc','Field C',''),
        textInput('fieldd','Field D','')
      )
    )
  ),
  server = function(input, output) {

    for (i in 1:4) { 
      insertUI(
        selector = paste0('#field', letters[i]), 
        where = "afterEnd",
        ui = textInput(paste0('field',i), paste('Field',i),'')
      )
    }
  }
)

EDIT:

If there are no reference widgets then you can also dynamically define flowLayout on the server side using non-standard evaluation.

Say, you want to generate three textInput widgets. You would normally do this by typing

flowLayout(
  textInput('fielda', 'Field A'),
  textInput('fieldb', 'Field B'),   
  textInput('fieldc', 'Field C')
)

on the client side. You can do the same thing by pasting strings with paste0 function on the server side,

        i = 1:3
        UI <- paste0("flowLayout(",
                paste0("textInput(", 
                       "'field", letters[i], "', ",
                       paste0("'Field ", LETTERS[i], "'"), 
                       ")",
                       collapse = ", "),
              ")")

saving it in a variable, say, UI, then parsing string and finally evaluating it.

eval(parse(text = UI))

In the example below you have 15 dynamically generated widgets

enter image description here

Full example:

library(shiny)
library(shinydashboard)

shinyApp(
  ui = dashboardPage(
    dashboardHeader(),
    dashboardSidebar(),
    dashboardBody(
      wellPanel(
      uiOutput("ui1")
    )
   )
  ),
  server = function(input, output) {

      output[["ui1"]] <- renderUI({
        i = 1:15
        UI <- paste0("flowLayout(",
                paste0("textInput(", 
                       "'field", letters[i], "', ",
                       paste0("'Field ", LETTERS[i], "'"), 
                       ")",
                       collapse = ", "),
              ")")
        # print(UI)
        eval(parse(text = UI))
      })

  }
)
Comments