shiny module inside module loosing reactive value - r

I am trying to reuse the Filter module code provided in the Mastering Shiny book. It takes a dataframe, generate a "select" widget for each column and return a reactive that output a boolean vector. This vector can be used to filter the dataframe row-wise according to the selected data range from the widgets.
This works as intended when I simply reuse the filter module directly in a ShinyApp.
But when I try to use it from inside another module, the returned reactive outputs logical(0) where it should output a vector with length equal to row number of the input dataframe.
Here is a minimal working example based on the code from the book.
library(shiny)
library(purrr)
# Filter module from https://mastering-shiny.org/scaling-modules.html#dynamic-ui
#helper functions
make_ui <- function(x, id, var) {
if (is.numeric(x)) {
rng <- range(x, na.rm = TRUE)
sliderInput(id, var, min = rng[1], max = rng[2], value = rng)
} else if (is.factor(x)) {
levs <- levels(x)
selectInput(id, var, choices = levs, selected = levs, multiple = TRUE)
} else {
# Not supported
NULL
}
}
filter_var <- function(x, val) {
if (is.numeric(x)) {
!is.na(x) & x >= val[1] & x <= val[2]
} else if (is.factor(x)) {
x %in% val
} else {
# No control, so don't filter
TRUE
}
}
#Module
filterUI <- function(id) {
uiOutput(NS(id, "controls"))
}
filterServer <- function(id, df) {
moduleServer(id, function(input, output, session) {
vars <- reactive(names(df))
output$controls <- renderUI({
map(vars(), function(var) make_ui(df[[var]], NS(id, var), var))
})
reactive({
each_var <- map(vars(), function(var) filter_var(df[[var]], input[[var]]))
reduce(each_var, `&`)
})
})
}
#App
filterApp <- function() {
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
textOutput("n"),
filterUI("filter"),
),
mainPanel(
verbatimTextOutput("debug"),
tableOutput("table")
)
)
)
server <- function(input, output, session) {
df <- datasets::ToothGrowth[seq(1,60,5),] #subset rows from ToothGrowth
filter <- filterServer("filter", df)
output$table <- renderTable(df[filter(), , drop = FALSE])
output$n <- renderText(paste0(sum(filter()), " rows"))
output$debug <- renderPrint(filter())
}
shinyApp(ui, server)
}
filterApp() #This works !
Then a simple Module to test the filter from inside it :
filterPageUI <- function(id) {
tagList(
filterUI(NS(id, "filter"))
)
}
filterPageServer <- function(id, df) {
moduleServer(id, function(input, output, session) {
filterServer("filter", df = df)
})
}
And the ShinyApp modification to use this new module :
filterPageApp <- function() {
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
textOutput("n"),
filterPageUI("filterpage"),
),
mainPanel(
verbatimTextOutput("debug"),
tableOutput("table")
)
)
)
server <- function(input, output, session) {
#subset rows from ToothGrowth
df <- datasets::ToothGrowth[seq(1,60,5),]
filter <- filterPageServer("filterpage", df)
output$table <- renderTable(df[filter(), , drop = FALSE])
output$n <- renderText(paste0(sum(filter()), " rows"))
output$debug <- renderPrint(filter())
}
shinyApp(ui, server)
}
filterPageApp() #This does not work!
I suspect that the problem comes from namespacing, maybe inside the map/reduce logic. But I cant wrap my head around it.
Moreover, the last paragraph from the chapter says that this module should be usable elsewhere without modifications.
A big advantage of using a module here is that it wraps up a bunch of advanced Shiny programming techniques. You can use the filter module without having to understand the dynamic UI and functional programming techniques that make it work.
Any advice would be greatly appreciated. Thanks in advance !

In your filterServer function you have to use session$ns("var") instead of NS(id, "var"). The former will include enclosing namespace whereas the later will only include current namespace. I added two messages that will show in the console what I mean.
filterServer <- function(id, df) {
moduleServer(id, function(input, output, session) {
message("session namespace: ", session$ns("test"))
message("raw namespace: ", NS(id, "test"))
vars <- reactive(names(df))
output$controls <- renderUI({
map(vars(), function(var) make_ui(df[[var]], session$ns(var), var))
})
reactive({
each_var <- map(vars(), function(var) filter_var(df[[var]], input[[var]]))
reduce(each_var, `&`)
})
})
}

Related

Demo use of future_promise and zeallot not working

I am experimenting with Shiny and async programming in the hopes of using it in a larger, more complex, public application.
In this example, I thought that unlist() would wait until the value of v1 is returned because of the use of future_promise and the promise pipe, %...>%.
However, I get the error:
Error in unlist: object 'v1' not found
How can I get this demo code to work?
app.R
library("zeallot")
library("shiny")
library("primes")
library("future")
library("promises")
library("tidyverse")
plan(multisession)
source("/home/law/whatbank_multicore_test/src/expensive_calc.R")
ui <- fluidPage(
actionButton("do", "Do Expensive Calc"),
textOutput("text")
)
server <- function(input, output, session) {
observeEvent(input$do, {
output$text <- renderText({
future_promise(zeallot::`%<-%`(c(v1, v2), expensive_calc()), seed = TRUE) %...>%
{
tmp <- unlist(v1)
}
})
})
}
shinyApp(ui, server)
expensive_calc.R
expensive_calc <- function(){
min <- 10000
max_num <- sample(80000:210000, 1)
rap <- ruth_aaron_pairs(min, max_num, distinct = FALSE)
list(rap, 11)
}
As pointed out to me in the RStudio Community the zeallot call needs to be within the future_promise braces. Here is the solution:
server <- function(input, output, session) {
observeEvent(input$do, {
output$text <- renderText({
future_promise(seed = TRUE) %...>%
{
zeallot::`%<-%`(c(v1, v2), expensive_calc())
tmp <- unlist(v1)
}
})
})
}

How to use reactive inside custom function?

I try to use reactive expression inside function I wrote. What I need to do is to pass some data to function (as parameter) and then return value from reactive expression. Unfortunately when I run this code then I got an error: 'data' must be a data frame or matrix. Btw 'data' I'm passing to function is already a data frame. Is it something wrong with my code? I mean using reactive in function?
library(shiny)
library(dplyr)
moduleServer <- function(id, module) {
callModule(module, id)
}
mod_btn_ui <- function(id) {
ns <- NS(id)
tagList(
verbatimTextOutput(ns("x"))
)
}
mod_btn_server <- function(id){
moduleServer(id, function(input, output, session) {
ns <- NS(id)
output$x <- renderPrint({
print(reactiveDf2(quakes))
})
reactiveDf2 <- function(data){
x <- 1
r <- reactive({
if (x == 2) {
newDf <- data
} else {
newDf <- data[1:10,]
}
})
return(r)
}
})
}
# Final app #
ui <- fluidPage(
tagList(
mod_btn_ui("test"))
)
server <- function(input, output, session) {
mod_btn_server("test")
}
shinyApp(ui = ui, server = server)
When I run the code there is no data in text area displayed.

How to select a subset of elements of the input R6 class within a shiny module to perform operations on them

Can I access a list of all input widgets within a module (let us name it myModule) and check if their state is isTruthy().
I found a way that works when I know (or can deduce) the exact names of the widgets (see 1).
All_Inputs <- vapply(paste0('axis',1:3),
function(x) { isTruthy(input[[x]]) },
logical(1))
Of course, I also could do it with a long list of if (isTruthy(input$a) && isTruthy(input$b) && .... But both solutions are not satsifactory to me, mostly because of drawbacks regarding readability and maintainability.
I know that the input class has them all listed under names that start with myModule-[AnyName]. But I do not know how to use that information to access them using a loop or even better an apply function.
As input is a named list, you could use vapply on names(input):
library(shiny)
counterButton <- function(id, label = "Counter") {
ns <- NS(id)
tagList(
actionButton(ns("button"), label = label),
verbatimTextOutput(ns("out"))
)
}
counterServer <- function(id) {
moduleServer(
id,
function(input, output, session) {
count <- reactiveVal(0)
observeEvent(input$button, {
count(count() + 1)
})
output$out <- renderText({
count()
})
count
}
)
}
ui <- fluidPage(
counterButton("counter1", "Counter #1"),
counterButton("counter2", "Counter #2"),
textOutput('istruthy')
)
server <- function(input, output, session) {
counterServer("counter1")
counterServer("counter2")
output$istruthy <- renderText({
vapply(names(input),
function(x) {
ifelse(startsWith(x, "counter2-"), isTruthy(input[[x]]), TRUE)
},
logical(1))
})
}
shinyApp(ui, server)

Shiny modules: Destroy module ui if server-function fails

How to display a blank UI (alternatively destroy module UI), if the module server-function fails, without moving all the UI-code to the server function?
Simple reproducible example:
library(shiny)
my_module_ui <- function(id) {
ns <- NS(id)
tags$div(
tags$h1("Don't show me if my_module_server fails!"),
plotOutput(ns("my_plot"))
)
}
my_module_server <- function(input, output, session) {
tryCatch({
my_data <- cars * "A" # fail for demo
# my_data <- cars
output$my_plot <- renderPlot({
cars2 <- my_data + rnorm(nrow(my_data))
plot(cars2)
})
}, error=function(cond) {
message("Destroy UI here!")
})
}
ui <- fluidPage(
my_module_ui("my_id")
)
server <- function(input, output, session) {
callModule(my_module_server, "my_id")
}
shinyApp(ui, server)
My current solution is to have nothing but a uiOutput() in my_module_ui and render the entire ui in the server function. I want to prevent this, since large modules get very messy if all UI-components are placed within the module server-function.
In addition I would preferably also like to avoid returning values from callModule() that destroy the UI and do this from within the server-function instead.
Thanks!
How about you assign a value to the session object and evaluate this value before you create the UI (from server side via renderUI().
1) Move rendering of UI to server side
Use renderUI(my_module_ui("my_id")) on server side and uiOutput("module") on ui side.
2) To detect whether your server module was successful assign a value to the session object
my_module_server <- function(input, output, session) {
tryCatch({
...
session$userData$mod_server <- TRUE
}, error = function(cond) {
session$userData$mod_server <- NULL
})
}
3) Use this value to make the call of your module ui conditional
output$module <- renderUI({
callModule(my_module_server, "my_id")
if(!is.null(session$userData$mod_server)) my_module_ui("my_id")
})
Reproducible example:
library(shiny)
my_module_ui <- function(id) {
ns <- NS(id)
tags$div(
tags$h1("Don't show me if my_module_server fails!"),
plotOutput(ns("my_plot"))
)
}
my_module_server <- function(input, output, session) {
tryCatch({
my_data <- cars * "A" # fail for demo
# my_data <- cars
output$my_plot <- renderPlot({
cars2 <- my_data + rnorm(nrow(my_data))
plot(cars2)
})
session$userData$mod_server <- TRUE
}, error = function(cond) {
session$userData$mod_server <- NULL
})
}
ui <- fluidPage(
uiOutput("module")
)
server <- function(input, output, session) {
output$module <- renderUI({
callModule(my_module_server, "my_id")
if(!is.null(session$userData$mod_server)) my_module_ui("my_id")
})
}
shinyApp(ui, server)
With a little code reordering, and the use of the amazing shinyjs package this can be done.
Note that I added an input to simulate errors and not errors, to see how the UI dissapears. Also all is done in the server part of the module. I hope this will help you. The code has inline comments explaining the steps.
library(shiny)
library(shinyjs)
my_module_ui <- function(id) {
ns <- NS(id)
tagList(
# input added to be able to throw errors and see the ui dissapear
selectInput(
ns('trigger'), 'Error trigger',
choices = list('no error' = c(2,1), 'error' = c('A', 'B')),
selected = 2
),
tags$div(
# div with id, to select it with shinyjs and hide it if necessary
id = ns('hideable_div'),
tags$h1("Don't show me if my_module_server fails!"),
plotOutput(ns("my_plot"))
)
)
}
my_module_server <- function(input, output, session) {
# get all the things prone to error in a reactive call, that way you capture the final
# result or a NULL reactive when an error occurs
foo <- reactive({
tryCatch({
if (input$trigger %in% c(2,1)) {
trigger <- as.numeric(input$trigger)
} else {
trigger <- input$trigger
}
cars * trigger
}, error=function(cond) {
message("Destroy UI here!")
})
})
# obseveEvent based on the error reactive, to check if hide or not the UI
observeEvent(foo(), {
# hide checking if foo is null, using shinyjs
if (is.null(foo())) {
shinyjs::hide('hideable_div')
} else {
shinyjs::show('hideable_div')
}
}, ignoreNULL = FALSE, ignoreInit = FALSE)
# outputs, with validation of the error reactive. That way code after validate is not
# executed but the app does not get blocked (gray)
output$my_plot <- renderPlot({
shiny::validate(
shiny::need(foo(), 'no data')
)
cars2 <- foo() + rnorm(nrow(foo()))
plot(cars2)
})
}
ui <- fluidPage(
# really important for shinyjs tu work!!!!!!!
shinyjs::useShinyjs(),
my_module_ui("my_id")
)
server <- function(input, output, session) {
callModule(my_module_server, "my_id")
}
shinyApp(ui, server)

keep selected rows when changing dataset in shiny DT datatable

I am using the DT package to display a data table in my shiny app. Since I provide different data sets, I have radio buttons to select them and the data table updates automatically.
What I would like to do is to preselect the available rows from df1 in df2 when switching the datasets. At the moment, my selection always get erased. When I try to save the selected rows (uncomment the two rows), my table get reset directly.
library(shiny)
library(DT)
df1 <- data.frame(names=letters,
values=1:26)
df2 <- data.frame(names=letters,
values=(1:26)*2)[seq(1,26,2),]
ui <- shinyUI(
fluidPage(
sidebarLayout(
sidebarPanel(
radioButtons("dataset", label=h5("Select dataset"),
choices=list("df1"='df1',
"df2"='df2'),
selected='df1', inline=TRUE)
),
mainPanel(
DT::dataTableOutput("name_table")
)
)
)
)
Server side...
server <- function(input, output, session) {
getDataset <- reactive({
result <- list()
result[['dataset']] <- switch(input$dataset,
'df1'=df1,
'df2'=df2)
# result[['selection']] <-
# as.numeric(input$name_table_rows_selected)
return(result)
})
output$name_table <- DT::renderDataTable({
DT::datatable(getDataset()[['dataset']],
options=list(pageLength=5))
})
name_proxy = DT::dataTableProxy('name_table')
}
shinyApp(ui, server)
I used the DT table, since I need the proxy and some interaction with the data table.
You can save selected rows only when going to change df like
server <- function(input, output, session) {
dd=reactiveValues(select=NULL)
observeEvent(input$dataset,{
dd$select=as.numeric(isolate(input$name_table_rows_selected))
})
getDataset <- reactive({
result <- list()
result[['dataset']] <- switch(input$dataset,
'df1'=df1,
'df2'=df2)
return(result)
})
output$name_table <- DT::renderDataTable({
DT::datatable(getDataset()[['dataset']],
options=list(pageLength=5),
selection = list(mode = 'multiple', selected =dd$select )
)
})
name_proxy = DT::dataTableProxy('name_table')
}
shinyApp(ui, server)
Or a bit modification of #drmariod variant: use eventReactive instead of reactive
server <- function(input, output, session) {
getDataset <- eventReactive(input$dataset,{
result <- list()
result[['dataset']] <- switch(input$dataset,
'df1'=df1,
'df2'=df2)
result[['selection']] <- testing()
return(result)
})
testing <- function() {
list(selected=as.numeric(input$name_table_rows_selected))
}
output$name_table <- DT::renderDataTable({
DT::datatable(getDataset()[['dataset']],
options=list(pageLength=5),
selection=getDataset()[['selection']])
})
name_proxy = DT::dataTableProxy('name_table')
}
Hm, it looks like I found a solution, but I wonder if there is a better solution.
server <- function(input, output, session) {
getDataset <- reactive({
result <- list()
result[['dataset']] <- switch(input$dataset,
'df1'=df1,
'df2'=df2)
result[['selection']] <- testing()
return(result)
})
testing <- function() {
list(selected=as.numeric(input$name_table_rows_selected))
}
output$name_table <- DT::renderDataTable({
DT::datatable(getDataset()[['dataset']],
options=list(pageLength=5),
selection=getDataset()[['selection']])
})
name_proxy = DT::dataTableProxy('name_table')
}
I wonder, sometimes comes a processing message. and on each click the table shortly "blinks"... Would be great to get a better answer.

Resources