R: Как элегантно отделить логику кода от UI / html-тегов?

9

проблема

При динамическом создании 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")
    )
  )  
)

Если я теперь хочу вставить эту структуру в теги пользовательского интерфейса, я обычно получаю что-то вроде:

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-пакеты, такие как whisker , но они, похоже, имеют то же ограничение и не поддерживают теги или структуры списков.

Спасибо!

Комфорт Орел
источник
Вы можете сохранить файл CSS в wwwпапке, а затем применить таблицы стилей?
МК
В случае применения css, конечно, но я искал общий подход, который учитывает изменения в html-структуре и т. Д.
Comfort Eagle
Ничего полезного, чтобы добавить, но голосование и комментирование в сочувствии. В идеале учли htmlTemplate()бы условности и петли аля руль, усы, веточку ...
Будет

Ответы:

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+ полная мощность R.

Грег Л
источник
Спасибо за ответ! Я тоже делал это так, но это становится совершенно непрактичным, когда большая часть HTML не может быть повторно использована. Я думаю, что какой-то шаблонный движок был бы единственным жизнеспособным решением: /
Comfort Eagle
1

Может быть, вы могли бы рассмотреть 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#issuecomment-252957684 .

Тонио Либранд
источник
Спасибо за ваш вклад. Хотя ваш код более компактен, проблема смешивания html и логики остается. : /
Comfort Eagle