Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

downloadButton does not work on startup if hidden, downloads html of app instead, improve docs to clarify. #4191

Open
Roleren opened this issue Feb 25, 2025 · 8 comments · May be fixed by #4196

Comments

@Roleren
Copy link

Roleren commented Feb 25, 2025

Just spent 12 hours coding a single button of a shiny (modular) app and it turned out shiny disables my hidden buttons on default, why would it disable programmatic clicks on a hidden download button ?

Could we maybe add a note to docs of downloadButton {shiny} that simply states:

"Shiny by default disables programmatic clicks for hidden downloadButtons, use: output options(output, "my_button_id", suspendWhenHidden = FALSE) to suppress this behavior"

Will save future versions of myself in alternate universes a lot of time.
Thank you.

@Roleren Roleren changed the title downloadButtons does not work on startup if hidden, downloads html, improve docs to clarify. downloadButton does not work on startup if hidden, downloads html, improve docs to clarify. Feb 25, 2025
@Roleren Roleren changed the title downloadButton does not work on startup if hidden, downloads html, improve docs to clarify. downloadButton does not work on startup if hidden, downloads html of app instead, improve docs to clarify. Feb 25, 2025
@philibe
Copy link

philibe commented Feb 25, 2025

It's a normal behavior in HTML +css.

You have to activate the element and make it invisible with a display duration of 0 seconds.

library(shiny)
library(shinyjs)



ui <- fluidPage(
  useShinyjs(),
  tags$head(
    tags$style(type = "text/css", HTML(".texte_code {
                                       font-family: Courier New,Courier,Lucida Sans Typewriter,Lucida Typewriter,monospace; 
                                       font-weight: 400; 
                                       }")),
    tags$style(type = "text/css", HTML(".display_none {
                                       display: none; 
                                       }")),
    tags$style(type = "text/css", HTML(".display_inline {
                                       display: inline; 
                                       }")),    
    tags$style(type = "text/css", 
               HTML(paste0("
               .active_invisible { animation: animeHiddend 0s linear 0s 1 normal forwards;
                                  height: 0px;}

               ")
               )
    )
  ),
  h3("works :"),
  downloadButton("downloadData1", "downloadData1"),
  h3("doesn't work "),
  splitLayout(
    actionButton("BtndownloadData2",HTML("See downloadData2 <span class='texte_code'>display: none</span>")),
    div(id="divDownloadData2",
        downloadButton("downloadData2", "downloadData2"),
        class="display_none"
    ),
    cellWidths =c("25%","25%")
  ),
  h3("works :"),
  splitLayout(
    actionButton("BtndownloadData3",HTML("See downloadData3 <span class='texte_code'>.active_invisible</span>")),
    div(id="divDownloadData3",
        downloadButton("downloadData3", "downloadData3"),
        class="active_invisible"
    ),
    cellWidths =c("25%","25%")
  )
  
)


server <- function(input, output) {
  
  data <- mtcars
  
  output$downloadData1 <- downloadHandler(
    filename = function() {
      paste("data-", format(Sys.time(),'%Y-%m-%d_%H-%M-%S'), ".csv", sep="")
    },
    content = function(file) {
      write.csv(data, file)
    }
  )
  
  output$downloadData2 <- downloadHandler(
    filename = function() {
      paste("data-", format(Sys.time(),'%Y-%m-%d_%H-%M-%S'), ".csv", sep="")
    },
    content = function(file) {
      write.csv(data, file)
    }
  )
  
  output$downloadData3 <- downloadHandler(
    filename = function() {
      paste("data-", format(Sys.time(),'%Y-%m-%d_%H-%M-%S'), ".csv", sep="")
    },
    content = function(file) {
      write.csv(data, file)
    }
  )

  
  observeEvent(input$BtndownloadData2,{
    shinyjs::removeClass(id="divDownloadData2", class="display_none")
    shinyjs::addClass(id="divDownloadData2", class="display_inline")
    
  },ignoreInit=TRUE )
  
  observeEvent(input$BtndownloadData3,{
    shinyjs::removeClass(id="divDownloadData3", class="active_invisible")
    shinyjs::addClass(id="divDownloadData3", class="display_inline")
    
  },ignoreInit=TRUE )  
  

}

shinyApp(ui, server)

@Roleren
Copy link
Author

Roleren commented Feb 27, 2025

Yes, I guessed there were some tricks, but most of us using shiny are not front end developers, so a minimum of documentation would be great to at least be aware of these behaviors.

From my back-end background, it makes no sense that this is the default behavior. It breaks all the rules of documentation for hidden optimizations, which should always be clear to the reader of the documentation.

For normal back-end code things are clear, while Shiny has dragged me down in 20-hour rabbit holes more times than I can remember.

I just want to suggest a small snippet is added to the docs of downloadButton for future users, to avoid this one at least.

@philibe
Copy link

philibe commented Feb 27, 2025

Same for me : I learned this « normal behavior » at my beginning in R Shiny 7 years ago during some long hours.

This is not specific to downloadHandler().

For example it can happen for some elements behind unselected tabs (tabsetPanel( tabPanel(..), tabPanel(..))), and it can be more tricky than CSS.

Also for shinyBS::bsCollapse(... shinyBS::bsCollapsePanel()) etc.

But Shiny is simpler than others tools when you don't know UI: the learning curve is more easy. I learnt CSS+ web UI with Shiny.

@Roleren
Copy link
Author

Roleren commented Feb 28, 2025

To not make this thread stall, here are 3 options:

  1. Ignore us and just close the issue.
  2. Add docs yourself and close
  3. Let me make a pull-request with a suggestion for doc update

Which one do you prefer ?

@philibe
Copy link

philibe commented Feb 28, 2025

You have to wait for the Shiny team's response :)

PS: I'm a simple user.

@gadenbuie
Copy link
Member

I think that it's not obvious that outputOptions() would have an effect on downloadButton() and downloadLink() and I think a note with a link to outputOptions() would be useful. Relatedly, we should like to outputOptions() from other outputs as well. Thanks for the suggestion, I'll make a PR shortly.

@gadenbuie
Copy link
Member

Here's a smaller reprex showing how to use outputOptions():

library(shiny)
library(shinyjs)

ui <- fluidPage(
  useShinyjs(),
  h3("Hidden Download Button Issue #4191"),

  # Button to trigger the download, but not via the download button
  actionButton("do_download", "Do the Download"),

  # Hidden download button
  div(
    id = "download_div",
    downloadButton("download_data", "Download Data"),
    style = "display: none"
  )
)

server <- function(input, output) {
  output$download_data <- downloadHandler(
    filename = function() {
      paste("data-", format(Sys.time(), '%Y-%m-%d_%H-%M-%S'), ".csv", sep = "")
    },
    content = function(file) {
      write.csv(mtcars, file)
    }
  )

  outputOptions(output, "download_data", suspendWhenHidden = FALSE)

  observeEvent(input$do_download, {
    shinyjs::click("download_data")
  })
}

shinyApp(ui, server)

@gadenbuie
Copy link
Member

@Roleren I've proposed a documentation update in #4196. It's a little different than what you suggested for two reasons:

  • outputOptions() is used in the server function. I added links to outputOptions() for other server functions, so this is for consistency. If we documented this in the UI functions, I think it'd be unclear where to call outputOptions().
  • Technically we aren't disabling the button or preventing programmatic clicks on the button itself, but it's the download handler that's "detached" from the button (i.e. why you don't get the file you expect).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants