shiny.fluent icon indicating copy to clipboard operation
shiny.fluent copied to clipboard

Download links and downloadHandlers do not work from inside a PivotItem

Open justacec opened this issue 1 year ago • 3 comments

Code example

library(shiny)
library(shiny.fluent)
library(shinyjs)

ui <- fluentPage(
  useShinyjs(),

  # This one is outside the Pivot and will likely work correctly
  div(
    downloadButton("download_outside", label = "")
  ),

  Pivot(
    PivotItem(id = 'pivot1', headerText = 'One', alwaysRender = TRUE, tagList(
      # This one is inside the Pivot and will not work correctly
      div(
        downloadButton("download_inside", label = "")
      )
    ))
  )
)

server <- function(input, output, session) {  
  output$download_inside <- downloadHandler(
    filename = function() {
      paste("data-", Sys.Date(), ".csv", sep="")
    },
    content = function(file) {
      write.csv(iris, file)
    }
  )

  output$download_outside <- downloadHandler(
    filename = function() {
      paste("data-", Sys.Date(), ".csv", sep="")
    },
    content = function(file) {
      write.csv(iris, file)
    }
  )
}

shiny::runApp(shinyApp(ui, server), launch.browser = FALSE)

Bug description

I was following your example to enable download links in shiny.fluent as presented in #39. While that example works, I found that the downloadHandler was not updating the href target in the link, if it was inside a Pivot structure. In this case, the link just opens a new version of the application in a new tab, vice presenting the user with a download file dialog.

The example code shows both cases simultaneously for side-by-side comparison.

The provided code example here strips out your PrimaryButton and its callback because the issue seems to be the way that the downloadHandler updates the href on the typically hidden downloadButton and I wanted to focus only on that. I have also removed the hidden aspect so that the mouseover link pops up for easy reference

Expected behavior

The href of the shiny downloadButton would be correctly set

Comments

As an aside, I have also attempted to set the output options to not suspend when hidden, just in case that was an issue. Changing that had no effect.

justacec avatar Jun 08 '23 11:06 justacec

My suspicion on this is that the downloadButton is hidden inside the react_data structure which is rendered after the page has been downloaded after the server function is executed. When the server function executes, there is no button in the DOM for the id selector to catch it, and therefore the href is not updated to the correction value.

A fix for this would be some sort of callback to be executed after the data is rendered, but not sure how that would work.

A secondary deeper dirtier fix for this would be to have a hidden download button on the main page with reactive values for the filename and the data that can be updated by later nested modules. The reactive values would just need to be passed to each of the follow-on modules.

I am not sure if this is the issue, what are your thoughts?

justacec avatar Jun 08 '23 12:06 justacec

I have a potential working solution which honors keeping all of the relevant code in the same file (for modularity) and it supports nested modules cleanly. The idea is to inject a static hidden download button from the server function right after the body tag which means that it completly circumvents the React system. Then, when the downloadHandler is called, the DOM contains the element.

library(shiny)
library(shiny.fluent)
library(shinyjs)

ui <- fluentPage(
  useShinyjs(),

  # This one is outside the Pivot and will likely work correctly
  div(
    downloadButton("download_outside", label = "")
  ),

  Pivot(
    PivotItem(id = 'pivot1', headerText = 'One', alwaysRender = TRUE, tagList(
      # This one is inside the Pivot and will not work correctly
      div(
        PrimaryButton.shinyInput('downloadButton', text = 'Download', iconProps = list(iconName = "Download"))      )
    ))
  )
)

server <- function(input, output, session) {
  insertUI('body', 'afterBegin', session = session, ui = tagList(
      div(
      style = 'visibility: hidden; display: none',
      downloadButton('download_inside', label = 'Download')
      )
  ))

  observeEvent(input$downloadButton, {
    click('download_inside')
  })

  output$download_inside <- downloadHandler(
    filename = function() {
      paste("data-", Sys.Date(), ".csv", sep="")
    },
    content = function(file) {
      write.csv(iris, file)
    }
  )

  output$download_outside <- downloadHandler(
    filename = function() {
      paste("data-", Sys.Date(), ".csv", sep="")
    },
    content = function(file) {
      write.csv(iris, file)
    }
  )

  outputOptions(output, 'download_inside', suspendWhenHidden = FALSE)

}

shiny::runApp(shinyApp(ui, server), launch.browser = FALSE)

justacec avatar Jun 08 '23 13:06 justacec

Thanks @justacec for taking time to submit such a detailed description of the issue.

The issue boils down to downloadHandler not working in reactContext, we'll use this example as a starting point to fixing the issue:

library(shiny)
library(shiny.fluent)
library(shinyjs)

ui <- fluentPage(
  useShinyjs(),
  # This one is outside the Pivot and will likely work correctly
  downloadButton("download_outside", label = ""),
  shiny.react:::ReactContext(
    # This one is inside the Pivot and will not work correctly
    downloadButton("download_inside", label = "")
  )
)

server <- function(input, output, session) {  
  output$download_inside <- downloadHandler(
    filename = function() {
      paste("data-", Sys.Date(), ".csv", sep="")
    },
    content = function(file) {
      write.csv(iris, file)
    }
  )

  output$download_outside <- downloadHandler(
    filename = function() {
      paste("data-", Sys.Date(), ".csv", sep="")
    },
    content = function(file) {
      write.csv(iris, file)
    }
  )
}

shiny::runApp(shinyApp(ui, server))

jakubsob avatar Aug 28 '23 13:08 jakubsob