R:如何优雅地将代码逻辑与UI/html标记分离开来



问题

当动态创建ui元素(shiny.tagshiny.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")
)
)  
)

如果我现在想把这个结构推到ui标签中,我通常会得到这样的结果:

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)

正如你所看到的,这已经相当混乱了,与我真正喜欢的例子相比仍然算不上什么。

所需解决方案

我希望找到一个类似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}}"
)
)

以前的尝试

首先,我认为shiny::htmlTemplate()可以提供一个解决方案,但这只适用于文件和文本字符串,而不适用于shiny.tags,但这些似乎有相同的限制,不支持标签或列表结构。

谢谢!

我喜欢使用生成Shiny 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, ...)
}

然后我可以用这样的东西来编写我的UI:

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)

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

此外,在本例中,我刚刚内联了数据。我认为您的示例中的数据结构确实将数据与UI问题(样式、HTML标记(混合在一起,这可能解释了一些令人费解的地方。我看到的唯一数据是"橙色"作为标题,"弹劾"/"工具"作为内容。

如果你有更复杂的数据或需要更具体的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
)
)

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

也许您可以考虑研究glue()get()

get((:

get()可以将字符串转换为变量/对象。

所以你可以缩短:

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

get(y$type)(y$value)

(参见下面的示例(。

胶水((:

CCD_ 12提供了CCD_。如果在一个字符串中包含大量的字符串和变量,它可能会更可读。我想它看起来也很接近您想要的结果的语法。

代替:

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

你会写:

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

您的示例将简化为:

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)}) 
)
})
)
)

使用:

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")
)
)  
)

替代方案:

我认为htmltemplate是个好主意,但另一个问题是不需要的空白:https://github.com/rstudio/htmltools/issues/19#issuecomment-252957684.

最新更新