Shiny Modules with Observes and reactiveValues - r

I have been trying to reconstruct the following simplistic Shiny app using modules since I believe that will be the best way to organize this code inside a much larger application where I will use these kinds of linked-slider-numeric inputs in many places.
However, I cannot figure out how to achieve the same kind of functionality from within a module.
Here's an example app that works exactly as intended, but not using modules:
library(shiny)
# Let's build a linked Slider and Numeric Input
server <- function(input, output) {
values <- reactiveValues(numval=1)
observe({
values$numval <- input$slider
})
observe({
values$numval <- input$number
})
output$slide <- renderUI({
sliderInput(
inputId = 'slider'
,label = 'SN'
,min = 0
,max = 10
,value = values$numval
)})
output$num <- renderUI({
numericInput(
inputId = 'number'
,label = 'SN'
,value = values$numval
,min = 0
,max = 10
)
})
}
ui <- fluidPage(
uiOutput('slide'),
uiOutput('num')
)
shinyApp(ui, server)
Here's my attempt. (Note that "mortalityRate" and associated strings are just an example of the variable name(s) I'll be using later). I have tried several variations on this attempt, but inevitably I get errors, usually indicating I'm doing something that can only be done inside a reactive context:
numericSliderUI <- function(id, label = "Enter value", min = 1, max = 40, value) {
ns <- NS(id)
tagList(
sliderInput(inputId = paste0(ns(id), "Slider"), label = label, min = min, max = max, value = value),
numericInput(inputId = paste0(ns(id), "Numeric"), label = label, min = min, max = max, value = value)
)
}
numericSlider <-
function(input,
output,
session,
value,
mortalityRateSlider,
mortalityRateNumeric
) {
values <- reactiveValues(mortalityRate = value())
observe({
values[['mortalityRate']] <- mortalityRateSlider()
})
observe({
values[['mortalityRate']] <- mortalityRateNumeric()
})
return( reactive( values[['mortalityRate']] ) )
}
library(shiny)
# source("modules.R") # I keep the modules in a separate file, but they're just pasted above for convenience here on StackOverflow.
ui <- fluidPage(
uiOutput('mortalityRate')
)
server <- function(input, output) {
values <- reactiveValues(mortalityRate = 1)
mortalityRateValue <- callModule(
numericSlider,
id = 'mortalityRate',
value = values[['mortalityRate']],
mortalityRateSlider = reactive( input$mortalityRateSlider ),
mortalityRateNumeric = reactive( input$mortalityRateNumeric )
)
values[['mortalityRate']] <- reactive( mortalityRateValue() )
output$mortalityRate <- renderUI(numericSliderUI('mortalityRate', value = values[['mortalityRate']]))
}
shinyApp(ui = ui, server = server)
I know that I must be doing something wrong with the reactiveValues and the way I'm using the observe statements inside the module, but this is my best attempt at using the module structure, so any help figuring out what I'm doing wrong would be very helpful.

Here is working code. There are a variety of changes, so I'll direct you to this Github page that also sets up a structure for using renderUI with modules. In general, I think the problems in your code involved trying to define reactive values inside the callModule function, and in passing the values of the sliders and numeric box back and forth.
Other features of using modules are that in your actual UI call, you need to call the UI module, where in turn you can call uiOutput. Inside renderUI is where you can set up the inputs. Additionally, inside modules you don't need the session namespaces, but you do need to wrap those ids in session$ns() to ensure they work across modules.
UI and Server Modules:
numericSliderUI <- function(id) {
ns <- NS(id)
uiOutput(ns('mortalityRate'))
}
numericSlider <- function(input, output, session) {
values <- reactiveValues(mortalityRate = 1)
observe({
values[['mortalityRate']] <- input$Slider
})
observe({
values[['mortalityRate']] <- input$Numeric
})
output$mortalityRate <- renderUI(
tagList(
sliderInput(inputId = session$ns("Slider"), label = "Enter value:", min = 1, max = 40, value = values[['mortalityRate']]),
numericInput(inputId = session$ns("Numeric"), label = "Enter value:", min = 1, max = 40, value = values[['mortalityRate']])
)
)
return(list(value = reactive({values[['mortalityRate']]})))
}
UI and Server functions:
ui <- fluidPage(
numericSliderUI('mortalityRate')
)
server <- function(input, output, session) {
mortalityRateValue <- callModule(numericSlider, 'mortalityRate')
}
shinyApp(ui = ui, server = server)

Related

Communication between modules and looping through values to create several modules

Goal
I have five expectations:
Solution using modules
Communication between modules
Dynamic creation of modules
local storage using shinyStore
Export result in dataframe
What has worked so far
This is a continuation of the following question.
I have a Shiny app that currently has two modules, but I have had issues with both of them communicating. The first module Selects any number of species within a Species pool (SpeciesSelect), this module is in the file R/SpeciesSelect.R within my working directory with the following code.
SpeciesSelect_UI <- function(id, SpeciesList){
ns <- NS(id)
tagList(
shiny::selectizeInput(inputId = ns("SpeciesNames"), label = "SpeciesName",
choices = SpeciesList,
multiple = T)
)
}
SpeciesSelect_Server <- function(id){
moduleServer(id, function(input, output, session) {
# return the reactive here
return(reactive({input$SpeciesNames}))
})
}
And the second module (SpeciesCount) would use those species in order to select how you sample them, and in some cases to count them when the method is equal to pinpoint. This is stored in R/SpeciesCount.R and the code is as follows:
SpeciesCount_UI <- function(id, Species){
ns <- NS(id)
tagList(
shinyMobile::f7Card(
f7Flex(
textOutput(ns("SpeciesAgain")),
uiOutput(ns("Sampling_type_ui")),
uiOutput(ns("SpeciesCount"))
)
)
)
}
SpeciesCount_Server <- function(id, Species){
moduleServer(id, function(input, output, session) {
output$SpeciesAgain <- renderText({Species})
ns <- session$ns
output$Sampling_type_ui <- renderUI({
#req(input$SpeciesName)
req(Species)
f7Select(inputId = ns("Sampling_type"),
label = "Sampling type",
choices = c("5m circle", "15m circle", "Pin-point"))
})
output$SpeciesCount <- renderUI({
if (req(input$Sampling_type) == "Pin-point") {
shinyMobile::f7Stepper(inputId = ns("Species1"), label = "Species count", min = 1, max = 1000, step = 1, value = 1)
}
})
})
}
Each of the modules is working well on its own as shown in the following example:
library(shiny)
library(shinyMobile)
library(shinyStore)
source("R/SpeciesCount.R")
source("R/SpeciesSelect.R")
SpeciesList <- c("Species1", "Species2", "Species3", "Species4", "Species5")
ui = f7Page(
title = "Show navbar",
f7SingleLayout(
navbar = f7Navbar("Hide/Show navbar"),
f7Button(inputId = "toggle", "Toggle navbar", color = "red"),
SpeciesSelect_UI(id = "SpeciesList", SpeciesList = SpeciesList),
lapply(seq_along(SpeciesList), function(i) {
SpeciesCount_UI(id = i, Species = SpeciesList[i])
})
)
)
server = function(input, output, session) {
lapply(seq_along(SpeciesList), function(i) {
SpeciesCount_Server(id = i, Species = SpeciesList[i])
})
observeEvent(input$toggle, {
updateF7Navbar()
})
}
shinyApp(ui, server)
I have 4 issues that are not working well, first, the communication between modules, and then looping through the results of the first module to get several of the second module, the localStorage issue, and finally exporting it to a dataframe
Communication between modules and dynamic UI generation
In order to isolate both issues, for the communication problem, I selected only one species and took out the lapply function to see if I can get the SpeciesCount to recognise the output of the SpeciesSelect_Server and incorporate it into the SpeciesCount module, here is the code I ended up with:
library(shiny)
library(shinyMobile)
library(shinyStore)
source("R/SpeciesCount.R")
source("R/SpeciesSelect.R")
LIST <- c("Species1", "Species2", "Species3", "Species4", "Species5")
ui = f7Page(
title = "Show navbar",
f7SingleLayout(
navbar = f7Navbar("Hide/Show navbar"),
f7Button(inputId = "toggle", "Toggle navbar", color = "red"),
SpeciesSelect_UI(id = "SpeciesList", SpeciesList = LIST),
SpeciesCount_UI(id = "SpeciesCount", Species = SpeciesSelected())
)
)
server = function(input, output, session) {
SpeciesSelected <- SpeciesSelect_Server(id = "SpeciesList")
SpeciesCount_Server(id = "SpeciesCount", Species = SpeciesSelected())
observeEvent(input$toggle, {
updateF7Navbar()
})
}
shinyApp(ui, server)
But the results of the SpeciesSelect module are not generating any UI in the SpeciesCount module
Adding the LocalStorage issue
This app is going to be used in the field, that means, that at time we might get connectivity issues, I have issues at storing the values of the Species Select Module, then for sure I will have issues with the next module this is the shiny app I am using
library(shiny)
library(shinyMobile)
library(shinyStore)
source("R/SpeciesCount.R")
source("R/SpeciesSelect.R")
SpeciesList <- c("Species1", "Species2", "Species3", "Species4", "Species5")
ui = f7Page(
title = "Show navbar",
f7SingleLayout(
navbar = f7Navbar("Hide/Show navbar"),
f7Button(inputId = "toggle", "Toggle navbar", color = "red"),
SpeciesSelect_UI(id = "SpeciesList", SpeciesList = SpeciesList),
lapply(seq_along(SpeciesList), function(i) {
SpeciesCount_UI(id = i, Species = SpeciesList[i])
})
)
)
server = function(input, output, session) {
lapply(seq_along(SpeciesList), function(i) {
SpeciesCount_Server(id = i, Species = SpeciesList[i])
})
observeEvent(input$toggle, {
updateF7Navbar()
})
}
shinyApp(ui, server)
And I modified the species select for that also
SpeciesSelect_UI <- function(id, SpeciesList){
ns <- NS(id)
tagList(
shiny::selectizeInput(inputId = ns("SpeciesNames"), label = "SpeciesName",
choices = SpeciesList,
multiple = T)
)
}
SpeciesSelect_Server <- function(id){
moduleServer(id, function(input, output, session) {
ns <- session$ns
# return the reactive here
observeEvent(input$save, {
updateStore(session, name = ns("SpeciesNames"), input$SpeciesNames)
}, ignoreInit = TRUE)
observeEvent(input$clear, {
# clear current user inputs:
updateTextInput(session, inputId = ns("SpeciesNames"), value = "")
# clear shinyStore:
updateStore(session, name = ns("SpeciesNames"), value = "")
}, ignoreInit = TRUE)
return(reactive({ns(input$SpeciesNames)}))
})
}
But nothing gets stored. Maybe creating a module for shiny store is needed?
Export as a dataframe
This one is tied two point 2:
So lets say I am in the following input set:
The idea would be to generate a reactive that has the following that frame, that I can then export as a CSV file. I think I can handle the export, but I am unsure on how to generate the data.frame from the dynamic UI:
data.frame(Species = c("Species1", "Species2", "Species3"), Method = c("Pin-point","5m circle", "15m circle"), abundance = c(5, 1, 1))
Your first module is probably already silently returning the reactive but for clarity you can make it explicit. In you first module, return a reactive:
SpeciesSelect_Server <- function(id){
moduleServer(id, function(input, output, session) {
# return the reactive here
return(reactive({input$SpeciesNames}))
})
}
Now call the module AND assign its output a name where you'd like to use it (in another module or in your app server), like this:
selected_species <- SpeciesSelect_Server(id = "SpeciesList")
Now selected_species can be called, observed, etc with:
selected_species()

Set a default value in shiny inputs (in case the user deletes it in the UI)

I am trying to set a default (or fallback) value for numericInput() in my shiny app to prevent NAs.
I am aware that the NA can be dealt with later in the server.r, but was wondering if there is a more elegant way of replacing the value within the input whenever a user deletes it in the ui.
The best way is to use the validate package with need() (see this SO thread), but here is something simpler and closer to what you are asking for:
library(shiny)
ui <- fluidPage(
numericInput("obs", "Observations:", 10, min = 1, max = 100),
verbatimTextOutput("value")
)
server <- function(input, session, output) {
dafault_val <- 0
observe({
if (!is.numeric(input$obs)) {
updateNumericInput(session, "obs", value = dafault_val)
}
})
output$value <- renderText({ input$obs })
}
shinyApp(ui, server)
I'd recommend using library(shinyvalidate), which is RStudios "official" way to solve this:
library(shiny)
library(shinyvalidate)
ui <- fluidPage(
numericInput(
inputId = "myNumber",
label = "My number",
value = 0,
min = 0,
max = 10
),
textOutput("myText")
)
server <- function(input, output, session) {
iv <- InputValidator$new()
iv$add_rule("myNumber", sv_required(message = "Number must be provided"))
iv$add_rule("myNumber", sv_gte(0))
iv$add_rule("myNumber", sv_lte(10))
iv$enable()
output$myText <- renderText({
req(iv$is_valid())
input$myNumber
})
}
shinyApp(ui, server)

shiny modules: Store parameters (additional argument) already when creating module-UI instead of passing it to module's server function?

I have created a module sliderCheckbox which bundles together a sliderInput and a checkBoxInput to disable the sliderInput - basically a possibility to state "I don't know", which is necessary for survey-like inputs. When the slider is disabled, I want it to return a default value - most often the initial value, but not necessarily.
Now my question is: Is there any possibility to pass this default value when initialising the UI, that is with sliderCheckboxInput()? As the default value is a property like minimum and maximum, that is where it logically belongs to, and it also fits better to the rest of my setup.
Example:
library(shiny)
library(shinyjs)
sliderCheckboxInput <- function(id,description="",
min = 0,
max = 100,
value = 30,
default= NULL ##HERE I would want the default value to be set
cb_title = "I don't know"){
ns <- NS(id)
fluidRow(
column(width=9,
sliderInput(ns("sl"),
paste0(description, collapse=""),
min = min,
max = max,
value = value)
),
column(width=2,
checkboxInput(ns("active"),
cb_title, value=FALSE )
)
)
}
sliderCheckbox<- function(input, output, session,
default=NA) { #Problem: set default when initialising module
oldvalue<- reactiveVal()
observeEvent(input$active, {
if (input$active){
oldvalue(input$sl)
disable("sl")
updateSliderInput(session, "sl", value=default)
}else {
updateSliderInput(session, "sl", value=oldvalue())
enable("sl")
}
toggleState("sl", !input$active)
})
onclick("sl",
if(input$active) updateCheckboxInput(session, "active", value=FALSE)
)
return ( reactive({
if (input$active){
default
}else {
input$sl
}
}))
}
ui <- fluidPage(
useShinyjs(),
# Sidebar with a slider input for number of bins
sidebarLayout(
sidebarPanel(
sliderCheckboxInput("bins", "Number of bins:",
min = 1,
max = 50,
value = 30)
),
# Show a plot of the generated distribution
mainPanel(
plotOutput("distPlot")
)
)
)
server <- function(input, output, session) {
bins_nr <- callModule(sliderCheckbox, "bins", default=44)
output$distPlot <- renderPlot({
# generate bins based on input$bins from ui.R
x <- faithful[, 2]
bins <- seq(min(x), max(x), length.out = bins_nr() + 1)
# draw the histogram with the specified number of bins
hist(x, breaks = bins, col = 'darkgray', border = 'white')
})
}
shinyApp(ui, server)
You can send the value from the ui to the server using a hidden textInput
library(shiny)
library(shinyjs)
sendValueToServer <- function(id, value) {
hidden(textInput(
id, "If you can see this, you forgot useShinyjs()", value
))
}
myModuleUI <- function(id, param) {
ns <- NS(id)
tagList(
sendValueToServer(ns("param_id"), param),
textOutput(ns("text_out"))
)
}
myModule <- function(input, output, session) {
param <- isolate(input$param_id)
output$text_out <- renderText({
param
})
}
shinyApp(
ui = fluidPage(
useShinyjs(),
myModuleUI("id", "test")
),
server = function(input, output, session) {
callModule(myModule, "id")
}
)
There are probably more direct ways to do this using the JavaScript API of shiny but this is a "pure R" solution which should be enough for most usecases. Note that you can use the input value at initialization time with
isolate(input$text_in)
because the ui is always built before the server. Things get more complicated if everything is wrapped into renderUI but this does not seem to be the case for you.
Somewhat late to the party, but I think a neater way to do this is to use session$userData. This is available to both the main server function and the module's sewrver function.
So, in the main server, before callModule creates the module server:
session$userData[["module_id"]]$defaultValue <- myDefaultValue
and then at the end of module server function:
return ( reactive({
if (input$active){
session$userData[["module_id"]]$defaultValue
} else {
input$sl
}
})
)
That strikes me as neater, more robust and more generic than using a hidden input.

Reactive function not being found within app

I have a fairly involved app. When I call a particular eventReactive function, let's call it function A, within a reactive expression, I get an error that function A cannot be found.
I'm unable to reproduce the exact app because it is proprietary, but I did create a dummy app that simulates the setup I have. I realize that there must be some difference between the dummy app and what I actually have, but I can't figure it out. The function in question is there, so I fundamentally don't understand why it's not being found.
library(shiny)
ui <- fluidPage(
sliderInput(inputId = "num",
label = "Choose a number",
value = 25, min = 1, max = 100),
actionButton(inputId = "action",
label = "Update"),
plotOutput("hist"),
verbatimTextOutput("stats")
)
server <- function(input, output) {
data <- eventReactive(input$action, {
input$num*2
})
data2 <- reactive({
data()*2
})
output$stats <- renderPrint({
data2()
})
}
shinyApp(ui = ui, server = server)

R Shiny : dynamic number of tables within a tab

first I know there is a lot of threads covering my problem, I read them all, but I did not manage to do it. I got a list of 10 data.frame which I built through the following code :
list_of_df=list()
for (i in seq(1,10)){
number_row=sample(seq(5,10),size = 1)
num=seq(1,number_row)
val=sample(x = letters,size = number_row,replace = TRUE )
df=data.frame(num=num,
val=val)
rownames(df)=NULL
list_of_df[[i]]=df
}
I want the user to enter n, the number of tables he wants to see. And then display n random tables from the list_of_df. I want to display those tables inside tabs. Here is what I did, I grabbed some ideas here and there, but obviously it does not work and I do not know why.
library(shiny)
# ui function
ui = pageWithSidebar(
headerPanel('Dynamic Tabs'),
sidebarPanel(
numericInput(inputId = "numput",label = "number of tables",value = 1,min = 1,max = 5)
),
mainPanel(
uiOutput('mytabs')
)
)
# server function
server = function(input, output, session){
random_tables<- reactive({
index=sample(seq(1,10),size = input$numput,replace=FALSE)
list_of_df[[index]]
})
size<-reactive({
length(random_tables())
})
for (i in 1:size()) {
local({
my_i <- i
tablename <- paste("table_", my_i, sep="")
output[[tablename]] <- renderTable({
random_tables()[[i]]
})
})
}
output$mytabs = renderUI({
nTabs = size()
myTabs = lapply(paste0('table_', 1: nTabs), function(x){
tabPanel(x, tableOutput(x))
})
do.call(tabsetPanel, myTabs)
})
}
shinyApp(ui, server)
So, if you see what I should do ...
Here is a working version:
library(shiny)
# ui function
ui = pageWithSidebar(
headerPanel('Dynamic Tabs'),
sidebarPanel(
numericInput(inputId = 'numput',label = "number of tables",value = 1,min = 1,max = 5)
),
mainPanel(
uiOutput('mytabs')
)
)
# server function
server = function(input, output, session){
list_of_df=list()
for (i in seq(1,10)){
number_row=sample(seq(5,10),size = 1)
num=seq(1,number_row)
val=sample(x = letters,size = number_row,replace = TRUE )
df=data.frame(num=num,
val=val)
rownames(df)=NULL
list_of_df[[i]]=df
}
random_tables<- reactive({
index=sample(seq(1,10),size = input$numput,replace=FALSE)
list_of_df[index]
})
size<-reactive({
input$numput
})
observe({
lapply(seq_len(size()), function(i) {
local({
my_i <- i
tablename <- paste("table_", my_i, sep="")
output[[tablename]] <- renderTable({
random_tables()[[i]]
})
})
})
})
output$mytabs = renderUI({
nTabs = size()
myTabs = lapply(paste0('table_', seq_len(nTabs)), function(x){
tabPanel(x, tableOutput(x))
})
do.call(tabsetPanel, myTabs)
})
}
shinyApp(ui=ui,server=server)
A couple issues, you subset the list with double brackets, but it isn't working like you think it is, you need single brackets. Next when you select a single table random_table() is a data.frame so when you call length you get 2, the number of columns. So just use the input$numput for size() since they are the same anyways. Also, I put the dynamic output in an observe so that it can access the reactive size(). A small thing, but I used seq_len instead of 1:aNumber since it is more robust.
Hope this helps

Resources