R: Como separar elegantemente a lógica do código da UI / html-tags?

9

Problema

Ao criar dinamicamente ui-elementos ( shiny.tag, shiny.tag.list...), muitas vezes eu achar que é difícil separá-lo de minha lógica de código e geralmente acabam com uma bagunça complicada de nested tags$div(...), misturado com loops e instruções condicionais. Embora chato e feio de se ver, também é passível de erros, por exemplo, ao fazer alterações nos modelos html.

Exemplo reproduzível

Digamos que eu tenho a seguinte estrutura de dados:

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

Se agora quero inserir essa estrutura em tags de interface do usuário, geralmente acabo com algo como:

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)

Como você pode ver, isso já é bastante confuso e ainda nada comparado aos meus exemplos reais.

Solução desejada

Eu esperava encontrar algo próximo a um mecanismo de modelo para R, que permitisse definir modelos e dados separadamente :

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

Tentativas anteriores

Primeiro, pensei que shiny::htmlTemplate()poderia oferecer uma solução, mas isso funcionaria apenas com arquivos e seqüências de texto, não com shiny.tags. Também observei alguns pacotes r como o whisker , mas esses parecem ter a mesma limitação e não suportam tags ou estruturas de lista.

Obrigado!

Comfort Eagle
fonte
Você pode salvar um arquivo css na wwwpasta e aplicar as folhas de estilo?
MKa 14/11/19
No caso de aplicação de css, com certeza, mas eu estava procurando uma abordagem geral que permite mudanças em html-estrutura, etc.
Comfort Águia
Nada útil para adicionar, a não ser voto positivo e comentário em comiseração. Idealmente, htmlTemplate()permitiria condicionais e loops guidão ala, bigode, galho ...
será

Respostas:

2

Gosto de criar elementos de interface do usuário composíveis e reutilizáveis ​​usando funções que produzem tags HTML brilhantes (ou htmltoolstags). No seu exemplo de aplicativo, eu pude identificar um elemento "page" e, em seguida, dois contêineres de conteúdo genéricos e, em seguida, criar algumas funções para eles:

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

E então eu poderia compor minha interface do usuário com algo assim:

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)

Sempre que eu precisar ajustar o estilo ou o HTML de um elemento, basta ir direto para a função que gera esse elemento.

Além disso, acabei de incluir os dados neste caso. Eu acho que a estrutura de dados no seu exemplo realmente combina dados com preocupações da interface do usuário (estilo, tags HTML), o que pode explicar alguns dos problemas. Os únicos dados que vejo são "laranja" como cabeçalho e "impeach" / "tool" como conteúdo.

Se você tiver dados mais complicados ou precisar de componentes de interface do usuário mais específicos, poderá usar as funções novamente, como blocos de construção:

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

Espero que ajude. Se você estiver procurando por exemplos melhores, pode verificar o código-fonte por trás dos elementos de entrada e saída do Shiny (por exemplo selectInput()), que são essencialmente funções que emitem tags HTML. Um mecanismo de modelagem também pode funcionar, mas não há necessidade real quando você já tem htmltools+ a potência total de R.

greg L
fonte
Obrigado pela resposta! Eu costumava fazer isso assim também, mas se torna bastante impraticável quando grande parte do html não pode ser reutilizado. Eu acho que algum tipo de template-engine seria a única solução viável: /
Comfort Eagle
1

Talvez você possa considerar investigar glue()e get().

pegue():

get() pode transformar strings em variáveis ​​/ objetos.

Então você pode diminuir:

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

para

get(y$type)(y$value)

(veja o exemplo abaixo).

cola():

glue()fornece uma alternativa para paste0(). Poderia ser mais legível se você concentrar muitas seqüências de caracteres e variáveis ​​em uma sequência. Suponho que ele também se parece com a sintaxe do resultado desejado.

Ao invés de:

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

Você escreveria:

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

Seu exemplo seria simplificado para:

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

Usando:

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

Alternativas:

Acho que htmltemplate é uma boa ideia, mas outro problema são os espaços em branco indesejados: https://github.com/rstudio/htmltools/issues/19#issuecomment-252957684 .

Tonio Liebrand
fonte
Obrigado pela sua contribuição. Enquanto seu código é mais compacto, o problema de misturar html e lógica permanece. : /
Comfort Eagle