Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

R: How to elegantly separate code logic from UI / html-tags?

Problem

When dynamically creating ui-elements (shiny.tag, shiny.tag.list, ...), I often find it difficult to separate it from my code logic and usually end up with a convoluted mess of nested tags$div(...), mixed with loops and conditional statements. While annoying and ugly to look at, it's also error-prone, e.g. when making changes to html-templates.

Reproducible example

Let's say I have the following data-structure:

my_data <- list(
  container_a = list(
    color = "orange",
    height = 100,
    content = list(
      vec_a = c(type = "p", value = "impeach"),
      vec_b = c(type = "h1", value = "orange")
    )
  ),
  container_b = list(
    color = "yellow",
    height = 50,
    content = list(
      vec_a = c(type = "p", value = "tool")
    )
  )  
)

If I now want to push this structure into ui-tags, I usually end up with something like:

library(shiny)

my_ui <- tagList(
  tags$div(
    style = "height: 400px; background-color: lightblue;",
    lapply(my_data, function(x){
      tags$div(
        style = paste0("height: ", x$height, "px; background-color: ", x$color, ";"),
        lapply(x$content, function(y){
          if (y[["type"]] == "h1") {
            tags$h1(y[["value"]])
          } else if (y[["type"]] == "p") {
            tags$p(y[["value"]])
          }
        }) 
      )
    })
  )
)

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

As you can see, this already is quite messy and still nothing compared to my real like examples.

Desired solution

I was hoping to find something close to a templating engine for R, that would allow to define templates and data separately:

# syntax, borrowed from handlebars.js
my_template <- tagList(
  tags$div(
    style = "height: 400px; background-color: lightblue;",
    "{{#each my_data}}",
    tags$div(
      style = "height: {{this.height}}px; background-color: {{this.color}};",
      "{{#each this.content}}",
      "{{#if this.content.type.h1}}",
      tags$h1("this.content.type.h1.value"),
      "{{else}}",
      tags$p(("this.content.type.p.value")),
      "{{/if}}",      
      "{{/each}}"
    ),
    "{{/each}}"
  )
)

Previous attempts

First, I thought that shiny::htmlTemplate() could offer a solution, but this would only work with files and text strings, not shiny.tags. I also had a look at some r-packages like whisker , but those seems to have the same limitation and do not support tags or list-structures.

Thank you!

like image 723
Comfort Eagle Avatar asked Nov 13 '19 18:11

Comfort Eagle


2 Answers

I like creating composable and reusable UI elements using functions that produce Shiny HTML tags (or htmltools tags). From your example app, I could identify a "page" element, and then two generic content containers, and then create some functions for those:

library(shiny)

my_page <- function(...) {
  div(style = "height: 400px; background-color: lightblue;", ...)
}

my_content <- function(..., height = NULL, color = NULL) {
  style <- paste(c(
    sprintf("height: %spx", height),
    sprintf("background-color: %s", color)
  ), collapse = "; ")

  div(style = style, ...)
}

And then I could compose my UI with something like this:

my_ui <- my_page(
  my_content(
    p("impeach"),
    h1("orange"),
    color = "orange",
    height = 100
  ),
  my_content(
    p("tool"),
    color = "yellow",
    height = 50
  )
)

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

Any time I need to tweak the styling or HTML of an element, I'd just go straight to the function that generates that element.

Also, I've just inlined the data in this case. I think the data structure in your example really mixes data with UI concerns (styling, HTML tags), which might explain some of the convoluted-ness. The only data I see is "orange" as the header, and "impeach"/"tool" as the content.

If you have more complicated data or need more specific UI components, you can use functions again like building blocks:

my_content_card <- function(title = "", content = "") {
  my_content(
    h1(title),
    p(content),
    color = "orange",
    height = 100
  )
}

my_ui <- my_page(
  my_content_card(title = "impeach", content = "orange"),
  my_content(
    p("tool"),
    color = "yellow",
    height = 50
  )
)

Hope that helps. If you're looking for better examples, you can check out the source code behind Shiny's input and output elements (e.g. selectInput()), which are essentially functions that spit out HTML tags. A templating engine could also work, but there's no real need when you've already got htmltools + the full power of R.

like image 175
greg L Avatar answered Oct 12 '22 08:10

greg L


Maybe you could consider looking into glue() and get().

get():

get() can turn strings into variables/objects.

So you could shorten:

if (y[["type"]] == "h1") {
    tags$h1(y[["value"]])
} else if (y[["type"]] == "p") {
    tags$p(y[["value"]])
}

to

get(y$type)(y$value)

(see the example below).

glue():

glue() provides an alternative to paste0(). It could be more readable if you concentenate lots of strings and variables to a string. I assume it also looks close to the syntax of your desired result.

Instead of:

paste0("height: ", x$height, "px; background-color: ", x$color, ";")

You would write:

glue("height:{x$height}px; background-color:{x$color};")

Your example would simplify to:

tagList(
  tags$div(style = "height: 400px; background-color: lightblue;",
    lapply(my_data, function(x){
      tags$div(style = glue("height:{x$height}px; background-color:{x$color};"),
        lapply(x$content, function(y){get(y$type)(y$value)}) 
      )
    })
  )
)

Using:

library(glue)
my_data <- list(
  container_a = list(
    color = "orange",
    height = 100,
    content = list(
      vec_a = list(type = "p", value = "impeach"),
      vec_b = list(type = "h1", value = "orange")
    )
  ),
  container_b = list(
    color = "yellow",
    height = 50,
    content = list(
      vec_a = list(type = "p", value = "tool")
    )
  )  
)

Alternatives:

I think htmltemplate is a good idea, but another problem are the undesired whitespaces: https://github.com/rstudio/htmltools/issues/19#issuecomment-252957684.

like image 1
Tonio Liebrand Avatar answered Oct 12 '22 09:10

Tonio Liebrand