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.

Другие вопросы по тегам