library(shinygouv)

Note sur les orientations initiales de développement : présentation des options

Comparaison entre les différentes possibilités pour répondre au besoin d’utilisation du framework DSFR

Pour ce faire, nous avons rédigé un tableau comparatif entre les éléments trouvés dans le framework DSFR et {shiny}. Il permet notamment de mettre en évidence la direction technique à employer selon deux méthodes envisagées pour utiliser DSFR dans une application {shiny}.

Récapitulatif

Option A {shiny} + {bslib} :

Utiliser les fonctionnalités de {bslib} pour utiliser le framework DSFR.

Avantages :

  • Simple d’utilisation, une fonction ou deux pour appliquer le bon thème bootstrap qui reprend les règles CSS de DSFR

Inconvénients:

  • Limité par les paramètres modifiables.
  • Nécessiterait de transformer le CSS de DSFR pour une compatibilité avec bootstrap (ce qui semble être un non-sens :)).
  • Il faut maintenir le thème DSFR qui est le résultat de la transformation du CSS. Donc une connaissance assez élevée sur bootstrap et DSFR.

ThinkR ne peut garantir le succès de cette méthode. Cette méthode n’a jamais été expérimentée.

Option B {shiny} et replace_me :) :

On utilise seulement le package {shiny} avec des scripts au démarrage pour remplacer les classes bootstrap par les classes de DSFR. Voici une app qui le fait https://github.com/MTES-MCT/dseshiny/blob/main/dseshiny/R/dse_ui_regexp.R

Exemple pour le actionButton:

library(shiny)
addResourcePath("fonts", system.file("dsfr-v1.6.0/dist/fonts", package = "shinygouv"))


ui <- fluidPage(
  includeCSS(system.file("dsfr-v1.6.0/dist/dsfr.min.css", package = "shinygouv")),
  h2("Exemple avec un bouton"),
  br(),
  actionButton("test", "test"),
  textOutput("texte")
)

remplace_bttn <- function(ui) {
  res <- rapply(
    ui,
    function(attribut) {
      gsub(
        x = attribut,
        pattern = "btn btn-default",
        replacement = "fr-btn"
      )
    },
    how = "replace",
    classes = "character"
  )
}

server <- function(input, output, session) {
  output$texte <- renderText({
    input$test
  })
}

shinyApp(remplace_bttn(ui), server)

Avantages:

  • Facile à mettre en oeuvre pour les utilisateurs.
  • Ne change pas les fonctions {shiny} à utiliser.
  • Applicable sur une application déjà existante.

Inconvénients:

  • Limité par ce que propose {shiny}. On ne peut appliquer que les classes DSFR qui ont un équivalent déjà existantes dans bootstrap.
  • Cela réécrit le code au lancement de l’application et peut donc altérer l’expérience utilisateur.
  • Ne permet pas d’appliquer les règles d’utilisations des différents composants de DSFR.
  • Double travail : il faut maintenir un tableau de correspondance entre les deux frameworks. Donc une connaissance assez élevée sur les deux.

Option C From scratch

Coder l’équivalent de la partie ‘UI’ {shiny} (ou quasiment) avec le framework DSFR. La partie ‘server’ reposera toujours sur la partie {shiny}. Un exemple : https://gitlab-forge.din.developpement-durable.gouv.fr/dreal-pdl/csd/shiny.dsfr et aussi https://github.com/ThinkR-open/w3css

Exemple de code pour actionButton :

library(shiny)

addResourcePath("fonts", system.file("dsfr-v1.6.0/dist/fonts", package = "shinygouv"))

actionButton_dsfr <- function(inputId, label) {
  tags$button(
    id = inputId,
    class = "fr-btn",
    value = 0,
    label
  )
}

ui <- fluidPage(
  tags$head(
    includeCSS(system.file("dsfr-v1.6.0/dist/dsfr.min.css", package = "shinygouv")),
    tags$script(
      "$(document).on('click', '.fr-btn', function(evt) {
  var el = evt.target;
  while (el.nodeName != 'BUTTON'){
    el = el.parentNode;
  }
  el = $(el);
  el.val( parseInt(el.val()) + 1)
  el.trigger('updated');
});
    var buttons = new Shiny.InputBinding();

$.extend(buttons, {
  find: function(scope) {

    return $(scope).find('.fr-btn')
  },
  getValue: function(el) {
    console.log($(el))
    return parseInt($(el).val())
  },
  setValue: function(el, value) {
    // JS code to set value
  },
  receiveMessage: function(el, data) {
    // this.setValue(el, data);
  },
  subscribe: function(el, callback) {
    $(el).on('updated.buttons', function(e) {
      callback();
    });

  },
  unsubscribe: function(el) {
    $(el).off('.buttons');
  }
});
Shiny.inputBindings.register(buttons);"
    )
  ),
  actionButton_dsfr("test", "test"),
)

server <- function(input, output, session) {
  observeEvent(input$test, {
    message(input$test)
  })
}

shinyApp(ui, server)

Ceci sera transformé en fonction et documenté dans un package

Avantages :

  • Permet de reprendre exactement la totalité des composants de DSFR
  • Ne dépend plus de bootstrap et {shiny} (mais n’est pas incompatible)
  • Permet de maintenir seulement les changements liés à DSFR
  • C’est la façon la plus propre et la solution la plus pérenne dans le temps pour les utilisateurs

Inconvénients:

  • Nécessite plus de développement et donc de temps
  • Oblige les utilisateurs à remplacer les fonctions {shiny} par les fonctions DSFR
  • Bonne connaissance en JS pour maintenir le package

On peut tout à fait imaginer une fonction qui parcourt le code pour remplacer les appels à {shiny} avec les fonctions {DSFR}.

Option D from scratch mais pas vraiment

On pourrait recoder les inputs en conservant les classes CSS qui permettent l’activation du backend JS (comme pour shinyWidgets).

Exemple pour le actionButton de {shinyWidgets} https://github.com/dreamRs/shinyWidgets/blob/46359eccb5cfaac80cbe2949da231e3416afcc37/R/actionBttn.R#L63

Ou pour notre actionButon :

Le fait de lui donner la classe action-button lui permet d’être reconnu par le JS de {shiny} et donc d’avoir une interaction. De même, le fait de lui donner la classe fr-btn en plus, lui permet d’être reconnu par le CSS du DSFR.

library(shiny)

addResourcePath("fonts", system.file("dsfr-v1.6.0/dist/fonts", package = "shinygouv"))

actionButton_dsfr <- function(inputId, label) {
  tags$button(
    id = inputId,
    class = "fr-btn action-button",
    value = 0,
    label
  )
}

ui <- fluidPage(
  includeCSS(system.file("dsfr-v1.6.0/dist/dsfr.min.css", package = "shinygouv")),
  actionButton_dsfr("test", "test")
)

server <- function(input, output, session) {
  observeEvent(input$test, {
    message(input$test)
  })
}

shinyApp(ui, server)

Avantages :

  • Permet de reprendre le JS de {shiny} pour les composants compatibles DSFR (exemple actionButton)

Néanmoins, le temps gagné sur les inputs basiques pourrait permettre d’implementer de nouveaux inputs si nécéssaire

  • Implémentation plus simple pour les composants compatibles.
  • Pas besoin de connaissances avancées en JS pour les composants compatibles.

Inconvénients :

  • Lié au package {shiny} et aux inputs implémentés
  • Si l’input n’existe pas dans {shiny}, alors il faudra recoder la partie JS (c’est vrai pour les options A et B)
  • Maintenance : suivre {shiny} et DSFR. Autrement dit, si la classe de l’input {shiny} devient autre chose que action-button notre code ne marchera plus (peu probable…).
  • Les librairies bootstrap qui gravitent autour de {shiny} ne sont pas compatibles DSFR. Il faudra donc les remplacer par les librairies maison ou des implémentation de lib JS. Un exemple : shinyWidget

Les changements sont énumérés:

On peut tout à fait imaginer une fonction qui parcourt le code pour remplacer les appels à {shiny} avec les fonctions {DSFR}.

Les inputs recensés dans les apps “ssm-écologie” par option

Les éléments tels que navbarPage(), tabPanel() etc sont possibles dans la limite de ce que propose le DSFR. Ils nécessitent plus ou moins une demi-journée par composant (codé avec des htmlTemplates).

Notre recommandation

Nous préconisons l’option D. Pourquoi ?

Cette approche permet de mettre en avant le travail réalisé sur le framework DSFR avec une implémentation possible de l’intégralité des fonctionnalités sans la contrainte et la lourdeur de connaitre le JS. Elle permet aussi de faciliter la maintenance du package par les futures mainteneurs. Bien que cette option ne réponde pas directement à l’objectif : éviter d'avoir des fonctions spécifiques pour ne pas géner le passage d'un template à un autre, on peut tout à fait imaginer une fonction qui parcourt le code pour remplacer les appels à {shiny} avec les fonctions {DSFR}.