Shiny input update fails with nested outputs - r

The following minimal example demonstrates a problem with updating nested outputs followed by updating the inputs with the content they had before the update of the outputs. This minimal example comes from a much larger project, so solutions to the problem like "don't nest the outputs" aren't viable unless Shiny doesn't actually allow nested outputs.
When you run the example, you'll see two radio buttons and a text input. Enter some text and change the radio button. You will see that after the radio button event is handled, the text input has indeed been updated. But if you change the radio button a second time, the text input at the observeEvent() will be blank.
It's like the updateTextInput() function works fine for updating what you see in the browser, but not at all for what Shiny gets back from the browser for that input on the next event.
To add a bit of strange, also note that if you change the updated text input in any way (e.g., add or remove one character to/from it), then it works as expected. Is the problem here my code, my expectations, or a bug in Shiny?
ui <- fluidPage(fluidRow(column(12, tagList(
uiOutput("outer_div")
))))
server <-function(input, output, session) {
updateScreen = function() {
output$outer_div = renderUI(uiOutput("inner_div"))
output$inner_div = renderUI(tagList(
radioButtons(inputId = "button", label="", choices = c("A", "B")),
textInput("text", label="", value="")
))
}
updateScreen() #initialize screen
observeEvent(input$button, {
print(paste0("Text input is ", input$text))
updateTextInput(session, inputId = "text" , value=input$text)
updateRadioButtons(session, inputId="button", selected=input$button)
updateScreen()
})
}
shinyApp(ui = ui, server = server)

Does this work for you?
ui <- fluidPage(fluidRow(column(12, tagList(
uiOutput("outer_div")
))))
server <-function(input, output, session) {
updateScreen = function() {
output$outer_div = renderUI(uiOutput("inner_div"))
output$inner_div = renderUI(tagList(
radioButtons(inputId = "button", label="", choices = c("A", "B")),
textInput("text", label="", value="")
))
}
updateScreen() #initialize screen
data <- isolate(input$text)
observeEvent(input$button, {
updateTextInput(session, inputId = "text" , value=data)
updateRadioButtons(session, inputId="button", selected=input$button)
print(paste0("Text input is ", input$text))
#updateScreen()
})
}
shinyApp(ui = ui, server = server)

Related

Disabling selectizeInput in second page of datatable in RShiny

I have a datatable, in which I have embedded selectizeInputs. I have used jquery to enable some options in the selectizeInputs (like creation options).
Now, due to a business use case I would like to disable some selectizeInputs (dynamically selected, through some condition). These inputs could be on the 2nd, 3rd,.. nth page of the datatable as well.
However, I am only able to disable the inputs on the 1st page and not on the subsequent pages. I am attaching a minimal and reproducible example, it would be great if someone could help me out.
library(shiny)
library(DT)
ui <- fluidPage(
shinyjs::useShinyjs(),
selectizeInput(
inputId = "input",
label = "",
choices = letters[1:26],
selected = letters[1]
),
fluidRow(
DTOutput(outputId = "table"),
tags$script(HTML("Shiny.addCustomMessageHandler('unbind-DT', function(id) {
Shiny.unbindAll($('#'+id).find('table').DataTable().table().node());
})"))
)
)
df <- data.frame('a' = c(1,2), 'sel_input' = NA)
df[1,'sel_input'] <- as.character(
selectizeInput(inputId = 'mselect', choices=c('car','cars','dog'),
label=NULL, selected=NULL))
df[2,'sel_input'] <- as.character(
selectizeInput(inputId = 'nselect', choices=c('lambo','audi','merc'),
label=NULL, selected=NULL))
js <- c(
"function(){Shiny.bindAll(this.api().table().node());",
" $('#mselect').selectize({
delimiter: \',\',
persist: false,
create: function(input) {
return {
value: input,
text: input
}
}
});",
"$('#nselect').selectize({
delimiter: \',\',
persist: false,
create: function(input) {
return {
value: input,
text: input
}
}
});",
"$('#mselect')[0].selectize.enable()",
"$('#nselect')[0].selectize.disable()",
"}"
)
server <- function(input, output, session) {
observe({
print(input$mselect)
})
session$sendCustomMessage('unbind-DT', 'table')
output$table <- renderDT({
datatable(
data = df,
escape = FALSE,
options = list(
dom='tp',
pageLength=1,
processing=F,
preDrawCallback = JS('function(){Shiny.unbindAll(this.api().table().node());}'),
drawCallback = JS(js)
)
)
})
}
shinyApp(ui = ui, server = server)
So, I was able to resolve it on my own. Basically the problem here is not R/Rshiny based. It is actually a javascript bug in the code that I was overlooking.
When you do pagination, the elements only in current (selected) page are a part of the DOM. All others are removed/not created. In the code above, in the drawCallback (this is the piece of code that runs every time the datatable needs to be re-rendered) I am issuing commands for all the elements, irrespective if they are present in the DOM or not. Because of this the javascript code fails and the disabling/enabling does not happen.
The solution here is to first check if an element is active in the DOM or not and only then issue enabling/disabling command.
So, in essence, enclose the above commands in a if else statement
if ($('#mselect').length > 0){
$('#mselect').selectize()[0].selectize.enable();
}
if ($('#nselect').length > 0){
$('#nselect').selectize()[0].selectize.disable();
}
This way the javascript code will only run when that specific element exists in the DOM and then you would be able to implement disabling selectInput in second page of paginated datatable.

Updating selection of server-side selectize input with >1000 choices fails

I have a shiny app containing a server-side selectize input with a long list (>10k) of choices.
I would like to update the selection upon clicking a button. Here is a reproducible example
library(shiny)
ui <- fluidPage(
actionButton('buttonid','Button'),
selectizeInput("listid", "A long list", choices = NULL)
)
server <- function(input, output, session)
{
updateSelectizeInput(session, "listid", choices = paste("Item", 1:10000), server = T)
observeEvent(input$buttonid,
{
updateSelectizeInput(session, "listid", selected = "Item 1234")
})
}
shinyApp(ui = ui, server = server)
The above code results in a blank selection when I press the button.
However, if I search for "Item 1234", then change selection, and press the button, now the item gets selected.
Also, trying to select an item between Item 1 and 1000 does not give problems, presumably because 1000 items get loaded at the beginning.
This seems akin to this old bug, but I am not sure if there is a workaround https://github.com/rstudio/shiny/issues/1018
The short answer is explicitly re-specify your choices and server in your update.
server <- function(input, output, session)
{
myChoices <- paste("Item", 1:10000)
updateSelectizeInput(session, "listid", choices = myChoices, server = T)
observeEvent(input$buttonid,
{
updateSelectizeInput(session, "listid",
server = TRUE,
choices = myChoices,
selected = "Item 1234")
})
}
The default for server in updateSelectizeInput is FALSE. This causes the code to drop into a control statement which uses updateSelectInput instead. From the function code
function (session, inputId, label = NULL, choices = NULL, selected = NULL,
options = list(), server = FALSE)
{
...
if (!server) {
return(updateSelectInput(session, inputId, label, choices,
selected))
}
...
This sends a message to the client assuming all the choices are present (but as you mentioned, only the first 1000 are present).
Just setting server = TRUE results in an error when you click the button.
Warning: Error in [.data.frame: undefined columns selected
[No stack trace available]
I didn't fully trace the reason, but it ultimately creates an empty choices data.frame with an attribute denoting the selected value. I'm guessing somewhere else in function calls to the session object, this attribute is being used to try to select a column created from the empty data.frame.
The update function doesn't seem to change the choices stored on the server, so that's presumably why it's there when you search for it. During the changing the selected value, it seems to be trying to select from a NULL list of choices instead of the list of choices on the server.
It seems like you essentially have to recreate your selectizeInput when updating with a selected value beyond your initial list.
You can use maxOptions to render all values, might be enough depending on the size of your choices:
selectizeInput("listid", "A long list", choices = NULL, options = list(maxOptions = 10000))

hideTab doesn't work when tabsetPanel and hideTab are inside an observer in R shiny

I'm traying to create an app that reads some user and password and then create a tabsetPanel inside a renderUI.
The app is supposed to read a code and type number from a data base and if the type is 1 then hides some tabPanel, however all the tabpanels are always shown.
library(shiny)
library(RPostgreSQL)
con=dbConnect(........)
ui <- fluidPage(
textInput("user","User:"),
passwordInput("password", "Password:"),
actionButton("go", "Go",class = "btn-primary"),
uiOutput("panel")
)
server <- function(input, output, session) {
observeEvent({input$go}, {
code<-dbGetQuery(con,"SELECT type FROM table")[[1]]
#code is a number
if(dim(code)[1]==1){
type=reactive(dbGetQuery(con,"SELECT type FROM table2")[[1]])
#type() is a number
output$panel=renderUI(
tabsetPanel(id = "tab",
tabPanel("Tab1"),
tabPanel("Tab2")
)
)
observe({
if(type()==1){
hideTab(inputId = "tab", target = "Tab1")
}
})
}
})
}
shinyApp(ui, server)
The problem is that de observer is executed before the renderUI and doesn't re-execute, I think.
Generally speaking, you've mixed up 3 different processes.
Checking the user has access can be put in a separate function, outside of the scope of server, simply returning TRUE or FALSE (and possibly an error).
Dynamically loading the tabs. If this must only occur after the user has logged in, you can simply opt to not display Tab1. If the tabs has to be loaded regardless (but still dynamically), put it outside of the scope of observeEvent({input$go}, {...}). Consider, just for now, to setup the tabsetpanel with tabs in the ui.
Showing/hiding the tab.
Within a reactive/observe, you do not need to use additional reactives. They already are set to run. So type should be just be type = dbGetQuery(...), and the observe nested within an observe/observeEvent makes no sense.
Lastly, to debug why the tab is not hidden, use the good ol' fashioned print and look at your console. Try updating to
observe({
cat('Testing type: ', type(), '\n')
if(type()==1){
cat('Hiding tab...\n')
hideTab(inputId = "tab", target = "Tab1")
}
})
and watch out for those messages in your console. Are they printed? Then the fault might be on the client-side (perhaps you mispelled something). Are the messages missing? Then you know the code never executed, and you'll have to investigate why.
Update:
Looking further into the matters, try using the browsers Inspect-function. For the viewer in Rstudio (and Chrome), you can right-click and select "Inspect element". A new window appears (or is docked within the window), which allows you to inspect the HTML DOM and view the JavaScript console. Here, we notice an important message:
Uncaught There is no tabsetPanel (or navbarPage or navlistPanel) with id equal to 'tab'
Simply put, the hideTab command is sent before the client has finished loading the tabpanels.
One solution, that did not work, is as follows:
server <- function(input, output, session) {
type <- reactiveVal(0)
type_delayed <- debounce(type, Inf)
observeEvent({input$go}, {
code<-data.frame(code=1)
#code is a number
if(dim(code)[1]==1){
#type(dbGetQuery(con,"SELECT type FROM table2")[[1]])
type(1)
#type() is a number
output$panel=renderUI(
tabsetPanel(id = "tab",
tabPanel("Tab1"),
tabPanel("Tab2")
)
)
}
})
observe({
cat('Testing type: ', type_delayed(), '\n')
if( type_delayed() ==1){
cat('Hiding tab...\n')
hideTab(inputId = "tab", target = "Tab1")
}
})
}
I.e., we delay the execution of hiding the tab. Except it's a bad solution, because you have to choose a timing that is as soon as possible, but not so soon that the client isn't ready.
I suggest the following solution: Instead of hiding the panel, don't add it until you need it:
ui <- fluidPage(
textInput("user","User:"),
passwordInput("password", "Password:"),
actionButton("go", "Go",class = "btn-primary"),
actionButton("add", "Add tab"),
uiOutput("panel")
)
server <- function(input, output, session) {
i <- 1
observeEvent({input$go}, {
code<-data.frame(code=1)
#code is a number
if(dim(code)[1]==1){
#type <- dbGetQuery(con,"SELECT type FROM table2")[[1]]
type <- 1
#type() is a number
output$panel=renderUI({
if (type == 1) {
i <<- 1
tabsetPanel(
id = "tab",
tabPanel("Tab1")
)
} else {
i <<- 2
tabsetPanel(
id = "tab",
tabPanel("Tab1"),tabPanel("Tab2")
)
}
})
}
})
observeEvent(input$add, {
i <<- i + 1
appendTab('tab', tabPanel(paste0('Tab', i)))
})
}

Multiple inputs triggering same observeEvent not working for button in modalDialog

I have created a minimal code below to reproduce the problem I am facing in my app.
What I am trying to do is call the same observeEvent for multiple inputs where one of the actionButton for observeEvent is present in modalDialog which only gets created after function which is called within the observeEvent is called. The problem I am facing is that after adding this actionButton from modalDialog to call the observeEvent with multiple inputs the observeEvent is never called. If I remove this button the observeEvent is called. The following is my code:
library(shiny)
#Function called from shiny server
func <- function(input,output){
if(is.null(input$txt_Modal)){
output$txt <- renderText("No Text Entered Yet!")
showModal(modalDialog(title = "Choose Survival Time",
textInput(inputId = "txt_Modal", "Enter text:"),
easyClose = FALSE, footer = actionButton(inputId = "btn_Modal_OK","OK")))
}else{
output$txt <- renderText({input$txt_Modal})
}
}
##UI code
ui <- fluidPage(
actionButton(inputId = "btn", label = "Enter function and Print Value"),
textOutput(outputId = "txt")
)
##Server code
server <- function(input, output, session){
observeEvent({
input$btn
input$btn_Modal_OK
},{
func(input, output)
})
}
shinyApp(ui = ui, server = server)
Here input$btn_Modal_OK is the button that is created in the ModalDialog which is created within the function func. On removing input$btn_Modal_OK from the observeEvent the code works as expected.
The cause that I can think of why this is happening is because input$btn_Modal_OK is NULL at the start of the program. One method I can think to remove this error is to write a different observeEvent for input$btn_Modal_OK but my actual code has many lines of code within my observeEvent which I don't want to rewrite in another observeEvent and make the code bulky.
Please note that this is not what I am doing in my actual app, I have just written this code to reproduce the problem. Any help is highly appreciated!
The problem was that input$btn switched from NULL to 0 on initialisation and triggered the modal, but you want func only to be triggered when input$btn is actually pressed, that is when it has a value of 1 or above. That's why this solves your problem:
observeEvent(c(input$btn, input$btn_Modal_OK), {
validate(need(input$btn > 0, ''))
func(input, output)
})

In shiny's selectizeInput, is it imperative to click on a highligthed option to choose it?

I have an app with many selectizeInput fields that I would like the user to fill in quickly using the arrow keys to highlight an option from the drop-down menu, then press tab and have shiny record that value. It is annoying that in order to select a highlighted option from the drop-down menu of a selecizeInput field you have to click on it (or press Enter).
library(shiny)
ui <- bootstrapPage(
selectizeInput("sex", "sex", choices = c("m","f")),
selectizeInput("age", "age", choices = c("< 15",">15")),
selectizeInput("location", "location", choices = c("africa", "other"))
)
server <- function(input, output) {
}
shinyApp(ui = ui, server = server)

Resources