10

My shiny app produces some files that user can download. I have put downloadbutton in the ui for this purpose. However, when the page launches and before any calculation is done, there is nothing to download. I want to prevent user from downloading empty pages.

For this, I'm thinking to disable the downloadButton before the output is ready. But I don't know how to do that. I have found ways to disable ActionButton (such as ShinyBS package and other JS codes), but nothing for downloadButton.

Right now, I use validate() to throw errors if the output is not ready. However, when the downloadButton is clicked, a new empty web page opens with an error message in it which is ugly.

let me know what you think.

This is my ui code

 downloadButton('download', 'Download Lasso component matrix')),

and this is my server code :

  output$download_matrix <- downloadHandler(
      filename = function() { 
      validate(
      need(is.null(outputData())==FALSE, "No data to download yet")
      )
      paste('combined_model_matrix', '.txt', sep='') },
    content = function(file) {
      write.csv(outputData()$combinedAdjMtr, file)
})
DeanAttali
  • 25,268
  • 10
  • 92
  • 118
Elaheh kamaliha
  • 753
  • 2
  • 8
  • 17
  • 1
    Is the condition (ready to download) only visible to the server? If there is something you can detect on `ui.R`, you can try to wrap the `downloadButton` with a `conditionalPanel`. You can hide it should the condition do not satisfy. – Xin Yin Aug 11 '14 at 16:35
  • Or, is there anything else in the input other than the download button that will trigger the data processing? Because it seems that the data processing depends on some user input. I can give you an example if you can provide a little bit more of details. Is `outputData` a reactive value? – Xin Yin Aug 11 '14 at 16:46
  • You are right that a `conditionalPanel` is the easiest way to solve this problem, and I admit that I tend to use this shortcut sometimes. With bad feelings, because removing button that just cannot be pressed because of some special condition not met violates all rules of good interface design. I wish there were a `conditionalDisablePanel` in Shiny. As a workaround, I leave the button enabled and display an error message explaining why. – Dieter Menne Aug 11 '14 at 16:56
  • @DieterMenne, you can always write some Javascript to implement something like a `conditionalDisablePanel`. You can enhance your Shiny App siginificantly with Javascript actually. – Xin Yin Aug 11 '14 at 17:06
  • @DieterMenne: inspired by your `conditionalDisabledPanel` idea, here is one R snippet that does exactly that :) [gist:condDisabledPanel.R](https://gist.github.com/killkeeper/735422c65d639419ccb8) – Xin Yin Aug 11 '14 at 17:51
  • @XinYin yes data processing depends on the user input. User will upload some files and click an Action button to start the processing. The download button is in a tab set. – Elaheh kamaliha Aug 11 '14 at 18:25
  • I prefer not to hide the dowload tab, so that the user knows that there is a download option. – Elaheh kamaliha Aug 11 '14 at 18:26
  • @XinYin and yes, outputData is a reactive value. – Elaheh kamaliha Aug 11 '14 at 18:28
  • @Elahehkamaliha I've added an example as an answer you can probably adopt. It involves using some javascript to disable the download button. Let me know if it pertains to your application. – Xin Yin Aug 11 '14 at 18:54
  • @Xin Yin: I had used the Javascript version before, but it did not integrate nicely. So your `conditionalDisabledPanel` is highly welcome. I will forward it to the Shiny Team. – Dieter Menne Aug 12 '14 at 06:56

2 Answers2

24

Just adding another answer that works in a similar fashion to the one by Xin, but using a package (shinyjs) that natively supports enabling/disabling buttons, rather than having to deal with the messy javascript yourself. Using this package, you can simply call disable("download") or enable("download").

Here's a full example replicating the answer by Xin but with this package

library(shiny)
library(shinyjs)

runApp(shinyApp(
  ui = fluidPage(
    # need to make a call to useShinyjs() in order to use its functions in server
    shinyjs::useShinyjs(),  
    actionButton("start_proc", "Click to start processing data"),
    downloadButton("data_file")
  ),
  server = function(input, output) {
    observe({
      if (input$start_proc > 0) {
        Sys.sleep(1)
        # enable the download button
        shinyjs::enable("data_file")
        # change the html of the download button
        shinyjs::html("data_file",
                      sprintf("<i class='fa fa-download'></i>
                              Download (file size: %s)",
                              round(runif(1, 1, 10000))
                      )
        )
      }
    })

    output$data_file <- downloadHandler(
      filename = function() {
        paste('data-', Sys.Date(), '.csv', sep='')
      },
      content = function(file) {
        write.csv(data.frame(x=runif(5), y=rnorm(5)), file)
      }
    )

    # disable the downdload button on page load
    shinyjs::disable("data_file")
  }
))
r2evans
  • 141,215
  • 6
  • 77
  • 149
DeanAttali
  • 25,268
  • 10
  • 92
  • 118
  • Very nice. One question, though. What purpose is served by the call to `Sys.sleep(0)`? – Josh O'Brien Oct 17 '15 at 02:50
  • Whoops, I clearly meant to put a number in there.. I changed it to `1`. It's just a placeholder to show that time is passing, as if a data-crunching function is called – DeanAttali Oct 17 '15 at 04:36
  • 1
    @DeanAttali, i know it's late to ask but what if processing takes when calling `downloadHandler` ie. `parameters` passed to `content` in `output$data_file` calls some function and it's computation takes time.... any solution regarding this ? – parth Jun 20 '17 at 06:16
10

Based on your comment:

yes data processing depends on the user input. USer will upload some files and click anAction button to start the processing. The download button is in a tab set.

Let's say the action button is named input$start_proc.

In server.R:

shinyServer(function(input, output, session) {
   #... other code
   observe({
       if (input$start_proc > 0) {
           # crunch data...
           # when data is ready:
           session$sendCustomMessage("download_ready", list(...))
           # you can put extra information you want to send to the client 
           # in the ... part.
       } 
   })
   #... other code
})

Then in ui.R, you can write some javascript to handler the custom message event.


A full example is:

server.R

library(shiny)

fakeDataProcessing <- function(duration) {
  # does nothing but sleep for "duration" seconds while
  # pretending some background task is going on...
  Sys.sleep(duration)
}

shinyServer(function(input, output, session) {

  observe({
    if (input$start_proc > 0) {
      fakeDataProcessing(5)
      # notify the browser that the data is ready to download
      session$sendCustomMessage("download_ready", list(fileSize=floor(runif(1) * 10000)))
    }
  })

  output$data_file <- downloadHandler(
       filename = function() {
         paste('data-', Sys.Date(), '.csv', sep='')
       },
       content = function(file) {
         write.csv(data.frame(x=runif(5), y=rnorm(5)), file)
       }
  )
})

ui.R

library(shiny)

shinyUI(fluidPage(
  singleton(tags$head(HTML(
'
  <script type="text/javascript">
    $(document).ready(function() {
      // disable download at startup. data_file is the id of the downloadButton
      $("#data_file").attr("disabled", "true").attr("onclick", "return false;");

      Shiny.addCustomMessageHandler("download_ready", function(message) {
        $("#data_file").removeAttr("disabled").removeAttr("onclick").html(
          "<i class=\\"fa fa-download\\"></i>Download (file size: " + message.fileSize + ")");
      });
    })
  </script>
'
))),
  tabsetPanel(
    tabPanel('Data download example',
      actionButton("start_proc", h5("Click to start processing data")),
      hr(),

      downloadButton("data_file"),
      helpText("Download will be available once the processing is completed.")
    )
  )
))

In the example the data processing is faked by waiting for 5 seconds. Then the download button will be ready. I also added some "fake" fileSize information in the message to demonstrate that how you can send extra information to the user.

Note that because Shiny implements actionButton as <a> tag instead of <button>, and it binds click event on it. Therefore, in order to fully disable it, in addition to add a disabled attribute to make it appear to be disabled, you also need to override its click event by adding an inline onclick attribute. Otherwise the user can still accidentally click the (seemingly disabled) download button and triggers the download.

Xin Yin
  • 2,896
  • 21
  • 20