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

2

10

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!

Mottle answered 13/11, 2019 at 18:39 Comment(3)
You could save a css file under www folder and then apply the style sheets?Lashley
In the case of applying css, sure, but I was looking for a general approach that allows for changes in html-structure, etc.Mottle
Nothing useful to add but upvoting and commenting in commiseration. Ideally, htmlTemplate() would allow for conditionals and loops ala handlebars, mustache, twig...Hexachlorophene
W
3

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.

Wescott answered 28/11, 2019 at 18:6 Comment(1)
Thank you for the answer! I used to do it like this as well, but it becomes quite impractical when much of the html cannot be reused. I guess some kind of template-engine would be the only viable solution :/Mottle
T
1

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.

Trichoid answered 30/11, 2019 at 0:52 Comment(1)
Thanks for your input. While your code is more compact, the issue of mixing html and logic remains. :/Mottle

© 2022 - 2024 — McMap. All rights reserved.