R: Comment séparer élégamment la logique de code des balises UI / html?

9

Problème

Lors de la création dynamique ui-éléments ( shiny.tag, shiny.tag.list, ...), je trouve qu'il est souvent difficile de le séparer de ma logique de code et finissent généralement avec un désordre alambiqué de imbriqué tags$div(...), mélangé avec des boucles et des instructions conditionnelles. Bien que gênant et laid à regarder, il est également sujet aux erreurs, par exemple lors de la modification de modèles html.

Exemple reproductible

Disons que j'ai la structure de données suivante:

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

Si je veux maintenant pousser cette structure en ui-tags, je me retrouve généralement avec quelque chose comme:

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)

Comme vous pouvez le voir, c'est déjà assez désordonné et toujours rien par rapport à mes vrais exemples.

Solution souhaitée

J'espérais trouver quelque chose de proche d'un moteur de template pour R, qui permettrait de définir des modèles et des données séparément :

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

Tentatives précédentes

Tout d'abord, je pensais que cela shiny::htmlTemplate()pourrait offrir une solution, mais cela ne fonctionnerait qu'avec des fichiers et des chaînes de texte, pas l' shiny.tagart. J'ai également jeté un coup d'œil à certains r-packages comme whisker , mais ceux-ci semblent avoir la même limitation et ne prennent pas en charge les balises ou les structures de liste.

Je vous remercie!

Comfort Eagle
la source
Vous pouvez enregistrer un fichier CSS sous wwwdossier, puis appliquer les feuilles de style?
MKa
Dans le cas de l'application de CSS, bien sûr, mais je cherchais une approche générale qui permette des changements dans la structure html, etc.
Comfort Eagle
Rien d'utile à ajouter, mais un vote positif et des commentaires de commisération. Idéalement, htmlTemplate()cela permettrait de conditionner et de boucler ala guidon, moustache, brindille ...
Will

Réponses:

2

J'aime créer des éléments d'interface utilisateur composables et réutilisables à l'aide de fonctions qui produisent des balises HTML (ou htmltoolsbalises) Shiny . À partir de votre exemple d'application, j'ai pu identifier un élément "page", puis deux conteneurs de contenu génériques, puis créer des fonctions pour ceux-ci:

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

Et puis je pourrais composer mon interface utilisateur avec quelque chose comme ceci:

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)

Chaque fois que j'ai besoin de modifier le style ou le HTML d'un élément, je vais directement à la fonction qui génère cet élément.

En outre, je viens de souligner les données dans ce cas. Je pense que la structure des données dans votre exemple mélange vraiment les données avec les problèmes d'interface utilisateur (style, balises HTML), ce qui pourrait expliquer une partie de l'altération. Les seules données que je vois sont "orange" comme en-tête et "impeach" / "tool" comme contenu.

Si vous avez des données plus complexes ou avez besoin de composants d'interface utilisateur plus spécifiques, vous pouvez à nouveau utiliser des fonctions comme des blocs de construction:

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

J'espère que cela pourra aider. Si vous cherchez de meilleurs exemples, vous pouvez consulter le code source derrière les éléments d'entrée et de sortie de Shiny (par exemple selectInput()), qui sont essentiellement des fonctions qui crachent des balises HTML. Un moteur de template pourrait également fonctionner, mais il n'y a pas vraiment besoin quand vous avez déjà htmltools+ la pleine puissance de R.

greg L
la source
Merci pour la réponse! J'avais l'habitude de le faire comme ça aussi, mais cela devient assez peu pratique quand une grande partie du HTML ne peut pas être réutilisée. Je suppose qu'une sorte de moteur de modèle serait la seule solution viable: /
Comfort Eagle
1

Peut-être pourriez-vous envisager d'examiner glue()et get().

avoir():

get() peut transformer des chaînes en variables / objets.

Vous pouvez donc raccourcir:

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

à

get(y$type)(y$value)

(voir l'exemple ci-dessous).

la colle():

glue()fournit une alternative à paste0(). Cela pourrait être plus lisible si vous concentrez beaucoup de chaînes et de variables à une chaîne. Je suppose que cela ressemble également à la syntaxe du résultat souhaité.

Au lieu de:

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

Vous écririez:

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

Votre exemple se simplifierait pour:

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

En utilisant:

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

Alternatives:

Je pense que htmltemplate est une bonne idée, mais un autre problème concerne les espaces blancs indésirables: https://github.com/rstudio/htmltools/issues/19#issuecomment-252957684 .

Tonio Liebrand
la source
Merci pour votre contribution. Bien que votre code soit plus compact, le problème du mélange du HTML et de la logique demeure. : /
Comfort Eagle