knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )
This vignette will provide an overview of the formods framework for
creating reproducable modules that interact with each other. Each module
has its own namespace that is mantained by using a module short name as
a prefix for functions. For example the figure generation module uses
FG
. If you want to create a module, please submit an issue at the
formods github
repository with the
following information:
The following modules are currently available:
Other short names in use:
Currently in development:
To get started you need to create some template files. The example below assumes you are creating this module for a package called mypackage
and that you are running this command in a git repository. Say that this module is used to produce widgets
, the short name is MM
which stands for My Module
:
use_formods(SN = "MM", Module_Name = "My Module", element = "widgets", package = "mypackage")
This command will create the following files:
MM_module_components.R
- An app that can be used for testing the
module and highlighting the different UI elements that are used
within the module (found in inst/templates
).MM_Server.R
: A bare bones file containing the expected functions
and their minimal inputs. (found in R
).MM.yaml
: This module configuration file contains the the minimal
elements expected, but you can add your own fields to suit your
modules needs (found in inst/templates
).MM_funcs.R
: This script contains example code for the different elements
of the module that can be used in the function examples and also to quickly
test the different module functions while you develop them
(found in inst/test_apps
).MM_preload.yaml
: This yaml file contains a skeleton of a preload
configuration file. This will need to be populated to work with
MM_test_mksession()
(found in MM_funcs.R
) in order to create a
testing environment for your module (found in inst/prelaod
).The module template will create a standard set of functions for you. The MM
below will be replaced with whatever short name you choose above when you create the templates. These functions can be customized for your specific module. Some are optional and can be deleted. For example the MM_fetch_ds
function is only needed if your module creates datasets and provides them for other modules to use (like the DW
module exports data views to be used by other modules). The modules are designed to create elements. For example the DW
module creates data view elements, the FG
module is used to create figure elements, etc.
MM_Server
Shiny server function.MM_init_state
Creates an empty the formods state for the module.MM_fetch_state
Each module has a function to fetch the state.
Within this function there should be no interactivity. Any access of
the elements to the Shiny input should be isolated. Based on
differences between the input elements (current state of the app)
and the stored app state can be used to trigger different things.MM_fetch_code
This takes as it's first argument the module state.
When called with only this argument it should return a character
object containing all of the code needed to generate the elements of
this module represented in the app. You can assume that any modules
this one depends on will be defined previously. For example the FG
module will return only that code associated with generating
figures. It will be appended to the code from the UD
and DW
modules that define loading the dataset and creation of the data
views. For modules where no code is generated (e.g. ASM
) just
return NULL
.MM_append_report
If a module generates reportable outputs, this function will be used to append those outputs to the overall reports generated by formods.MM_fetch_ds
If a module provides data sets to be used in other modules
you need to create this function. For an example see the
DW_fetch_ds()
in the DW
module
This function should return at least the following:hasds
Boolean variable indicating if the modules currently has
any exportable datasets.isgood
General return status of the funciton. Set to FALSE if
any errors were encoutered.msgs
A character vector of any messages to pass back to the
user.ds
A module can provide multiple datasets. This is a list with
the following elements for each dataset:label
Text label for the datasetMOD_TYPE
Short name for the type of module.id
module ID.DS
Dataframe containing the actual dataset.DSMETA
Metadata describing DS.code
Complete code to build dataset.checksum
Module checksum.DSchecksum
Dataset checksum.MM_fetch_mdl
If a module provides ODE models to be used in other
modules you need to create this function. To see an example of this check out the
MB_fetch_mdl()
in the MB
module
This function should return at least the following:hasmdl
Boolean variable indicating if the modules currently has
any exportable models.isgood
General return status of the funciton. Set to FALSE if
any errors were encountered. msgs
A character vector of any messages to pass back to the
user.ts_details
List of timescale detailsmdl
A module can provide multiple models. This is a list with
the following elements for each model:label
Text label for the model. MOD_TYPE
Short name for the type of module.id
module ID.rx_obj
The rxode2 object that holds the model. rx_obj_name
The rxode2 object name in generated code.ts_obj
The timescale object that holds timescales list. rx_obj_name
The timescale object name in generated code.fcn_def
Text to define the modelMDLMETA
Verbose metadata describing model.code
Complete code to build the model.checksum
Checksum of the module the model came from.MDLchecksum
Checksum of the model.MM_preload
The ASM module provides the ability to load a previous analysis from yaml files. This function is called with the contents of those yaml files to process and load them. MM_mk_preload
This will create a preload list of the current state of the module.MM_test_mksession
When testing outside of Shiny it is useful to
have a pre-populated session, input, etc. objects with actual data.
Each module should have a test_mksession
function that populates
these objects with useful data. If your module depends on a
different module. The function should return the following:isgood
Boolean indicating the exit status of the function.msgs
Any messages generated when creating the test environment.session
The value Shiny session variable (in app) or a list
(outside of app) after initialization.input
The value of the shiny input at the end of the session
initialization.state
App state.react_tate`` The
react_state` components.MM_new_element
Creates a new module element.MM_fetch_current_element
Extracts the current element from the state object.MM_set_current_element
Sets the current element to the provided value.MM_del_element
Deletes the current active element.ui_mm_compact
This is a UI output that contains a compact view of your module that can be called from the main ui functions for the App. It is composed of the individual UI elements that are shown in the MM_module_components.R
file. This allows the user a quick way to utilize a model (using the ui_mm_compact
), and the ability to customize the module UI by manually arranging the pieces found in MM_module_components.R
. Say you are using the UD module to feed data into the DW module and the user goes back to the upload form and uploads a different data set. This will need to trigger a reset of the Data Wrangling module as well as tell your larger app that something has changed.
Changes in module states are detected with the react_state
object. For
a given module of type "MM"
with a module id of "ID"
you would
detect changes by reacting to react_state[["ID"]]
and looking for
changes in the checksum element below:
react_state[["ID"]][["MM"]][["checksum"]]
checksum
A checksum that can be used to detect changes in this
module. For example in the UD module this will change if the
uploaded file or the sheet selected from a currently uploaded file
changes.FM_le()
- Creates log entries (le
) that are displayed in the
console and recorded in the log file for the current session.FM_tc()
- This can be used to evaluate code, trap errors, and
process results.has_changed()
- Depreciated see has_updated()
has_updated()
- Used to compare objects to see if they have changed. icon_link()
- Used to create the ui for a link using an icon.is_installed()
- Used to test if a package is available.is_shiny()
- Used to test if a session object is simply a list or a legitimate Shiny session object. set_hold()
- Used to set a hold on one or more UI elements. This prevents internal updating of that UI element based on the current value in the App. fetch_hold()
- This will retrieve the hold status of a UI element.remove_hold()
- This will remove any holds set on a UI element.FM_build_comment()
- This creates comments from strings so they will form sections when viewed in RStudio.FM_add_ui_tooltip()
- Attaches a tooltip to a UI element.FM_init_state()
- Called at the top of your module state initialization function to create a skeleton of a module state that you can then build upon. FM_set_notification()
- Within you code you can create
notifications and attach them to a module state.FM_notify()
- Used in observeEvent()
to show notifications that
have not yet been displayed.FM_set_mod_state()
- Used to save any changes to the module state.FM_fetch_mod_state()
- Used to get the module state.FM_set_ui_msg()
- Attach verbose messages or errors that need to be pushed back to the user in the app.FM_pretty_sort()
- Used as a general sorting function that will try to make the sorted results prettier. FM_pause_screen()
- Pauses the screen when doing something on the server side that takes a while.FM_resume_screen()
- Resumes activity (unpauses the screen) when you're done with the pause.FM_fetch_data_format()
- Creates formatting information for display for a given data frame. The examples below require a Shiny session variable and a formods state object. Here we create some examples and other objects needed to demonstrate the functions below.
library(formods) # This creates the state and session objects sess_res = UD_test_mksession(session=list()) state = sess_res$state session = sess_res$session # Here we load an example dataset into the df object. data_file_local = system.file(package="formods", "test_data", "TEST_DATA.xlsx") sheet = "DATA" df = readxl::read_excel(path=data_file_local, sheet=sheet)
The mechanics of the fetch state functions mean that each time a fetch state is called, all of the UI elements in the App are pulled and placed in the app state. This generally works well with some exceptions. The main exception is when you want to have a UI element that changes another UI element. Say for example you have a selection box with a UI id of my_selection
. You want that selection to alter a text input with an id of my_text
. However if you just poll the ui elements you may update my_text
based on changes to my_selection
then have those overwritten by the current value of my_text
. To prevent this, you need to do two things:
my_selection
you need to set a hold on my_text
(done with set_hold()
).my_text
you need to do that only if there is no hold set. This is checked with fetch_hold()
Lastly you need to remove the hold. This is done after the UI has refreshed with the new text value populated in to my_text
(with the appropriate reactions set). This is done with an observeEvent that is triggered after everything else (with a priority of -100 below):
remove_hold_listen <- reactive({ list(input$my_selection) }) observeEvent(remove_hold_listen(), { # Once the UI has been regenerated we # remove any holds for this module state = MM_fetch_state(id = id, input = input, session = session, FM_yaml_file = FM_yaml_file, MOD_yaml_file = MOD_yaml_file, react_state = react_state) FM_le(state, "removing holds") # Removing all holds for(hname in names(state[["MM"]][["ui_hold"]])){ remove_hold(state, session, hname) } }, priority = -100)
The remove_hold_listen
object should contain all of the inputs that create holds.
If you want to tables and pulldown menues based on the types of data in each column you can use the FM_fetch_data_format()
function.
hfmt = FM_fetch_data_format(df, state) # Descriptive headers head(as.vector(unlist( hfmt[["col_heads"]]))) # Subtext head(as.vector(unlist( hfmt[["col_subtext"]])))
The custom headers can be used with the {rhandsontable}
package.
hot = rhandsontable::rhandsontable( head(df), width = "100%", height = "100%", colHeaders = as.vector(unlist(hfmt[["col_heads"]])), rowHeaders = NULL )
hot
To add subtext to a selection widget in Shiny you need to use the {shinyWidgets}
package.
sel_subtext = as.vector(unlist( hfmt[["col_subtext"]])) library(shinyWidgets) shinyWidgets::pickerInput( inputId = "select_example", choices = names(df), label = "Select with subtext", choicesOpt = list(subtext = sel_subtext))
To alter the formats shown here you need to edit the formods.yaml
configuration file and look at the FM
$\rightarrow$data_meta
section.
Notifications are created using the {shinybusy}
package and are
produced with two different functions: FM_set_notification()
and
FM_notify()
. This is done in a centralized fashion where notifications
are added to the state object as user information is processed. This
will set a notification called Example Notification
. Along with that a
timestamp is set:
state = FM_set_notification(state, "Something happened", "Example Notification")
That timestamp is used to track and prevent the notification from being shown multiple times. Next you need to setup the reactions to display the notifications. Here you can create a reactive expression of the inputs that will lead to a notification:
toNotify <- reactive({ list(input$input1, input$input2) })
Next you use observeEvent()
with that reactive expression to trigger
notifications. You need to use the fetch state function for that
module to get the state object with the notifications. Then
FM_notify()
will be called an any unprocessed notifications will be
displayed:
observeEvent(toNotify(), { state = MM_fetch_state(id = id, input = input, session = session, FM_yaml_file = FM_yaml_file, MOD_yaml_file = MOD_yaml_file, react_state = react_state) # Triggering optional notifications notify_res = FM_notify(state = state, session = session) })
Tooltips are created internally using the suggested {prompter}
package.
To add a tool tip to a ui element you would use the
FM_add_ui_tooltip()
function. For example to add the tool tip,
You need to type harder!
to a text input you would do the following:
uiele = shiny::textInput( inputId = "some_text", label = "You need to type harder!") uiele = FM_add_ui_tooltip(state, uiele, tooltip = "This is a tooltip", position = "left")
To pause the screen the {shinybusy}
package is also used. This is
controlled with two functions: FM_pause_screen()
is used to pause the
screen and/or update the pause message, and FM_resume_screen()
is used
end the pause and resume interaction with the user.
FM_pause_screen(state, session) FM_resume_screen(state, session)
When you create a formods state object it can have the following fields:
yaml
- Contents of the formods configuration file.MC
- Contents of the module configuration file.MM
- MM here is the short name of the current module.
MOD_TYPE
below), this is where you would store any app information.
(see below).MOD_TYPE
- Short name of the module.id
- ID of the module.FM_yaml_file
- formods configuration file.MOD_yaml_file
- Module configuration file.notifications
- Contains notifications set by the user through FM_set_notification()
.This field state$MM
is relatively free form but there are some
reserved elements. These reserved keyword are:
button_counters
- Counter that tracks button clicksui_hold
- List of hold elements that is populated with set_hold()
isgood
- Boolean variable indicating the state of the module. ui_msg
- Messaages returned to the UI with captured errors populated with FM_set_ui_msg()
Other than those fields you can store whatever else you need for your module.
The following is a suggested checkist to go over when making a module:
MM_mk_preload
MM_fetch_code
```{css, echo=FALSE} .scroll-100 { max-height: 100px; overflow-y: auto; background-color: inherit; }
# YAML configuration files {.tabset} ```r yaml= file.path(system.file(package="formods"), "templates", "formods.yaml") cat(readLines(file.path(system.file(package="formods"), "templates", "formods.yaml")), sep="\n") #library(shiny) #library(shinyAce) # renderUI({ # aceEditor("formods", value=readLines(file.path(system.file(package="formods"), "templates", "formods.yaml"))) # }) # yamls = list( # formods.yaml= file.path(system.file(package="formods"), "templates", "formods.yaml"), # ASM.yaml = file.path(system.file(package="formods"), "templates", "ASM.yaml"), # DS.yaml = file.path(system.file(package="formods"), "templates", "DW.yaml") # # ) # # for(yaml in names(yamls)){ # # cat("``` yaml") # cat(paste(readLines(file.path(system.file(package="formods"), "templates", "formods.yaml"))), collapse="\n") # }
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.