testing shiny modules with additional variables - r

I've been playing with shiny modules and finally got some bits and pieces working. However, I've been totally thrown by an issue testing individual modules.
What I usually do is turn each module into a small app to test how it works. This particular module takes additional variables, but I don't seem to be able to insert some test vars into the test app as I would usually. Unfortunately, this fails.
Is there a standard way of dealing with this?
Many thanks
histogram_ui <- function(id) {
tagList(
plotOutput(NS(id, "hist"))
)
}
histogram_server <- function(id, var, bin) {
moduleServer(id, function(input, output, session) {
data <- reactive(mtcars[[var()]])
#debug
observeEvent(var(), {
print(var())
})
output$hist <- renderPlot({
hist(data(), breaks = bin(), main = var())
})
})
}
#testing----
ui_t <- fluidPage(
histogram_ui("test")
)
server_t <- function(input, output, session) {
histogram_server("test", var = "mpg", bin = 10)
}
options(shiny.reactlog=TRUE) #ctrl+F3 to bring up
shinyApp(ui_t, server_t)

Try this
histogram_ui <- function(id) {
tagList(
plotOutput(NS(id, "hist"))
)
}
histogram_server <- function(id, var, bin) {
moduleServer(id, function(input, output, session) {
observeEvent(c(var(), bin()), {
print(var())
})
output$hist <- renderPlot({
hist(mtcars[[var()]], breaks = bin(), main = var())
})
})
}
#testing----
ui_t <- fluidPage(
selectInput("myvar","Choose",choices = colnames(mtcars)),
sliderInput("bins","Number of Bins", min=1, max=10, value=5),
histogram_ui("test")
)
server_t <- function(input, output, session) {
histogram_server("test", var = reactive(input$myvar), bin = reactive(input$bins))
}
options(shiny.reactlog=TRUE) #ctrl+F3 to bring up
shinyApp(ui_t, server_t)

Related

How to Instantiate a ShinyReForms in a Shiny Module

Its quite quick and easy to instantiate a ShinyReForms for a Shiny app by following the example at https://piotrbajger.github.io/shinyreforms/articles/tutorial.html. I cannot though see how to get it working in a Shiny module.
The app below is a smaller version of the example app, presented in a module, and with an extra output which shows the result of the namespaced checkbox.
The ‘submit’ button doesn’t return the expected output though. I’m opining that this is a name space issue, though I can’t see where to wrap an id with something like... ns(“myformid”).
Any suggestions please. Thanks
library(shiny)
library(shinyreforms)
modUI <- function(id) {
ns <- NS(id)
tagList(
uiOutput(ns('form_ui')),
)
}
modServer <- function(id) {
moduleServer(
id,
function(input, output, session) {
ns <- session$ns
myForm <- shinyreforms::ShinyForm$new(
"myForm",
submit = "Submit",
onSuccess = function(self, input, output) {
yourName <- self$getValue(input, "name_input")
output$result <- shiny::renderText({
paste0("Your name is ", yourName, "!")
})
},
onError = function(self, input, output) {
output$result <- shiny::renderText({
"Form is invalid!"
})
},
shinyreforms::validatedInput(
shiny::checkboxInput(ns("checkbox"), label = "I accept!"),
validators = c(
shinyreforms::ValidatorRequired()
)
)
)
myForm$server(input, output)
output$ot_checkox <- renderUI({
h4(input$checkbox, style = 'color: blue;')
})
output$form_ui <- renderUI({
tagList(
shiny::tags$h1("Example ShinyForm!"),
myForm$ui(), # <- ShinyForm will be included here!
uiOutput(ns('ot_checkox')),
shiny::tags$h4("Result:"),
shiny::textOutput(ns("result"))
)
})
}
)
}
ui <- shiny::bootstrapPage(
shinyreforms::shinyReformsPage(
shiny::fluidPage(
modUI('mod_id')
)
)
)
server <- function(input, output, session) {
modServer('mod_id')
}
shinyApp(ui, server)

How to return a reactive dataframe from within a shiny module that depends on a button click?

Aim: Return a reactive dataframe object from within the module named "modApplyAssumpServer"
Problem: I am getting an endless loop. Even if I wrap everything within the observeevent logic within isolate()
I have included another table in the app code below to indicate a simplified version of the logic that works outside of the module framework but that I can't seem to get to work within the module.
library(shiny)
library(dplyr)
df_agg_orig <- data.frame(proj_1 = c(2,3))
modGrowthInput <- function(id) {
ns <- NS(id)
tagList(
numericInput(ns("first"),label = "Assumption",value = 100),
)
}
modGrowthServer <- function(id, btnGrowth) {
moduleServer(id, function(input, output, session) {
list(
first = reactive({input$first})
)
})
}
modButtonUI <- function(id,lbl = "Recalculate"){
ns <- NS(id)
actionButton(inputId = ns("btn"),label = lbl)#,style = "pill",color = "primary",no_outline = T,size = "xs"
}
modButtonServer <- function(id){
moduleServer(id, function(input, output, session) {
reactive({input$btn})
})
}
modApplyAssumpServer <- function(id,btnGrowth, df_agg,case_vals){
moduleServer(id, function(input, output, session) {
stopifnot(is.reactive(btnGrowth))
stopifnot(is.reactive(df_agg))
mod_vals <- reactiveVal(df_agg())
observeEvent(btnGrowth(),{
isolate({mod_vals(df_agg() %>% mutate(proj_1 = proj_1*input$first))})
print("Looping problem...")
})
mod_vals()
})
}
#### Test App
GrowthInputApp <- function() {
ui <- fluidPage(
sidebarPanel(modGrowthInput("tst"),modButtonUI("tstGrowth")),
mainPanel(fluidRow( splitLayout( DT::DTOutput("no_module"),DT::DTOutput("module_tbl")))))
server <- function(input, output, session) {
btnGrowth <- modButtonServer("tstGrowth")
case_vals <- modGrowthServer("tst")
df_agg <- reactiveValues(df_wide = df_agg_orig)
#Outside of module test exhibiting expected/desired behavior (at least if the looping issue would let it do so :)
observeEvent(btnGrowth(),{
df_agg$df_wide$proj_1 <- round(df_agg$df_wide*case_vals$first(),2)
})
output$no_module <- DT::renderDT({DT::datatable(rownames = F,df_agg$df_wide,caption = "Not Updated Within Module")})
output$module_tbl <- DT::renderDT({DT::datatable(rownames = F,modApplyAssumpServer("tst",btnGrowth = btnGrowth,df_agg = reactive({df_agg_orig})),caption = "Table Returned From Module")}
)
}
shinyApp(ui, server)
}
runApp(GrowthInputApp())
Try this
library(shiny)
library(dplyr)
df_agg_orig <- data.frame(proj_1 = c(2,3))
modGrowthInput <- function(id) {
ns <- NS(id)
tagList(
numericInput(ns("first"),label = "Assumption",value = 10),
)
}
modGrowthServer <- function(id) {
moduleServer(id, function(input, output, session) {
list(
first = reactive({input$first})
)
})
}
modButtonUI <- function(id,lbl = "Recalculate"){
ns <- NS(id)
actionButton(inputId = ns("btn"),label = lbl)#,style = "pill",color = "primary",no_outline = T,size = "xs"
}
modButtonServer <- function(id){
moduleServer(id, function(input, output, session) {
reactive({input$btn})
})
}
modApplyAssumpServer <- function(id,btnGrowth, df_agg, val){
moduleServer(id, function(input, output, session) {
stopifnot(is.reactive(btnGrowth))
stopifnot(is.reactive(df_agg))
modvals <- eventReactive(btnGrowth(), {
print("Looping problem...")
#print(btnGrowth())
df_agg() %>% mutate(proj_1 = proj_1*val )
})
return(modvals())
})
}
#### Test App
GrowthInputApp <- function() {
ui <- fluidPage(
sidebarPanel(modGrowthInput("tst"),modButtonUI("tstGrowth")),
mainPanel(fluidRow( splitLayout( DT::DTOutput("no_module"),DT::DTOutput("module_tbl")))))
server <- function(input, output, session) {
btnGrowth <- modButtonServer("tstGrowth")
case_vals <- modGrowthServer("tst")
observe({ print(case_vals$first())})
df_agg <- reactiveValues(df_wide = df_agg_orig)
#Outside of module test exhibiting expected/desired behavior (at least if the looping issue would let it do so :)
observeEvent(btnGrowth(),{
df_agg$df_wide$proj_1 <- round(df_agg$df_wide*case_vals$first(),2)
})
mydf <- eventReactive(c(btnGrowth(),case_vals$first()), {
modApplyAssumpServer("tst", btnGrowth, reactive({df_agg$df_wide}), case_vals$first() )
})
#observe({print(btnGrowth())})
output$no_module <- renderDT({DT::datatable(rownames = F,df_agg$df_wide,caption = "Not Updated Within Module")})
output$module_tbl <- renderDT({DT::datatable(rownames = F, mydf() ,caption = "Table Returned From Module")} )
### using original data so no change after first click
#output$module_tbl <- renderDT({DT::datatable(rownames = F, modApplyAssumpServer("tst", btnGrowth, reactive({df_agg_orig}), case_vals$first() ),caption = "Table Returned From Module")}
#)
}
shinyApp(ui, server)
}
runApp(GrowthInputApp())

Testing if the module has been called in shiny

I write some shiny tests recently using testServer which is great in most of the scenarios. I'm thinking about following problem which I can't solve now.
Let's imagine simple modules (ui/server)
child_ui <- function(id) {
ns <- NS(id)
div(
selectInput(inputId = ns("select"), label = "select", choices = c("a", "b"), selected = "a"),
verbatimTextOutput(outputId = ns("out"))
)
}
child_srv <- function(id) {
moduleServer(id, function(input, output, session) {
output$out <- renderPrint(input$select)
})
}
... which are called inside of the other module but through the specific function call_child.
parent_ui <- function(id, module) {
ns <- NS(id)
div(
module(ns("parent"))
)
}
parent_srv <- function(id, module) {
moduleServer(id, function(input, output, session) {
call_child <- function(module) {
module(id = "parent")
NULL
}
call_child(module)
})
}
What I would like to do is to check whether child_srv has been called and I wonder if there is some way to deduct this. Preferred solution is to keep call_child untouched (maybe some evidences are in session object).
The only thing I can do is following:
testServer(
app = parent_srv,
args = list(module = child_srv),
expr = {
tinytest::expect_null(call_child(module = module))
})
# example app
shinyApp(
ui = parent_ui(id = "root", module = child_ui),
server = function(input, output, session)
parent_srv(id = "root", module = child_srv)
)
Does anyone has any advice how to properly test this scenario?

How to update shiny module with reactive dataframe from another module

The goal of this module is create a reactive barplot that changes based on the output of a data selector module. Unfortunately the barplot does not update. It's stuck at the first variable that's selected.
I've tried creating observer functions to update the barplot, to no avail. I've also tried nesting the selector server module within the barplot module, but I get the error: Warning: Error in UseMethod: no applicable method for 'mutate' applied to an object of class "c('reactiveExpr', 'reactive', 'function')"
I just need some way to tell the barplot module to update whenever the data it's fed changes.
Barplot Module:
#UI
barplotUI <- function(id) {
tagList(plotlyOutput(NS(id, "barplot"), height = "300px"))
}
#Server
#' #param data Reactive element from another module: reactive(dplyr::filter(austin_map, var == input$var))
barplotServer <- function(id, data) {
moduleServer(id, function(input, output, session) {
#Data Manipulation
bardata <- reactive({
bar <-
data |>
mutate(
`> 50% People of Color` = if_else(`% people of color` >= 0.5, 1, 0),
`> 50% Low Income` = if_else(`% low-income` >= 0.5, 1, 0)
)
total_av <- mean(bar$value)
poc <- bar |> filter(`> 50% People of Color` == 1)
poc_av <- mean(poc$value)
lowincome <- bar |> filter(`> 50% Low Income` == 1)
lowincome_av <- mean(lowincome$value)
bar_to_plotly <-
data.frame(
y = c(total_av, poc_av, lowincome_av),
x = c("Austin Average",
"> 50% People of Color",
"> 50% Low Income")
)
return(bar_to_plotly)
})
#Plotly Barplot
output$barplot <- renderPlotly({
plot_ly(
x = bardata()$x,
y = bardata()$y,
color = I("#00a65a"),
type = 'bar'
) |>
config(displayModeBar = FALSE)
})
})
}
EDIT :
Data Selector Module
dataInput <- function(id) {
tagList(
pickerInput(
NS(id, "var"),
label = NULL,
width = '100%',
inline = FALSE,
options = list(`actions-box` = TRUE,
size = 10),
choices =list(
"O3",
"Ozone - CAPCOG",
"Percentile for Ozone level in air",
"PM2.5",
"PM2.5 - CAPCOG",
"Percentile for PM2.5 level in air")
)
)
}
dataServer <- function(id) {
moduleServer(id, function(input, output, session) {
austin_map <- readRDS("./data/austin_composite.rds")
austin_map <- as.data.frame(austin_map)
austin_map$value <- as.numeric(austin_map$value)
list(
var = reactive(input$var),
df = reactive(austin_map |> dplyr::filter(var == input$var))
)
})
}
Simplified App
library(shiny)
library(tidyverse)
library(plotly)
source("barplot.r")
source("datamod.r")
ui = fluidPage(
fluidRow(
dataInput("data"),
barplotUI("barplot")
)
)
server <- function(input, output, session) {
data <- dataServer("data")
variable <- data$df
barplotServer("barplot", data = variable())
}
shinyApp(ui, server)
As I wrote in my comment, passing a reactive dataset as an argument to a module server is no different to passing an argument of any other type.
Here's a MWE that illustrates the concept, passing either mtcars or a data frame of random values between a selection module and a display module.
The critical point is that the selection module returns the reactive [data], not the reactive's value [data()] to the main server function and, in turn, the reactive, not the reactive's value is passed as a parameter to the plot module.
library(shiny)
library(ggplot2)
# Select module
selectUI <- function(id) {
ns <- NS(id)
selectInput(ns("select"), "Select a dataset", c("mtcars", "random"))
}
selectServer <- function(id) {
moduleServer(
id,
function(input, output, session) {
data <- reactive({
if (input$select == "mtcars") {
mtcars
} else {
tibble(x=runif(10), y=rnorm(10), z=rbinom(n=10, size=20, prob=0.3))
}
})
return(data)
}
)
}
# Barplot module
barplotUI <- function(id) {
ns <- NS(id)
tagList(
selectInput(ns("variable"), "Select variable:", choices=c()),
plotOutput(ns("plot"))
)
}
barplotServer <- function(id, plotData) {
moduleServer(
id,
function(input, output, session) {
ns <- NS(id)
observeEvent(plotData(), {
updateSelectInput(
session,
"variable",
choices=names(plotData()),
selected=names(plotData()[1])
)
})
output$plot <- renderPlot({
# There's an irritating transient error as the dataset
# changes, but handling it would
# detract from the purpose of this answer
plotData() %>%
ggplot() + geom_bar(aes_string(x=input$variable))
})
}
)
}
# Main UI
ui <- fluidPage(
selectUI("select"),
barplotUI("plot")
)
# Main server
server <- function(input, output, session) {
selectedData <- selectServer("select")
barplotServer <- barplotServer("plot", plotData=selectedData)
}
# Run the application
shinyApp(ui = ui, server = server)

Shiny namespace issue with nested callModules

I’m looking for some help with a simple Shiny app with a modularised design please. I think the problem is a name space issue so the example below is set out as a simplified version of my actual project. My feeling is that I have not set output$uis to the correct name space but I am lost on how to map to it.
The app generates 3 instances of select_formUI and should be namespace related to 3 instances of the server returned values from the callModules of select_form. The values from select_form are passed out in a tibble. The inner module binds all 3 tibbles into one reactive tibble all_gen_forms_rctv.
The process works fine until I uncomment the input_slt_type_db column in pass_back_test, which returns the input$slt_type_db. I’m looking for some help please to include this column in the output and see all_gen_forms_rctv change on user selections via output$outpt_test.
library(shiny)
library(shinyjs)
library(glue)
library(tibble)
select_formUI <- function(id){
ns <- NS(id)
tagList(selectInput(ns('slt_type_db'), 'select letter', choices = letters[1:5]))
}
select_form <- function(input, output, session){
#pass_back_test <- reactive({
tibble(
placehold = "FILL VALUE"
# , input_slt_type_db = input$slt_type_db
)
})
return(list(pass_back_test = reactive({pass_back_test()})))
}
inner_moduleUI <- function(id){
ns <- NS(id)
tagList(uiOutput(ns("outpt_forms_ui")))
}
inner_module <- function(input, output, session){
rctval_ui <- reactiveValues(all_ui=NULL)
gen_forms <- reactiveValues()
all_gen_forms_rctv <- reactive({
dplyr::bind_rows(lapply(reactiveValuesToList(gen_forms), function(current_module_output) {
current_module_output$pass_back_test()
}))
})
observeEvent(input$btn_start ,{
for(i in 1:3){
x_id = glue("mod_{i}")
rctval_ui$all_ui[[x_id]] <- select_formUI(x_id)
gen_forms[[x_id]] <- callModule(select_form, x_id)
}
})
output$outpt_forms_ui <- renderUI({
ns <- session$ns
tagList(
actionButton(ns('btn_start'), label = 'start'),
verbatimTextOutput(ns('outpt_test')),
hr(),
uiOutput(ns('uis'))
)
})
output$uis <- renderUI({
ns <- session$ns
tags$div(id = environment(ns)[['namespace']],
tagList(rctval_ui$all_ui))
})
output$outpt_test <- renderPrint({all_gen_forms_rctv()})
}
ui <- fluidPage(
useShinyjs(),
uiOutput('main_ui')
)
server <- function(input, output, session) {
output$main_ui <- renderUI({inner_moduleUI('inner_ns')})
callModule(inner_module, 'inner_ns')
}
shinyApp(ui = ui, server = server)
the problem is that the UI function of the select_form modul doesn't know that it is being called within another module. So you need to tell it by wrapping the the id in session$ns. The callModule function can handle this by itself so here there is no need to change anything. The inner_module function would the look like this
inner_module <- function(input, output, session) {
rctval_ui <- reactiveValues(all_ui=NULL)
gen_forms <- reactiveValues()
all_gen_forms_rctv <- reactive({
browser()
dplyr::bind_rows(lapply(reactiveValuesToList(gen_forms), function(current_module_output) {
current_module_output$pass_back_test()
}))
})
observeEvent(input$btn_start ,{
for(i in 1:3){
x_id = glue("mod_{i}")
rctval_ui$all_ui[[x_id]] <- select_formUI(session$ns(x_id))
gen_forms[[x_id]] <- callModule(select_form, x_id)
}
})
output$outpt_forms_ui <- renderUI({
ns <- session$ns
tagList(
actionButton(ns('btn_start'), label = 'start'),
verbatimTextOutput(ns('outpt_test')),
hr(),
uiOutput(ns('uis'))
)
})
output$uis <- renderUI({
ns <- session$ns
tags$div(id = environment(ns)[['namespace']],
tagList(rctval_ui$all_ui))
})
output$outpt_test <- renderPrint({all_gen_forms_rctv()})
}

Resources