R: Как элегантно отделить логику кода от UI / html-тегов?
Проблема
При динамическом создании 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")
)
)
)
Если теперь я хочу поместить эту структуру в 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.tag
с. Я также взглянул на некоторые r-пакеты, такие как Whker, но они, похоже, имеют такое же ограничение и не поддерживают теги или структуры списков.
Спасибо!
2 ответа
Мне нравится создавать составные и повторно используемые элементы пользовательского интерфейса, используя функции, которые создают блестящие HTML-теги (или htmltools
теги). В вашем примере приложения я мог бы идентифицировать элемент "страница", а затем два общих контейнера содержимого, а затем создать для них несколько функций:
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, ...)
}
А затем я мог бы составить свой пользовательский интерфейс примерно так:
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-код элемента, я просто перехожу прямо к функции, которая генерирует этот элемент.
Кроме того, я только что вставил данные в этом случае. Я думаю, что структура данных в вашем примере действительно смешивает данные с проблемами пользовательского интерфейса (стили, HTML-теги), что может объяснить некоторую запутанность. Единственные данные, которые я вижу, - это "оранжевый" в заголовке и "импичмент"/"инструмент" в качестве содержания.
Если у вас есть более сложные данные или вам нужны более конкретные компоненты пользовательского интерфейса, вы можете снова использовать функции, такие как строительные блоки:
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
+ полная мощность Р.
Может быть, вы могли бы рассмотреть возможность изучения glue()
а также get()
.
получить():
get()
может превращать строки в переменные / объекты.
Итак, вы можете сократить:
if (y[["type"]] == "h1") {
tags$h1(y[["value"]])
} else if (y[["type"]] == "p") {
tags$p(y[["value"]])
}
к
get(y$type)(y$value)
(см. пример ниже).
клей ():
glue()
предоставляет альтернативу paste0()
. Он мог бы быть более читабельным, если бы вы сконцентрировали в строке много строк и переменных. Я предполагаю, что это также похоже на синтаксис желаемого результата.
Вместо того:
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.