R: Come separare elegantemente la logica del codice dai tag UI / html?


9

Problema

Quando si crea dinamicamente ui-elementi ( shiny.tag, shiny.tag.list, ...), spesso trovo difficile separare dalla mia logica del codice e di solito finisce con un pasticcio contorta di nidificato tags$div(...), mescolato con cicli e istruzioni condizionali. Mentre noioso e brutto da guardare, è anche soggetto a errori, ad esempio quando si apportano modifiche ai modelli HTML.

Esempio riproducibile

Diciamo che ho la seguente struttura di dati:

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 ora voglio inserire questa struttura in ui-tag, di solito finisco con qualcosa del tipo:

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)

Come puoi vedere, questo è già abbastanza disordinato e ancora nulla rispetto ai miei veri esempi simili.

Soluzione desiderata

Speravo di trovare qualcosa di simile a un motore di template per R, che consentisse di definire modelli e dati separatamente :

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

Tentativi precedenti

Innanzitutto, ho pensato che shiny::htmlTemplate()potesse offrire una soluzione, ma avrebbe funzionato solo con file e stringhe di testo, non con shiny.tags. Ho anche dato un'occhiata ad alcuni pacchetti-r come whisker , ma quelli sembrano avere la stessa limitazione e non supportano tag o strutture di lista.

Grazie!


Potresti salvare un file CSS nella wwwcartella e quindi applicare i fogli di stile?
MKa

Nel caso dell'applicazione di CSS, certo, ma stavo cercando un approccio generale che consenta cambiamenti nella struttura html, ecc.
Comfort Eagle

Nulla di utile da aggiungere se non il voto e il commento in commiserazione. Idealmente,htmlTemplate() consentirei condizionali e passanti come manubrio, baffi, ramoscello ...
Will

Risposte:


2

Mi piace creare elementi dell'interfaccia utente componibili e riutilizzabili utilizzando funzioni che producono tag (o htmltoolstag) Shiny HTML . Dalla tua app di esempio, ho potuto identificare un elemento "pagina", quindi due contenitori di contenuti generici e quindi creare alcune funzioni per quelli:

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 poi ho potuto comporre la mia interfaccia utente con qualcosa del genere:

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)

Ogni volta che ho bisogno di modificare lo stile o l'HTML di un elemento, passerei direttamente alla funzione che genera quell'elemento.

Inoltre, ho appena inserito i dati in questo caso. Penso che la struttura dei dati nel tuo esempio mescoli davvero i dati con le preoccupazioni dell'interfaccia utente (stile, tag HTML), il che potrebbe spiegare alcuni aspetti contorti. Gli unici dati che vedo sono "orange" come intestazione e "impeach" / "tool" come contenuto.

Se disponi di dati più complicati o hai bisogno di componenti dell'interfaccia utente più specifici, puoi utilizzare nuovamente le funzioni come blocchi predefiniti:

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

Spero che aiuti. Se stai cercando esempi migliori, puoi controllare il codice sorgente dietro gli elementi di input e output di Shiny (ad es. selectInput()), Che sono essenzialmente funzioni che sputano tag HTML. Un motore di template potrebbe anche funzionare, ma non c'è davvero bisogno quando hai già htmltools+ tutta la potenza di R.


Grazie per la risposta! Lo facevo anche in questo modo, ma diventa abbastanza poco pratico quando gran parte dell'html non può essere riutilizzato. Immagino che una sorta di template-engine sarebbe l'unica soluzione praticabile: /
Comfort Eagle

1

Forse potresti considerare di esaminare glue()e get().

ottenere():

get() può trasformare le stringhe in variabili / oggetti.

Quindi potresti accorciare:

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

per

get(y$type)(y$value)

(vedi l'esempio sotto).

colla():

glue()fornisce un'alternativa a paste0(). Potrebbe essere più leggibile se si concentrano molte stringhe e variabili in una stringa. Presumo che assomigli anche alla sintassi del risultato desiderato.

Invece di:

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

Scriveresti:

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

Il tuo esempio semplificherebbe a:

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

alternative:

Penso che htmltemplate sia una buona idea, ma un altro problema sono gli spazi bianchi indesiderati: https://github.com/rstudio/htmltools/issues/19#issuecomment-252957684 .


Grazie per il tuo contributo. Mentre il tuo codice è più compatto, rimane il problema di mescolare html e logica. : /
Comfort Eagle
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.