Using do.call to create ui in R Shiny

Viewed 341

I was seeing if it was possible to use do.call to create an R Shiny ui function and am at a loss to understand the issue below. Firstly if I do a basic example without do.call...

library(shiny)

ui <- fluidPage(
  titlePanel("A title"),
  fluidRow(column(width = 12,"some content",))
)

server <- function(input, output) {}
shinyApp(ui = ui, server = server)

...then this renders as expected with a page that shows "A title" and "some content" under.

Then if I try to do what I think is exactly the same using do.call as below...

library(shiny)

page_data <- c(
  titlePanel("A title"),
  fluidRow(column(width = 12,"some content",))
)
ui <- do.call(fluidPage, as.list(page_data))

server <- function(input, output) {}
shinyApp(ui = ui, server = server)

... then this does not render the same page. Interestingly it does run without error and shows "A title" but not "some content". Any ideas to explain why it would render differently? And also is it possible to implement using a do.call type interface?

2 Answers

You can define a list instead of a vector at the beginning, and therefore not use as.list() later. However, I don't know why using list() is different than as.list(c()):

library(shiny)

page_data <- list(
  titlePanel("A title"),
  fluidRow(column(width = 12,"some content",))
)
ui <- do.call(fluidPage, page_data)

server <- function(input, output) {}
shinyApp(ui = ui, server = server)

Edit: actually, we can see some differences between the two expressions:

list(
  titlePanel("A title"),
  fluidRow(column(width = 12,"some content",))
)

[[1]]
<h2>A title</h2>

[[2]]
<div class="row">
  <div class="col-sm-12">some content</div>
</div>
as.list(c(
  titlePanel("A title"),
  fluidRow(column(width = 12,"some content",))
))

[[1]]


[[2]]
<h2>A title</h2>

$name
[1] "div"

$attribs
$attribs$class
[1] "row"


$children
$children[[1]]
<div class="col-sm-12">some content</div>

The culprit is actually not as.list but c. Shiny UI functions do not return plain HTML as one could think, but a shiny.tag object (or shiny.tag.list list if several shiny.tag objects are grouped together). A shiny.tag object is a list that contains information about the name of the HTML tag used and its children (e.g. other tags and its contents, for more information have a look here). The reason we see the HTML code when a shiny.tag object is printed is because there are special print and as.character methods for shiny.tag and shiny.tag.list objects.

Have a look at the output of fluidRow(column(width = 12,"some content")):

str(fluidRow(column(width = 12,"some content")))
List of 3
 $ name    : chr "div"
 $ attribs :List of 1
  ..$ class: chr "row"
 $ children:List of 1
  ..$ :List of 3
  .. ..$ name    : chr "div"
  .. ..$ attribs :List of 1
  .. .. ..$ class: chr "col-sm-12"
  .. ..$ children:List of 1
  .. .. ..$ : chr "some content"
  .. ..- attr(*, "class")= chr "shiny.tag"
 - attr(*, "class")= chr "shiny.tag"

Internally it's a list! However, if printed it outputs:

print(fluidRow(column(width = 12,"some content")))
<div class="row">
  <div class="col-sm-12">some content</div>
</div>

The same is true for titlePanel("A title"), it actually generates 2 shiny.tag objects in a shiny.tag.list:

str(titlePanel("A title"))
List of 2
 $ :List of 3
  ..$ name    : chr "head"
  ..$ attribs : Named list()
  ..$ children:List of 1
  .. ..$ :List of 3
  .. .. ..$ name    : chr "title"
  .. .. ..$ attribs : Named list()
  .. .. ..$ children:List of 1
  .. .. .. ..$ : chr "A title"
  .. .. ..- attr(*, "class")= chr "shiny.tag"
  ..- attr(*, "class")= chr "shiny.tag"
 $ :List of 3
  ..$ name    : chr "h2"
  ..$ attribs : Named list()
  ..$ children:List of 1
  .. ..$ : chr "A title"
  ..- attr(*, "class")= chr "shiny.tag"
 - attr(*, "class")= chr [1:2] "shiny.tag.list" "list"

If you combine this as a list, the single shiny.tag objects still retain their list structure, so it's no problem for do.call(fluidPage, page_data_list) to generate the UI. You basically create a list of lists:

page_data_list <- list(
  titlePanel("A title"),
  fluidRow(column(width = 12,"some content"))
)

str(page_data_list, max.level = 1)
List of 2
 $ :List of 2
  ..- attr(*, "class")= chr [1:2] "shiny.tag.list" "list"
 $ :List of 3
  ..- attr(*, "class")= chr "shiny.tag"

However, c tries to combine lists. The existing list objects don't get their own list in the newly created list, but the highest list levels get combined (c basically appends the lists):

page_data <- c(
  titlePanel("A title"),
  fluidRow(column(width = 12,"some content"))
)

str(page_data, max.level = 1)
List of 5
 $         :List of 3
  ..- attr(*, "class")= chr "shiny.tag"
 $         :List of 3
  ..- attr(*, "class")= chr "shiny.tag"
 $ name    : chr "div"
 $ attribs :List of 1
 $ children:List of 1

as.list(page_data) doesn't change anything because page_data is already a list.

Now the shiny.tag objects don't have the correct structure anymore and do.call(fluidPage, page_data) doesn't work.

Related