R:如何优雅地将代码逻辑与 UI/html-tags 分开?

Com*_*gle 9 r templating shiny htmltools

问题

在动态创建 ui 元素 ( shiny.tag, shiny.tag.list, ...) 时,我经常发现很难将它与我的代码逻辑分开,并且通常最终会出现一堆令人费解的嵌套tags$div(...),混合了循环和条件语句。虽然看起来令人讨厌和丑陋,但它也容易出错,例如在更改 html 模板时。

可重现的例子

假设我有以下数据结构:

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")
    )
  )  
)
Run Code Online (Sandbox Code Playgroud)

如果我现在想将此结构推送到 ui-tags 中,我通常会得到如下结果:

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)
Run Code Online (Sandbox Code Playgroud)

如您所见,与我的真实示例相比,这已经很混乱了,而且还算不上什么。

所需的解决方案

我希望找到接近R模板引擎的东西,这将允许分别定义模板和数据

# 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}}"
  )
)
Run Code Online (Sandbox Code Playgroud)

以前的尝试

首先,我认为这shiny::htmlTemplate()可以提供解决方案,但这仅适用于文件和文本字符串,而不适用于shiny.tags。我还查看了一些 r-packages,如whisker ,但它们似乎具有相同的限制,并且不支持标签或列表结构。

谢谢!

gre*_*g L 3

我喜欢使用生成闪亮 HTML 标签(或htmltools标签)的函数来创建可组合和可重用的 UI 元素。从您的示例应用程序中,我可以识别一个“页面”元素,然后识别两个通用内容容器,然后为它们创建一些函数:

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, ...)
}
Run Code Online (Sandbox Code Playgroud)

然后我可以用这样的东西来编写我的用户界面:

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)
Run Code Online (Sandbox Code Playgroud)

每当我需要调整元素的样式或 HTML 时,我都会直接转到生成该元素的函数。

另外,我刚刚在本例中内联了数据。我认为你的示例中的数据结构确实将数据与 UI 问题(样式、HTML 标签)混合在一起,这可能解释了一些令人费解的情况。我看到的唯一数据是“orange”作为标题,“impeach”/“tool”作为内容。

如果你有更复杂的数据或者需要更具体的UI组件,你可以像搭积木一样再次使用函数:

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
  )
)
Run Code Online (Sandbox Code Playgroud)

希望有帮助。如果您正在寻找更好的示例,您可以查看 Shiny 的输入和输出元素(例如selectInput())背后的源代码,这些元素本质上是吐出 HTML 标签的函数。模板引擎也可以工作,但当您已经拥有htmltoolsR 的全部功能时,就没有真正的需要了。