2

Inside a Shiny App I want to disable all buttons while the app is running. I have a lot of action buttons, dependencies and some renderui stuff, so that I think using shinyjs:disable(button) is crucial and very unclean over 40 and more buttons.

Is there an easy way to disable a button (or all buttons/sliders at once) while the shiny app is busy, like in the condition of the "loading.." element of my example app below?

or is there another way to disable all buttons from being clicked or make them invisible while long computations are running indicated by the "loading.." text?

In my example below I want to disable the action button while the app is busy (the "loading.." text is shown). I know for this example I could use shinyjs but I would prefer an overall solution while the app is busy. Any help is really welcome, I am completely new to html,css and java stuff so if somebody knows a solution to this, a short explanation would be really great!

Many thanks in advance!

library(shiny)


server <- function(input, output) {
  output$moreControls <- renderUI({if(input$obs!=10001) actionButton("button", "OK!")})
  observeEvent(input$button, {
  output$distPlot <- renderPlot({
    Sys.sleep(5)
    hist(rnorm(isolate(input$obs)), col = 'darkgray', border = 'white')
  })})
}

ui <- fluidPage(tags$head(tags$style(type="text/css", "
                                                                    #loadmessage {
                                     position: fixed;
                                     top: 95%;
                                     left: 0px;
                                     width: 100%;
                                     padding: 5px 0px 5px 0px;
                                     text-align: center;
                                     font-weight: bold;
                                     font-size: 100%;
                                     color: #000000;
                                     background-color: #CCFF66;
                                     z-index: 105;
                                     }
                                     ")),
                conditionalPanel(condition="$('html').hasClass('shiny-busy')",
                                 tags$div("Loading...",id="loadmessage")),
  sidebarLayout( sidebarPanel(

    sliderInput("obs", "Number of observations:", min = 10000, max = 100000, value = 10001,step=1000),
   uiOutput("moreControls")

    ),
    mainPanel(plotOutput("distPlot"))
  )
)

shinyApp(ui = ui, server = server) 
JmO
  • 572
  • 1
  • 4
  • 20

1 Answers1

8

I am not familiar with a simple way to do what you are describing, but of course that does not mean there is none ;) Here is a little workaround that I believe matches your requirements, and keeps your code relatively clean. We can use reactiveValuesToList(input) to get a list of our inputs, and then write a function that disables or enables them all. We can also decide to only toggle button inputs by subsetting the list based on attributes.

Working example below, hope this helps!


enter image description here


library(shiny)
library(shinyjs)

ui <- fluidPage(
  h3('Disable all inputs while running'),
  actionButton('btn_all_inputs','Run long process'),
  h3('Disable only buttons while running'),
  actionButton('btn_only_buttons','Run long process'),
  hr(),
  h3('Inputs'),
  textInput('text1', 'Text1',"my text:"),
  actionButton('btn1','Button 1'),
  actionButton('btn2','Button 2'),
  actionButton('btn3','Button 3'),
  sliderInput('slid3','Slider 1',min=0,max=1,value=0.5),
  useShinyjs()
)


server <- function(input, output, session){

  # Function to toggle input elements. 
  # input_list: List of inputs, reactiveValuesToList(input)
  # enable_inputs: Enable or disable inputs?
  # Only buttons: Toggle all inputs, or only buttons?
  toggle_inputs <- function(input_list,enable_inputs=T,only_buttons=FALSE)
  {
    # Subset if only_buttons is TRUE.
    if(only_buttons){
      buttons <- which(sapply(input_list,function(x) {any(grepl('Button',attr(x,"class")))}))
      input_list = input_list[buttons]
    }

    # Toggle elements
    for(x in names(input_list))
      if(enable_inputs){
        shinyjs::enable(x)} else {
          shinyjs::disable(x) }
  }

  observeEvent(input$btn_all_inputs,{
    input_list <- reactiveValuesToList(input)
    toggle_inputs(input_list,F,F)
    Sys.sleep(5)
    toggle_inputs(input_list,T,F)
  })

  observeEvent(input$btn_only_buttons,{
    input_list <- reactiveValuesToList(input)
    toggle_inputs(input_list,F,T)
    Sys.sleep(5)
    toggle_inputs(input_list,T,T)
  })
}

shinyApp(ui = ui, server = server)

Alternative solution

This solution uses custom JavaScript to enable/disable all inputs based on if Shiny is busy or idle. This will thus disable your inputs anytime Shiny is busy. I now set the script to disable all buttons, but you can easily extend it by adding more selections to document.getElementsByTagName(). Hope this comes closer to what you had in mind.

library(shiny)

ui <- fluidPage(
  h3('Disable buttons while running'),
  actionButton('btn_run','Run long process'),
  hr(),
  h3('Inputs'),
  textInput('text1', 'Text1',"my text:"),
  actionButton('btn1','Button 1'),
  sliderInput('slid3','Slider 1',min=0,max=1,value=0.5),
  includeScript('script.js')
)

server <- function(input, output, session){

  observeEvent(input$btn_run,{
    Sys.sleep(5)
  })
}

shinyApp(ui = ui, server = server)

script.js

$(document).on("shiny:busy", function() {
  var inputs = document.getElementsByTagName("button");
  console.log(inputs);
for (var i = 0; i < inputs.length; i++) {
inputs[i].disabled = true;
}
});

$(document).on("shiny:idle", function() {
  var inputs = document.getElementsByTagName("button");
  console.log(inputs);
for (var i = 0; i < inputs.length; i++) {
inputs[i].disabled = false;
}
});
Florian
  • 24,425
  • 4
  • 49
  • 80
  • Thank you this is a nice solution. But I will need to write the toogle_inputs code in every observeEvent part, right? As I have lots of them with lots and lots of long run processes it would be great to have a solution that is based on the "shiny busy" part like the "loading.." text. But nevertheless this helped me a lot! And if there is no html based solution I will use this for my app. Thanks again! – JmO Feb 18 '18 at 15:36
  • 1
    @JmO, I updated my answer with an alternative solution. In this case, you do not have to include code in all observeEvents, but it loses a bit of flexibility; it will disable buttons/inputs any time the app is busy. Anyway, hope this is useful. Disclaimer; I am by no means a JavaScript expert so there may still be room for improvement ;) – Florian Feb 18 '18 at 18:53
  • Hey Florian! Nice piece of code! I just added it to my shiny App and so far i'm really happy with how it disables buttons while my plotly plots are rendering. One follow up, I would like to perhaps also trigger a sweetalert on the busy signal, and trigger closing of the sweetalert on the "not busy anymore signal' Would you have an idea how to do that? I can post a separate question if you feel it's worth it – Mark Sep 13 '18 at 10:29
  • ps, the javascript does not disable other elements such as sliders and dropdown. I thought to be clever and change 'button' into 'input' but that didn't work. I'll try to play with it a bit more to also disable other elements – Mark Sep 13 '18 at 10:35
  • @Mark Feels big enough to post it as a separate question :) I am not all too familiar in JS myself so I would also have to play around with it a bit! – Florian Sep 13 '18 at 11:24