Modules Shiny (partie 3) :
Appel dynamique de modules

Quentin Fazilleau

2019/07/01


Dans l’article précédent nous avons vu les mécanismes de transfert de données entre l’application Shiny et ses modules.

Maintenant que les modules n’ont (presque plus) de secrets pour vous ; peut-être êtes-vous intéressé pour utiliser un même module plusieurs fois sans pour autant coder chaque appel ?

Dans cet article, nous verrons comment appeler dynamiquement un module et gérer ses outputs server et/ou UI avec 2 exemples :

  • Récupérer l’UI d’un module apellé dynamiquement.
  • Récupérer l’UI & server d’un module appelé dynamiquement.

Si vous souhaitez jouer les exemples en local

Tous les codes utilisés ici sont disponibles sur Github ardata-fr/Shiny-Modules-Tutorials. Ce dépôt est un package R dont les modules sont les fonctions exportées. Les applications Shiny sont dans le dossier inst.

Installer le package et lancer localement les applications :

# install.packages("remotes")
remotes::install_github("ardata-fr/Shiny-Modules-Tutorials")
library(shinyModulesTuto)
# Lister les applications disponibles
listEx()
# Lancer la 1ere application
runEx(listEx()[1])

Exemple 1 : Récupérer seulement l’output UI

Comme vous le savez, un module est composé de 2 fonctions. L’interface UI & la logique server.

Dans cet exemple, la logique server ne retourne rien. Nous avons simplement à gérer les sorties UI.

https://ardata.shinyapps.io/dynamic-call/ ou bien lancer la commande :

# Lancer localement l'application exemple
runEx("dynamic-call")

NB : Dans cet exemple, chaque sortie UI est shinyWidgets::panel.

Application demo

Application demo

Initialiser rv$all_ui

La reactive rv est initialisée comme ci-dessous :

rv <- reactiveValues(all_ui = list())

Appel dynamique au travers d’un observeEvent

A chaque chargement d’un dataset (cf : incrémentation de data_mod1$trigger), l’observeEvent :

  1. Exécute la partie server logique du module
callModule(
    module = data_info,
    id = data_mod1$trigger,
    data = data_mod1$data,
    data_name = data_mod1$data_name
)
  1. Récupére la sortie UI dans la reactive rv$all_ui
rv$all_ui[[data_mod1$trigger]] <- data_infoUI(id = data_mod1$trigger)

Agencement des éléments UI

Dans cet exemple, chaque sortie UI est un shinyWidgets::panel, ainsi on peut les utiliser dans un tagList :

output$all_results <- renderUI({
    tagList(rv$all_ui)
})

Exemple 2 : Récupérer les sorties UI & server

Est-ce que vous vous souvenez de l’application whole-app dans l’article précédent ?

Elle est encore disponible en ligne ou vous pouvez la lancer localement :

# Lancer localement l'application exemple
runEx("whole-app")

Pour cet exemple, j’ai créé le module merge_modules qui contient les modules utilisés dans whole-app sauf load_data. L’objectif est de faire une application contenant le module load_data pour envoyer les données vers le “super-module” merge_modules.

Ce “super-module” merge_modules retourne les parties UI de tous les modules sous-jacents. Sa partie server renvoie une reactiveValues contenant le nom ainsi que le nombre de fonctions appliquées sur chaque jeu de données chargé.

L’application est disponible en ligne https://ardata.shinyapps.io/dynamic-call-whole-app/ ou bien vous pouvez la lancer localement :

# Lancer localement l'application exemple
runEx("dynamic-call-whole-app")

NB : L’application est organisée avec un tabsetPanel, chaque sortie de merge_modules est ajoutée au travers d’un nouveau tabPanel. De plus, chaque tabPanel contient un bouton de fermeture.

Application demo

Application demo

Initialisation des variables

trick     <- reactiveVal(0)
res       <- list()
obs_close <- list()
  • La reactiveVal trick est un entier qui s’incrémente à chaque appel du module merge_modules. Elle est ensuite utilisée comme id pour tabPanel et pour les noms d’éléments de la liste res.
  • La liste res contient les sorties server de chaque appel au module merge_modules.
  • La liste obs_close contient les observers qui se déclenchent pour la fermeture des tabPanel.

Appel dynamique au travers d’un observeEvent

A chaque chargement de dataset (cf : incrémentation de data_mod1$trigger), l’observeEvent :

  1. Exécute la partie server logique du module et stocke le résultat dans la liste res.
res[[paste(id)]] <<- callModule(
    module = merge_modules,
    id = id,
    data = data,
    name = name
)
  1. Récupère la sortie UI et crée directement un tabPanel (via la fonction appendTab) :
appendTab(
    inputId = "all_tabs",
    tabPanel(
        title = tabTitle(name, id),
        value = id,
        tags$br(),
        merge_modulesUI(id = id)
    ),
    select = TRUE
)
  1. Ajoute un observer dans la liste obs_close pour déclencher la fermeture du tabPanel :
obs_close[[paste(id)]] <<- observe({
    shinyjs::onclick(id = paste0("close", id), closeTab(id = id))
})
  1. Incrémente la reactiveValue trick
trick(trick() + 1)

NB : L’incrémentation de trick délenche la mise à jour du renderUI ui_summary.

La reactiveVal trick est indispensable pour mettre à jour ui_summarycar la liste res n’est pas reactive.

Utiliser les sorties server dans l’application

La liste res peut être utilisée n’importe où dans l’application. Cependant, il faut penser à référencer la reactiveVal trick afin que le contexte reactif se mette à jour lorsque res est modifié.

# Summary of all opened tabs
output$ui_summary <- renderUI({
    # Reference trick to update reactive context
    fool <- trick()
    
    zz <- lapply(res, reactiveValuesToList)

    if (length(zz) == 0) {
        content <- div(class = "warn ", "No variable loaded")
    } else {
        # Use output of module merge_modules :
        #  - name
        #  - nb_funs
        content <- tags$ul(
          lapply(zz, function(x) {
            tags$li(paste(x$name, ":", x$nb_funs))
          })
        )
    }

    tagList(content)
})

Bonus : Fermer un tabPanel

Dans cet exemple, on peut “supprimer” l’appel à un module. Nous devons défaire les étapes réalisées au chapitre précédent :

  • Supprimer le tabPanel correspondant (UI part).
  • Supprimer l’élément correspondant dans la liste res (Server part).

Ces 2 étapes sont réalisées dans la fonction closeTab décrite ci-dessous. Cette fonction est lancée dans l’observer créé dynamiquement et situé dans obs_close :

closeTab <- function(id) {
    # Remove tab from UI
    removeTab(inputId = "all_tabs", target = paste(id))
    
    # Remove related element from list
    res[[paste(id)]] <<- NULL
    
    # Increase trick to refresh output$ui_summary
    trick(trick() + 1)
}

Le bouton de fermeture du tabPanel est créé avec la fonction ci-dessous :

# Function to add a cross after tabPanel title
tabTitle <- function(name, id) {
    tags$span(
        name, HTML("&nbsp;"),
        tags$span(
            id = paste0("close", id),
            class = "close",
            HTML("&times;")
        )
    )
}

Conclusion

L’appel dynamique à un module est très pratique lorsque celui-ci :

  • Peut être exécuté un nombre de fois indéfini.
  • Executé à partir d’une action utilisateur.

L’appel se fait ensuite dans un observeEvent ou bien un observe puis :

  • Si on a besoin de la sortie server alors la stocker comme nouvel élément d’une liste.

  • Pour la sortie UI deux solutions :

    • Stocker la sortie UI dans une reactiveValues (cf exemple 1).
    • Utiliser directement la sortie UI dans l’observer (cf exemple 2).

Etre le maître des modules Shiny

Au travers de ces 3 articles, nous avons vu :

J’espère que ces articles vous seront d’une grande aide pour que vous puissiez prendre un max de plaisir à développer vos applications Shiny !