knitr::opts_chunk$set(echo = TRUE)

What Is A Shiny Module

A shiny module contains a UI function and a server function. Both functions require the input id that is a string and much match between the UI and server for the same module instance. For a nice tutorial on shiny modules see this website.

UI module function

The module's UI function specifies how the shiny module will be displayed to the user. The string variable id should be an input for every model UI as it is used to create the namespace. The main aspect of a UI module is the namespace function ns <- shiny::NS(id). This is put as the first line of any module UI function. Effectively, what this function does is append the id string to the input of the ns() function. For example, if a user calls the UI module by running exampleViewer(id = 'example_name') then the namespace function ns() will concatenate the string 'example_name' to every reference in the UI, e.g., ns('serverReference') = 'example_name_serverReference'. Therefore, as long as the id input values are unique for each instance of a module, the references within the modules viewer and server will be unique and not clash across modules.

exampleViewer <- function(id) {
  ns <- shiny::NS(id)

  shiny::fluidRow(

    shiny::column(width = 12,

                  shinydashboard::box(
                    width = 12,
                    title = "Example Title ",
                    status = "info", 
                    solidHeader = TRUE,
                    DT::dataTableOutput(ns('serverReference'))              
                  )
    )
  )
}

Server module function

The module's server function is where all the data fetching and manipulation happens. The module server and UI server is linked by using the same id for example, when calling the server you would use exampleServer(id = 'example_name'). For the example above, the server function needs to specify what the serverReference dataTable is. The server automatically knows the namespace, so the DT::dataTableOutput(ns('serverReference')) can be define using the output output$serverReference (no namespace fucntion is require in the server code).

exampleServer <- function(
  id,
  extraInput
  ) {
  shiny::moduleServer(
    id,
    function(input, output, session) {

      output$serverReference <- data.frame(a=1:5, b=1:5)

    }
  )
}

Calling the modules in the main UI and server

ui <- fluidPage(
  exampleViewer("exampleName")
)

server <- function(input, output, session) {
  exampleServer("exampleName")
}

shinyApp(ui, server)

OHDSI Package Module Structure

A package's shiny app modules should be put inside the package inst/shiny folder and have the following structure:

+-- module.r
+-- helpers.r
+-- /www
+-- /modules
+-- /tests

Module R file

The module.r file contains the UI function and the server function for the main module.


module environment function

[Add instructions and example function to create the environment with all the functions used by the module]

ui function

server function

Each OHDSI package shiny module should interact with a database backend and therefore the standard input into an OHDSI package shiny module server is the database connection details, result schema, string to append to the result schema plus any module specific inputs. These should be contained in a list called resultDatabaseSettings. Therefore the module server should start with the following code:

exampleServer <- function(
  id, 
  resultDatabaseSettings
  ) {
  shiny::moduleServer(
    id,
    function(input, output, session) {

    }
    )
  }

loading modules

[talk about how to stop everything loading at launch]

Helper.r files

The helper-.r file contains any functions used across the submodules of a module.

www folder

The folder www contains all the html files that are used to display help information to the user ineracting with the shiny app. This folder should contain html files that will be loaded into the shiny app by a submodule or the main server function in module.r. Include javascript/images as well.

Sub-modules

This is a folder with shiny modules that are used by the main package module (termed sub-modules). For the PatientLevelPrediction shiny app, there were a set of tabs with different aspects of a model (discrimination, calibration, model, model design, etc.) and these tabs were independant of each other. This made is possible to create shiny modules per tab and these are the PatientLevelPrediction submodules.

Shiny App Testing

Testing shiny modules is similar to testing R packages with testthat, however, there are some notable specific differences for this set up that must be followed. The reccomended structure is to include a tests directory in the module's root.

Creating test files

In the test directory test files should have the prefix test-*.R, so creating a test file should be named tests/test-MyModuleServerTest.R. For resued fixtures and useful functions in tests, you can also include the files tests/setup.R and tests/helper-<name>.R which will be loaded when the tests start. For example, database connection strings that are specific to tests should be included in setup.R.

Tests can follow the normal (testthat patterns)[https://r-pkgs.org/tests.html#test-structure], however, testing a shiny module requires loading a shiny test server that can be used to simulate user inputs and test the behaviour of shiny::reactive calls as well as outputs. For example:

source("../module.R")
source("../modules/MyModuleCode.R")

# Imagine this code lives inside your module
exampleModuleServer <- function(id) {

  ns <- shiny::NS(id)
  shiny::moduleServer(id, function(input, output, session) {
    myReactive <- shiny::reactive({ paste("Input is ", input$testString) })

    output$testOutput <- shiny::renderText(myReactive())
  })
}

test_that("Example Test", {
  shiny::testServer(exampleModuleServer, args = list(id = "testModule"), {

    # session$setInputs allows simulation of user behaviour
    session$setInputs(
      testString = "foo"
    )

    # after modifying input output can be verified
    expect_equal(myReactive(), "Input is foo")
    expect_equal(output$testOutput, "Input is foo")

  })

})

Note the use of ../ in the path for source - this is becase code must be sourced relative to the tests directory!

Test data and fixtures

The modules themselves will also require test data. For that reason it is reccomended that a sqlite database containing test data should either be created in setup.R or be stored as flat files within the test directory. For example, creating a test sqlite database with the path tests/testDb.sqlite.

Global setup files

The following tests/setup.R file loads the required database connection string:

# setup.R
connectionDetails <- DatabaseConnector::createConnectionDetails(dbms = 'sqlite', server = 'testDb.sqlite')               

Running tests

These tests can then be called with the testthat command:

testthat::test_dir('<path_to_module>/tests')  

Github actions

Adding the following to a github actions script should allow tests to be run, for example as part of a HADES package where shiny source code is stored in inst/shiny/MyModule:

## Assumed to be after normal R package checkes
...
  ## Assumes an Renv is required for the shiny code. 
  ## If required packages are already installed skip this step       
  - name: Set up DiagnosticsExplorer Environment
    run: |
      install.packages("renv")
      setwd("inst/shiny/MyModule")
      renv::restore()
    shell: Rscript {0}

  - name: Test My shiny module
    run: |
      renv::activate("inst/shiny/MyModule/")
      testthat::test_dir("inst/shiny/MyModule/tests")
    shell: Rscript {0}

Best Practices & Tips

[add any naming conventions, code style conventions, etc. here]



jreps/shinyModuleViewer documentation built on Oct. 8, 2022, 11:58 p.m.