R Shiny modules with conditionalPanel and reactives - r

I am trying to modularize a complex Shiny app for which I have a conditionalPanel that should only appear given a certain input state.
Before I made everything modular, the input and conditionalPanel were both in ui.R, and I could reference the input using something like this:
conditionalPanel("input.select == 'Option one'", p('Option one is selected'))
Now that I have modularized things, accessing the input is more complicated. I thought the following was the way to do it, but it doesn't quite work. (Here I've combined things into a single standalone script):
library(shiny)
## Module code for 'selectorUI' and 'selector'
selectorUI <- function(id) {
ns <- NS(id)
selectizeInput(inputId = ns('select'),
label = 'Make a choice:',
choices = c('Option one', 'Option two'))
}
selector <- function(input, output, session) {
reactive(input$select)
}
## Main app
ui <- shinyUI(fluidPage(
selectorUI('id1'),
conditionalPanel(condition = "output.selected == 'Option one'", p('Option one is selected.'))
))
server <- shinyServer(function(input, output, session) {
output$selected <- callModule(selector, 'id1')
})
shinyApp(ui = ui, server = server)
I think this should work, but it doesn't - it only works if I make another reference to output$selected in the main ui section:
ui <- shinyUI(fluidPage(
selectorUI('id1'),
textOutput('selected'), ## Adding just this one line makes the next line work
conditionalPanel(condition = "output.selected == 'Option one'", p('Option one is selected.'))
))
Unfortunately of course this has the unwanted effect of rendering the result of textOutput('selected'). I can only guess that the reason this works is because it somehow triggers the reactive in a way that the JavaScript reference alone does not.
Any idea how I should be getting this conditionalPanel to work properly?
Thank you..
EDIT: Turns out not actually a bug: https://github.com/rstudio/shiny/issues/1318. See my own answer below.
But also note that I actually like the renderUI solution given in the accepted answer better than my original conditionalPanel approach.

After calling the module the ID of selectizeInput is id1-select. In javaScript there are two ways of accessing object properties:
objectName.property or objectName['property']
Since there is - in the ID we have to refer to it via string, so the second method is way to go.
The condition in conditionalPanel becomes:
input['id1-select'] == 'Option one'
Full example:
library(shiny)
## Module code for 'selectorUI' and 'selector'
selectorUI <- function(id) {
ns <- NS(id)
selectizeInput(inputId = ns('select'),
label = 'Make a choice:',
choices = c('Option one', 'Option two'))
}
## Main app
ui <- shinyUI(fluidPage(
selectorUI('id1'),
conditionalPanel(condition = "input['id1-select'] == 'Option one'",
p('Option one is selected.'))
))
server <- shinyServer(function(input, output, session) {
})
shinyApp(ui = ui, server = server)
EDIT:
This does work, but doesn't it violate the notion of modularity? You would have to know the code for the module internally calls that input 'select' in order to construct 'id1-select'.
Yes, you're right.
According to this article, the trick you used i.e. assigning a module call to the output$selected and then accessing its value on the client side via output.selected should work but it doesn't. I don't know why...it is maybe a bug. (I have the newest shiny version from github)
The only thing I can think of is to use renderUI instead of conditionalPanel as in the example below:
library(shiny)
## Module code for 'selectorUI' and 'selector'
selectorUI <- function(id) {
ns <- NS(id)
selectizeInput(inputId = ns('select'),
label = 'Make a choice:',
choices = c('Option one', 'Option two'))
}
selector <- function(input, output, session) {
reactive(input$select)
}
## Main app
ui <- shinyUI(fluidPage(
selectorUI('id1'),
uiOutput("dynamic1")
))
server <- shinyServer(function(input, output, session) {
output$dynamic1 <- renderUI({
condition1 <- callModule(selector, 'id1') # or just callModule(selector, 'id1')()
if (condition1() == 'Option one') return(p('Option one is selected.'))
})
})
shinyApp(ui = ui, server = server)

Turns out it actually isn't a bug, just a little tricky. According to Joe Cheng,
Right--we don't, by default, calculate/render output values if they aren't going to be visible. And if we don't calculate them, you can't use them in conditions.
You can change this behavior this by setting an output to calculate every time, you can use this in your server.R (replace outputId with the corresponding value):
outputOptions(output, "outputId", suspendWhenHidden = FALSE)
So to fix the problem with my original example, we only need to add that one line to the server function:
server <- shinyServer(function(input, output, session) {
output$selected <- callModule(selector, 'id1')
outputOptions(output, 'selected', suspendWhenHidden = FALSE) # Adding this line
})

Related

How to pass a calculated file name back to the UI in Shiny

In my shiny server I am figuring out the name of a markdown file which I want to show in the UI. I know how to pass the file name, as a string, back to the UI but I don't now how to tell includeMarkdown() to treat the string as a file name.
My code so far is below. Any advice?
library(shiny)
fileConn<-file("hello.md")
writeLines(c("# Hello","# World"), fileConn)
close(fileConn)
ui <- fluidPage(
includeMarkdown("hello.md"),
br(),
div("File name text:", textOutput("fileNameText", inline = TRUE)),
#includeMarkdown(fileNameText) # this needs help
)
server <- function(input, output, session) {
selectedName <- reactive({
paste0("hello.md") # this is actually more complicated...
})
output$fileNameText <- renderText(
paste0(selectedName())
)
}
shinyApp(ui = ui, server = server)
Your example code works fine, but from your description, I am thinking your asking how to pass a different filename to includeMarkdown() that was created somewhere else in the app, correct?
The first step is to understand includeMarkdown() as a UI element that will change depending on other UI elements (and stuff that happens in server). The solution is to use a placeholder in the ui to hold the place for the includeMarkdown() element, and create that particular element in server using renderUI.
Hopefully you can follow this example. I'm using uiOutput('displayFile') to hold the place for the element that's created in server.
library(shiny)
fileConn<-file("hello.md")
writeLines(c("# Hello","# World"), fileConn)
close(fileConn)
fileConn1<-file("goodbye.md")
writeLines(c("# Goodbye","# Everyone!"), fileConn1)
close(fileConn1)
ui <- fluidPage(
selectInput('file_selection', 'Choose Markdown File:', choices=c('hello.md','goodbye.md')),
uiOutput('displayFile')
)
server <- function(input, output, session) {
output$displayFile <- renderUI({
includeMarkdown(input$file_selection)
})
}
shinyApp(ui = ui, server = server)

Un-suspend elements in hidden part in Shiny

When I use standard Shiny, a change in the tabPanel somehow tells the output elements to un-suspend and show their value:
library(shiny)
ui1 <- fluidPage(
tabsetPanel(
tabPanel("Start", p("Nothing to see here, really")),
tabPanel("Calc", actionButton("do", "Calc"), verbatimTextOutput("out"))
)
)
server1 <- function(input, output, session) {
output$out <- renderPrint({
input$do
})
}
shinyApp(ui1, server1)
works as expected.
Now, if I switch to another framework ( bulma in this case), the elements stay suspended. In order to make them work I have to explicitly switch off the suspendWhenHidden flag.
library(shiny)
library(shinybulma)
ui2 <- bulmaPage(
bulmaTabs(
c("Start", "Calc"),
center = FALSE,
bulmaTab("Start", p("Nothing to see here, really")),
bulmaTab("Calc", bulmaActionButton("do", "Calc"), verbatimTextOutput("out"))
)
)
server2 <- function(input, output, session) {
## Need to comment out to make it run
# session$onFlushed(function() {
# outputOptions(output,
# "out",
# suspendWhenHidden = FALSE)
# })
output$out <- renderPrint({
input$do
})
}
shinyApp(ui2, server2)
I assume that there is a (JS?) trigger attached to a change in tabSets which un-suspends the elements once the element is shown. How can I do the same for the bulma case? In other words, how can I un-suspend the elements once the respective part is shown (either JS/R is fine)?
For future reference: I issued a "bug" report on the github repo of shiny. As assumed there is an event which needs to be triggered (shown) in order to get that work (on tab change in this case)
I submitted a PR on shinybulma so this should be fixed.

How to refer to reactive element in ui.R in Shiny

I am making an app with drag and drop feature using the ShinyDND package. I would like to pass a list from input as a parameter of dragSetUI, a function that needs to be run in ui.R. I tried renderUI and uiOutput, and it almost works, but the dragged elements fail to be dropped in the drop area. As you can see in the example below, nonreactive choices work like charm. I tried creating a reactive object as well as text output, but I could not find documentation on how to refer to these objects in ui.R. Normally one would refer to output$x as "x" in Output, but here, if I add anything between quotes, it reads as string. I am really stuck with this.
library(shiny)
library(shinyDND)
nonreactive_choices<-as.list(c("a","b","c"))
ui <- shinyUI(
mainPanel(
textInput("choices","Put here a,b,c:"),
dragSetUI("drag", textval = "reactive_choices"),
dragSetUI("drag", textval = "choice_rv"),
textOutput("reactive_choices"),
dragSetUI("drag", textval = nonreactive_choices),
dropUI("drop")
)
)
server = shinyServer(function(input, output,session) {
output$reactive_choices<-reactive({
strsplit(input$choices,",")
})
observe({
chlist<-strsplit(input$choices,",")
choice_rv<-reactiveVal(chlist)
})
})
# Run the application
shinyApp(ui = ui, server = server)
Let's see why the renderUI approach does not work with shinyDND. An app using shinyDND is linked to the dragndrop.js file, which is in the shinyDND folder. In this file one can see:
$(document).ready(function(){
......
$(".dragelement").on("dragstart",function(e){
e.originalEvent.dataTransfer.setData("Text",e.target.id);
});
......
});
This defines the action to perform on elements having class dragelement when a drag is starting, and this is defined when the document is ready. dragSetUI creates such elements.
When you use a dragSetUI inside a renderUI, this creates new elements of class dragelement. But the action defined on such elements in $(document).ready is not effective for these new elements, because this action has been defined just after the document is ready, and then before the effect of renderUI.
A solution consists in defining the action of the event dragstart inside the renderUI. This works:
library(shiny)
library(shinyDND)
nonreactive_choices<-as.list(c("a","b","c"))
ui <- shinyUI(
mainPanel(
textInput("choices","Put here d,e,f:"),
dragSetUI("drag", textval = nonreactive_choices),
uiOutput("dragset"),
dropUI("drop")
)
)
server = shinyServer(function(input, output,session) {
reactive_choices <- reactive({
strsplit(input$choices,",")[[1]]
})
output$dragset <- renderUI({
tagList(
dragSetUI("drag2", textval = as.list(reactive_choices())),
tags$script('$(".dragelement").on("dragstart",function(e){
e.originalEvent.dataTransfer.setData("Text",e.target.id);
});'
)
)
})
})
# Run the application
shinyApp(ui = ui, server = server)
Comment by #ismirsehregal helped me find the solution: shinyjqui can be used for my purposes and it seems to work from inside renderUI. Here is the edited code that does exactly what I needed.
library(shiny)
library(shinyjqui)
ui <- fluidPage(
textInput("choices","Put here a,b,c:"),
uiOutput("reactiveselect"),
orderInput(inputId = 'drop', label = 'Reactive drop', items = NULL,placeholder = "drop here..."),
verbatimTextOutput("droppedorder")
)
server <- function(input, output) {
output$reactiveselect <- renderUI({
req(input$choices)
reactiveitems<- unlist(strsplit(input$choices,","))
orderInput("groupstochoose", "groups to choose from:", connect='drop',items=reactiveitems)
})
output$droppedorder<-
renderPrint({input$drop_order})
}
shinyApp(ui, server)

Include a conditionalPanel in a Shiny module with condition based on global input

I am trying to write a Shiny module which shows a conditionalPanel based on input from the global UI. In the minimal example below the conditionalPanel should show a radioButtons widget when a checkbox in the global UI is clicked, but I can't get it to work.
What am I doing wrong?
library(shiny)
conditional <- function(input, output, session, check){
output$check <- reactive({check()})
outputOptions(output, "check", suspendWhenHidden = FALSE)
output$conditional <- renderUI({
ns <- session$ns
conditionalPanel(
condition = 'output.check',
radioButtons(ns('radioItem'),
'Select option',
choices = c('option 1','option 2'))
)
})
}
conditionalUI <- function(id){
ns <- NS(id)
uiOutput(ns('conditional'))
}
ui <- fluidPage(
fluidRow(checkboxInput('check','Show')),
fluidRow(conditionalUI('mymod'))
)
server <- function(input, output, session) {
check <- reactive({input$check})
callModule(conditional, 'mymod', check = check)
}
shinyApp(ui = ui, server = server)
Simple fix - The condition should be condition = input.check instead of condition = output.check.
You are having a problem with the naming conventions that shiny modules enforce.
Although you have a similar output object in your module, it is not the same as in server. If you specify an output
func <- function(input, output, session) {
output$something <- (...)
}
inside a module, that you called with
callModule(func, 'someIdentifier')
then your output id, which shiny uses to reference all the elements, becomes
someIdentifier-something
You can test this by writing uiOutput("mymod-conditional") instead of uiOutput(ns('conditional')).
Normally, this shouldn't bother you, since modules work the way that all references are resolved within a module. But the conditionalPanel condition, being in JavaScript ("on the other side" so to say), must use global references.
So the fix for your problem would be to change the condition to
condition = 'output["mymod-check"]'
Note that dashes cant be used with JavaScript dot notation, so bracket notation has to be used.
A trick that helped me identify the problem, was to inject JavaScript into the condition in order to show the current value of output on the client side. I placed condition = 'console.log(output)' inside the conditionalPanel so you can inspect the available object in the browser console.

Shiny UI Module Issue: server module not updating choices with reactive expression

I am having a lot of trouble getting a search filtering module working.
I am to run stats on a large database of cat owner information.
I want my search module to bring up a list of possible owners(that the user can select from) based on a selection from a list of cat breeds.
I thought wrapping the updateSelectInput with observe and using a reactive cat owner expression would facilitate this, in the module, but it is not working( and I can't guess why this is happening or how to debug this). It worked in these other posts([1]:R shiny passing reactive to selectInput choices , [2]:using values from a reactive input to directly input into a custom function)
Why won't my selectInput update with cat owners?
library(shiny)
df=data.frame(
cat=c("tabby","DSH","MSH","LSH","DSH","MSH","LSH","sphinx"),
owner=c("Foo","Bar","Bash","Foo","Foo","Foo","Bar","Bash"),stringsAsFactors = F)
refinedSearch<-function(input, output, session){
ownsCat<-reactive({df[df$cat%in%input$cat,"owner"]})
observe({updateSelectInput(session, "ownerSelected",
label ="Owned By",choices = ownsCat())})
return()
}
refinedSearchUI<-function(id){
ns <- NS(id)
fluidRow(
column(4,selectInput(ns("cat"),"Cat",selectize = T,
choices =c("tabby","DSH","MSH","LSH","sphinx") )),
column(4,selectInput(ns("ownerSelected"),"Owned By","",selectize = T))
)
}
ui <- fluidPage(
h1("Find cats owners"),
fluidRow(column(10,offset=1, refinedSearchUI("tmp"))),
fluidRow(column(10,offset=1, actionButton("addFilter","Add a Filter",
icon = icon("plus"))))
)
server <- function(input, output,session) {
refinedSearch(input,output,session)
observeEvent(input$add, {insertUI(selector = "#addFilter",where = "beforeBegin",
ui = refinedSearch(input,output,session))})
}
shinyApp(ui = ui, server = server)
Thank y'all for you time.
There seems to be quite a bit of confusion on how to call modules. You need to use the callModule() function in the server. Also, when inserting UI (using the insertUI()function), you need to call the refinedSearchUI() function, not the refinedSearch() function (which, again, should always be called through callModule(), so it should never actually get called directly like that).
I'd recommend a re-reading of the modules article.
You also have a typo. The event in your observeEvent() function should be input$addFilter, not input$add (which doesn't exist, so that observer is never fired..)
If you change your server function to this, your app will work as expected:
server <- function(input, output,session) {
callModule(refinedSearch, "tmp")
observeEvent(input$addFilter, {
id <- paste0("filter_", input$add)
insertUI(selector = "#addFilter",where = "beforeBegin",
ui = refinedSearchUI(id))
callModule(refinedSearch, id)
})
}

Resources