Disable selectInput and menu while shiny is busy - javascript

I would like to disable menu items and selectInput, while my Shiny app is loading. I have managed to disable buttons and textInput with some javacript (cf. Disable elements when Shiny is busy), but I can't get it to work with selectInput and menus. I'm not interested in alternatives solutions.
library(shiny)
js <- "$(document).on('shiny:busy', function() {
var $inputs = $('button,input,select,ul');
console.log($inputs);
$inputs.prop('disabled', true);
});
$(document).on('shiny:idle', function() {
var $inputs = $('button,input,select,ul');
console.log($inputs);
$inputs.prop('disabled', false);
});"
ui <- fluidPage(
titlePanel("Hello Shiny!"),
sidebarLayout(
sidebarPanel(
tags$head(tags$script(js)),
navlistPanel(
tabPanel("Component 1"),
tabPanel("Component 2")
)
),
mainPanel(
actionButton("buttonID","This adds 10 seconds of Sys.sleep"),
textInput("textID","Write text here..."),
selectInput("selectID","This should be disables while loading",choices=c("A","B","C"))
)
)
)
server <- function(input, output) {
observeEvent(input$buttonID,{
Sys.sleep(10)
})
}
shinyApp(ui, server)

Theres easier way of disabling widgets using shinyjs package. theres a reactiveValuesToList function which will collect all the reactivesinputs you have within the session and you can simply use that:
library(shiny)
library(shinyjs)
ui <- fluidPage(
titlePanel("Hello Shiny!"),
useShinyjs(),
sidebarLayout(
sidebarPanel(
navlistPanel(
tabPanel("Component 1"),
tabPanel("Component 2")
)
),
mainPanel(
actionButton("buttonID","This adds 5 seconds of Sys.sleep"),
textInput("textID","Write text here..."),
selectInput("selectID","This should be disables while loading",choices=c("A","B","C"))
)
)
)
server <- function(input, output) {
observeEvent(input$buttonID,{
myinputs <- names(reactiveValuesToList(input))
print(myinputs)
for(i in 1:length(myinputs)){
disable(myinputs[i])
}
Sys.sleep(5)
for(i in 1:length(myinputs)){
enable(myinputs[i])
}
})
}
shinyApp(ui, server)

ANSWER A)
The simple answer to your question is to set selectize = FALSE in your selectInput.
In the shiny docs, it's stated that the selectInput function uses the selectize.js JavaScript library by default (see below).
"By default, selectInput() and selectizeInput() use the
JavaScript library selectize.js
(https://github.com/brianreavis/selectize.js) to instead of the basic
select input element. To use the standard HTML select input element,
use selectInput() with selectize=FALSE."
By setting selectize = FALSE you are instead using the standard HTML select input element. This, in turn, is now picked up by your jquery var $inputs = $('button,input,select,ul');. I'm not sure why the element is not picked up when using the selectize.js library.
See the below example. Note that the selectInput options look different when using the html standard (not as nice aesthetically imo).
# ANSWER A)
library(shiny)
js <- "$(document).on('shiny:busy', function() {
var $inputs = $('button,input,select,ul');
console.log($inputs);
$inputs.prop('disabled', true);
});
$(document).on('shiny:idle', function() {
var $inputs = $('button,input,select,ul');
console.log($inputs);
$inputs.prop('disabled', false);
});"
ui <- fluidPage(
titlePanel("Hello Shiny!"),
sidebarLayout(
sidebarPanel(
tags$head(tags$script(js)),
navlistPanel(
tabPanel("Component 1"),
tabPanel("Component 2")
)
),
mainPanel(
actionButton("buttonID", "This adds 3 seconds of Sys.sleep"),
textInput("textID", "Write text here..."),
selectInput("selectID", "This should be disables while loading", choices=c("A","B","C"), selectize = FALSE)
)
)
)
server <- function(input, output) {
observeEvent(input$buttonID,{
Sys.sleep(3)
})
}
shinyApp(ui, server)
ANSWER B)
Below is how I got the selectize.js selectInput to disable when shiny is busy, as per your question, using the conditionalPanel. It's a bit of a hacky solution, but works well.
Note that I am creating two selectInputs which are similar. One is initialised as disabled using the shinyjs package. This is the one that is displayed when shiny is busy. Using jquery snippet $('html').hasClass('shiny-busy') we make use of the shiny-busy class applied when shiny is busy performing logic on the server side. When this class is removed (i.e. when shiny is idle) the conditionalPanel swaps in the other selectInput UI element that is not disabled.
# EXAMPLE B)
library(shiny)
library(shinyjs)
js <- "$(document).on('shiny:busy', function() {
var $inputs = $('button,input,select,ul');
console.log($inputs);
$inputs.prop('disabled', true);
});
$(document).on('shiny:idle', function() {
var $inputs = $('button,input,select,ul');
console.log($inputs);
$inputs.prop('disabled', false);
});"
ui <- fluidPage(
titlePanel("Hello Shiny!"),
sidebarLayout(
sidebarPanel(
tags$head(tags$script(js)),
navlistPanel(
tabPanel("Component 1"),
tabPanel("Component 2")
)
),
mainPanel(
useShinyjs(),
actionButton("buttonID", "This adds 3 seconds of Sys.sleep"),
textInput("textID", "Write text here..."),
#selectInput("selectID", "This should be disables while loading", choices=c("A","B","C"), selectize = FALSE)
div(
conditionalPanel(
condition="$('html').hasClass('shiny-busy')",
shinyjs::disabled(
selectInput(
inputId = 'selectID_disabled', # note the addition of "_disabled" as IDs need to be unique
label = "This should be disables while loading",
choices = "Loading...",
selected = NULL,
selectize = TRUE,
width = 250,
size = NULL
)
)
),
conditionalPanel(
condition="$('html').hasClass('')",
selectInput(
inputId = 'selectID',
label = "This should be disables while loading",
choices = c("A","B","C"),
selected = NULL,
selectize = TRUE,
width = 250,
size = NULL
)
),
style = "margin-top:20px;"
),
)
)
)
shinyServer(function(input, output, session) {
observeEvent(input$buttonID,{
Sys.sleep(3)
})
})
shinyApp(ui, server)

Related

How to hide a conditional panel using js in R Shiny when any action or other button is clicked other than specified inputs?

I am trying to hide the conditional panel illustrated below when there is any user input other than the user clicking on the action button "Delete" or making a selection in the selectInput() function rendered in the conditional panel, as shown in the below image. Other user inputs will be added (action buttons, radio buttons, selectInputs, etc.) so it isn't feasible to list each action that causes the conditional panel to hide. That conditional panel should always render upon the click of "Delete". Any suggestions for how to do this? Code is shown at the bottom.
Code:
library(rhandsontable)
library(shiny)
mydata <- data.frame('Col 1' = c(1,24,0,1), check.names = FALSE)
rownames(mydata) <- c('Term A','Term B','Term C','Term D')
ui <- fluidPage(br(),
rHandsontableOutput("mytable"),br(),
fluidRow(
column(1,actionButton("addCol", "Add",width = '70px')),
column(1,actionButton("delCol","Delete",width = '70px')),
column(3,conditionalPanel(condition = "input.delCol",uiOutput("delCol"))) # js here
)
)
server <- function(input, output) {
output$mytable = renderRHandsontable(df())
df <- eventReactive(input$addCol, {
if(input$addCol > 0){
newcol <- data.frame(mydata[,1])
names(newcol) <- paste("Col",ncol(mydata)+1)
mydata <<- cbind(mydata, newcol)
}
rhandsontable(mydata,rowHeaderWidth = 100, useTypes = TRUE)
}, ignoreNULL = FALSE)
observeEvent(input$delCol,
{output$delCol<-renderUI(selectInput("delCol",label=NULL,choices=colnames(mydata),selected="Col 1"))}
)
}
shinyApp(ui,server)
Per MikeĀ“s comment I started with shinyjs and this simple example from the shinyjs reference manual, with minor modification for 2 buttons:
library(shiny)
library(shinyjs)
ui = fluidPage(
useShinyjs(), # Set up shinyjs
actionButton("btn", "Click to show"),
actionButton("btn1","Click to hide"),
hidden(p(id = "element", "I was invisible"))
)
server = function(input, output) {
observeEvent(input$btn, {show("element")})
observeEvent(input$btn1,{hide("element")})
}
shinyApp(ui,server)
Then I expanded it to my code in the OP per the below. Note that this will still require an observeEvent() for each user action that triggers selectInput() to hide instead of a global hide every time there's any user input other than a click of "Delete" action button. I'm not sure this is possible but will continue researching. Multiple observeEvents() won't be too bad of an option in any case.
Resolving code:
library(rhandsontable)
library(shiny)
library(shinyjs)
mydata <- data.frame('Col 1' = c(1,24,0,1), check.names = FALSE)
rownames(mydata) <- c('Term A','Term B','Term C','Term D')
ui <- fluidPage(
useShinyjs(), # set up shinyjs
br(),
rHandsontableOutput("mytable"),br(),
fluidRow(
column(1,actionButton("addCol", "Add",width = '70px')),
column(1,actionButton("delCol","Delete",width = '70px')),
column(3, hidden(uiOutput("delCol2")))) # hide selectInput()
)
server <- function(input,output,session){
output$mytable = renderRHandsontable(dat())
dat <- eventReactive(input$addCol, {
if(input$addCol > 0){
newcol <- data.frame(mydata[,1])
names(newcol) <- paste("Col",ncol(mydata)+1)
mydata <<- cbind(mydata, newcol)
}
rhandsontable(mydata,rowHeaderWidth = 100, useTypes = TRUE)
}, ignoreNULL = FALSE)
observeEvent(input$delCol, show("delCol2")) # clicking Delete button reveals selectInput()
observeEvent(input$addCol, hide("delCol2")) # clicking Add hides selectInput()
output$delCol2 <-renderUI({
selectInput(
"delCol3",
label=NULL,
choices=colnames(mydata),
selected="Col 1"
)
})
}
shinyApp(ui,server)

Set an alert when all boxes are closed or collapsed in shinydashboardPlus

I have a shinydashboard with 10 boxes, and which can be closed or collapsed. All the box id starts with "box". I was trying set an alert button when all boxes are closed or collapsed.
Below is the code of the dashboard:
library(shiny)
library(shinydashboardPlus)
box_create <- function(i){
shinydashboardPlus::box(tags$p(paste0("Box",i)),
id = paste0("box", i),
closable = TRUE,
collapsible = TRUE)
}
all_box <- purrr::map(1:10, ~box_create(.x))
ui <- dashboardPage(
dashboardHeader(),
dashboardSidebar(),
dashboardBody(
all_box
)
)
server <- function(input, output, session) {
}
shinyApp(ui, server)
I have noticed on pressing close or collapse button shiny sets the display = 'none' for that box.
So is it possible to extract all styles associated with the id which starts with 'box' and check if all the style property sets as 'none' using jQuery?
Using shinydashboardPlus's dev version you can access the box state via it's id (pattern: input$mybox$collapsed).
Please see this related article.
Install it via devtools::install_github("RinteRface/shinydashboardPlus")
library(shiny)
library(tools)
library(shinydashboard)
library(shinydashboardPlus)
box_ids <- paste0("box", 1:10)
box_create <- function(box_id){
shinydashboardPlus::box(tags$p(toTitleCase(box_id)),
id = box_id,
closable = TRUE,
collapsible = TRUE)
}
all_boxes <- lapply(box_ids, box_create)
ui <- dashboardPage(
dashboardHeader(),
dashboardSidebar(),
dashboardBody(
all_boxes
)
)
server <- function(input, output, session) {
observe({
if(all(sapply(box_ids, function(box_id){input[[box_id]]$collapsed}))){
showModal(modalDialog("All boxes are collapsed!"))
}
})
# observe({
# print(paste("Is box1 collapsed?", input$box1$collapsed))
# })
}
shinyApp(ui, server)
I thought you wanted a button when all were collapsed OR all were closed. Here is a solution for that.
library(shiny)
library(shinydashboard)
library(shinydashboardPlus)
box_create <- function(i){
shinydashboardPlus::box(tags$p(paste0("Box",i)),
id = paste0("box", i),
closable = TRUE,
collapsible = TRUE)
}
all_box <- purrr::map(1:10, ~box_create(.x))
ui <- dashboardPage(
dashboardHeader(),
dashboardSidebar(),
dashboardBody(
all_box,
div(id = "alert_button")
)
)
server <- function(input, output, session) {
observe({
boxes <- paste0("box", 1:10)
visible_status <- sapply(boxes, \(x) input[[x]]$visible)
collapsed_status <- sapply(boxes, \(x) input[[x]]$collapsed)
if (!any(visible_status) || all(collapsed_status)) {
insertUI(
selector = "#alert_button",
ui = div(id = "added", actionButton("btn","all hidden OR all collapsed"))
)
} else {
removeUI(selector = "#added")
}
})
}
shinyApp(ui, server)

Attach javascript listener when using `renderUI` in Shiny app

I am ultimately trying to capture the time it takes a user to click on a series of histograms after they are displayed. However, in the example app below, a javascript error appears at the loading of the app:
Uncaught TypeError: Cannot set properties of null (setting 'onclick')
at HTMLDocument. ((index):31:30)
at e (jquery.min.js:2:30038)
at t (jquery.min.js:2:30340)
Presumably this is because document.getElementById("img") doesn't find img at the loading of the app, but I don't know how to resolve that.
I can get this to work when the histogram is displayed outside of the renderUI, but I need to change the histogram dynamically from the server, so I need this to work with a rendered UI.
shinyApp(
ui = fluidPage(
tags$script('
// ------ javascript code ------
$(document).ready(function(){
// function to set Shiny input value to current time:
const clockEvent = function(inputName){Shiny.setInputValue(inputName, new Date().getTime())}
// trigger when the value of output id "img" changes:
$(document).on("shiny:value",
function(event){
if (event.target.id === "img") {clockEvent("displayed_at")}
}
)
// trigger when the image, after being sent or refreshed, is clicked:
document.getElementById("img")
.onclick = function(){clockEvent("reacted_at")}
})
// ------------------------------
'),
sidebarLayout(
sidebarPanel(
actionButton(inputId="render_dynamic", label= "Create Dynamic UI")
),
mainPanel(
uiOutput("dynamic")
)
)
),
server = function(input, output) {
output$img <- renderImage({
outfile <- tempfile(fileext='.png')
png(outfile, width=400, height=400)
hist(rnorm(100))
dev.off()
list(src = outfile,
contentType = "image/jpeg")
},
deleteFile = FALSE)
output$reaction_time <- renderPrint(paste('reaction time (ms)', input$reacted_at - input$displayed_at))
output$dynamic <- renderUI({
req(input$render_dynamic > 0)
div(id = 'image_container',
imageOutput("img", click = "photo_click"),
textOutput("reaction_time"))
})
}
)
Here is an approach avoiding renderUI and using bindEvent:
library(shiny)
ui = fluidPage(
tags$script('
// ------ javascript code ------
$(document).ready(function(){
// function to set Shiny input value to current time:
const clockEvent = function(inputName){Shiny.setInputValue(inputName, new Date().getTime())}
// trigger when the value of output id "img" changes:
$(document).on("shiny:value",
function(event){
if (event.target.id === "img") {clockEvent("displayed_at")}
}
)
// trigger when the image, after being sent or refreshed, is clicked:
document.getElementById("img")
.onclick = function(){clockEvent("reacted_at")}
})
// ------------------------------
'),
sidebarLayout(
sidebarPanel(
actionButton(inputId="render_dynamic", label= "Create Dynamic UI")
),
mainPanel(
imageOutput("img"),
textOutput("reaction_time")
)
)
)
server = function(input, output, session) {
output$img <- renderImage({
outfile <- tempfile(fileext='.png')
png(outfile, width=400, height=400)
hist(rnorm(100))
dev.off()
list(src = outfile,
contentType = "image/jpeg")
}, deleteFile = FALSE) |> bindEvent(input$render_dynamic)
output$reaction_time <- renderPrint({
paste('reaction time (ms)', input$reacted_at - input$displayed_at)
}) |> bindEvent(input$reacted_at)
}
shinyApp(ui, server)
I don't know if there is a good reason for you to output the plot as an image and show it via renderImage instead of using renderPlot directly - but here is the renderPlot version:
library(shiny)
ui = fluidPage(
tags$script('
// ------ javascript code ------
$(document).ready(function(){
// function to set Shiny input value to current time:
const clockEvent = function(inputName){Shiny.setInputValue(inputName, new Date().getTime())}
// trigger when the value of output id "img" changes:
$(document).on("shiny:value",
function(event){
if (event.target.id === "img") {clockEvent("displayed_at")}
}
)
// trigger when the image, after being sent or refreshed, is clicked:
document.getElementById("img")
.onclick = function(){clockEvent("reacted_at")}
})
// ------------------------------
'),
sidebarLayout(
sidebarPanel(
actionButton(inputId="render_dynamic", label= "Create Dynamic UI")
),
mainPanel(
plotOutput("img"),
textOutput("reaction_time")
)
)
)
server = function(input, output, session) {
output$img <- renderPlot({
hist(rnorm(100))
}) |> bindEvent(input$render_dynamic)
output$reaction_time <- renderPrint({
paste('reaction time (ms)', input$reacted_at - input$displayed_at)
}) |> bindEvent(input$reacted_at)
}
shinyApp(ui, server)
PS: If you are still interested in how to solve the renderUI problem please check the following post on GitHub.
Seems like, from the client's perspective, the document is fully loaded before you renderUI another element. So the JQuery $(document).ready(...) gives its OK to proceed with trying to attach an event to an element which is not there (yet).
Options to avoid renderUI have already been given. If you don't want the "placeholder" blank space, you can set the image height to zero upon rendering:
ui <- fluidPage(
## ...
imageOutput("img", click = "photo_click",
height = 0
)
## ...

Embedded inputs in R Shiny Datatable - javascript issue

I have an R/Shiny app containing a datatable from the DT package.
Following this thread render dropdown for single column in DT shiny, I've been able to embed action buttons into a column in my datatable, which trigger a set of corresponding observers.
However, when the datatable is paginated, my action buttons will only function correctly for those buttons on the first page. Buttons on subsequent pages don't work. This remains the case even if I reorder the data using column sorting, as any buttons which were on page 2+ as of the initial render will not work even if they are reordered onto page 1.
I expect the problem is in how the callback argument is using javascript (which is unfortunately over my head) to render the action buttons correctly. Can anyone advise how to get the action buttons working on subsequent pages?
Here is my minimal reprex, using mtcars data:
library(shiny)
library(DT)
ui <- fluidPage(
titlePanel("reprex1")
,fluidRow(
dataTableOutput("dt1")
)
)
server <- function(input, output) {
output$dt1 <- renderDataTable({
mtlocal <- mtcars
for(n in 1:nrow(mtlocal)){
mtlocal$actionbutton[[n]] <- as.character(
actionButton(
paste0("buttonpress",n), label = paste0("buttonpress",n)
)
)
}
datatable(
mtlocal
,escape = FALSE
,selection = "none"
,callback = JS("table.rows().every(function(i, tab, row) {
var $this = $(this.node());
$this.attr('id', this.data()[0]);
$this.addClass('shiny-input-container');
});
Shiny.unbindAll(table.table().node());
Shiny.bindAll(table.table().node());")
)
}, server = FALSE)
lapply(
1:nrow(mtcars),function(x){
observeEvent(
input[[paste0("buttonpress",x)]],{
showModal(
modalDialog(
h2(paste0("You clicked on button ",x,"!"))
)
)
}
)
}
)
}
# Run the application
shinyApp(ui = ui, server = server)
The callback is executed only once, and then Shiny.bind/unbind is lost when the table is redrawn. You have to use the options preDrawCallback and drawCallback:
datatable(
mtlocal
, escape = FALSE
, selection = "none"
, options = list(
preDrawCallback = JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = JS('function() { Shiny.bindAll(this.api().table().node()); } ')
)
)

update column visibility in DT::datatable while using DT::replaceData

Is there a way (other than a 'colvis' button) to dynamically update which columns are visible in a DT::datatabe while using the DT::replaceData to update the table?
(The reason I can not use the 'colvis' button (as shown here:https://rstudio.github.io/DT/extensions.html) is I need to have a few different short cut convenient buttons that hide and show multiple complex patterns at once.)
This is an example of how I would initiate my app. Is there a way in js or the server-side to hide and show columns? Thanks!
library(shiny)
library(DT)
shinyApp(
ui = fluidPage(
fluidRow(
column(2, actionButton('refresh', 'Refresh Data', icon = icon('refresh'))),
column(10, DT::dataTableOutput('foo'))
)
),
server = function(input, output, session) {
df = iris
n = nrow(df)
df$ID = seq_len(n)
loopData = reactive({
input$refresh
df$ID <<- c(df$ID[n], df$ID[-n])
df
})
output$foo = DT::renderDataTable(isolate(DT::datatable(loopData(),
options = list(
columnDefs = list(list(visible=FALSE,targets=c(0,1,2))))
)))
proxy = dataTableProxy('foo')
observe({
replaceData(proxy, loopData(), resetPaging = FALSE)
})
}
)

Categories