1

Background

I want to pass a dataset (iris in the below reprex) between modules. Once passed, I want to click a button to download the dataset. I'm not able to download the dataset. It's part of a larger app, so I want to keep things as consistent as possible in the reprex. For instance, I want to keep the use of bs4Dash, but also I want to keep the file structure the same. This applies also to the use of boxDropdown() and boxDropdownItem(). As per their documentation here if I pass an id argument, it will behave as an actionButton(), therefore I don't use an explicit actionButton() for the downloadHandler(). In the reprex below, I added print statements to track and verify the process. I had tried returning a value from the module server (for instance seen here) but that didn't work. So I created this reprex to help debug this.

File structure

app/
├── global.R
├── server.R
├── ui.R
└── modules/

app/modules
├── first_module.R
└── second_module.R

Reprex

To test the reprex you run the global.R. I am trying to solve the issue of the data not being downloaded.

global.R
# Load necessary libraries
require(shiny)
require(bs4Dash)

# Source the modules
source(file = "modules/second_module.R", local = TRUE)
source(file = "modules/first_module.R", local = TRUE)
server.R
# Define the server for the Shiny app
## This isn't necessarily needed with the use of moduleServer()
## Included here in case the file is needed in the codebase
server <- function(input, output, session) {
  
  # Call the second module's server with the iris dataset
  secondModuleServer(id = "dataDownload", dataset = iris)
  
  # Call the first module's server
  firstModuleServer(id = "firstModule")
  
}
ui.R
# Define the UI for the Shiny app
ui <- bs4DashPage(
  header = bs4DashNavbar(),
  sidebar = bs4DashSidebar(
    sidebarMenu(
      menuItem("First Module", tabName = "firstModule", icon = icon("home"))
    )
  ),
  controlbar = bs4DashControlbar(),
  footer = bs4DashFooter(),
  title = "Minimal Viable Shiny App",
  body = bs4DashBody(
    tabItems(
      tabItem(
        tabName = "firstModule",
        firstModuleUI(id = "firstModule", tabName = "firstModuleTab")
      )
    )
  )
)
first_module.R
#' A Shiny Module: Pass downloadable dataset to another module
#' @title Modularized Downloading
#' @description This module will pass iris to the other module where it should download

# Source the second module
source("modules/second_module.R", local = TRUE)

# Define the first module's UI
firstModuleUI <- function(id, tabName) {
  ns <- NS(id)
  tabItem(tabName = tabName,
          tabPanel("First Module",
                   box(title = "Reprex: Modularized Download",
                       dropdownMenu = secondModuleUI(id = ns("dataDownload"))
                   )
          )
  )
}

# Define the first module's server
firstModuleServer <- function(id) {
  moduleServer(id, function(input, output, session) {
    # Pass the iris dataset to the second module's server
    secondModuleServer(id = "dataDownload", dataset = iris)
  })
}
second_module.R
# A Shiny Module - To Download/Export Data In Different File Types
#' @title Download/Export User Updated Data
#' @description Users can download/export data as CSV


# Define the second module's UI
secondModuleUI <- function(id) {
  ns <- NS(id)
  boxDropdown(
    boxDropdownItem("CSV", id = ns("csvdownload"), icon = icon("file-csv")),
    icon = icon("download")
  )
}

secondModuleServer <- function(id, dataset) {
  moduleServer(id, function(input, output, session) {
    ns <- session$ns
    print(head(dataset)) # check if the data being received
    # Trigger the downloadHandler when the download_button is clicked
    observeEvent(input$csvdownload, {
      print("debug: button works")
      output$csvdownload <- downloadHandler(
        filename = function() {
          paste("iris.csv", sep = "")
        },
        content = function(file) {
          write.csv(dataset, file, row.names = FALSE)
          print("debug: download processed")
        }
      )
    })
    
  })
}

Thanks for your help.

  • You can't declare output renderers inside of observers. Have you tried removing the `observeEvent()` call completely and instead writing `output$csvdownload <- downloadHandler(...)` in the module server body directly? – AlexR Apr 30 '23 at 10:41
  • Okay, I separated those two components. I still want to trigger the download when the `boxDropdownItem()` is clicked. So I modified it based on [this post here](https://community.rstudio.com/t/simulating-a-click-when-using-shinys-downloadhandler/15413/3), i.e. creating a hidden downloadButton. But it still doesn't work – s-mabdurrazak Apr 30 '23 at 16:17

1 Answers1

1

Perhaps you are looking for this (no change in the remaining code).

# Define the second module's UI
secondModuleUI <- function(id) {
  ns <- NS(id)
  boxDropdown(
    boxDropdownItem("", uiOutput(ns("csvdownload")) ),
    icon = icon("download")
  )
}

secondModuleServer <- function(id, dataset) {
  moduleServer(id, function(input, output, session) {
    ns <- session$ns
    print(head(dataset)) # check if the data being received
    # Trigger the downloadHandler when the download_button is clicked
    observeEvent(input$csvdownload, {
      print("debug: button works")
    })
    
    output$csvdownload <- renderUI({
      downloadBttn(ns("saveCSV"),
                   HTML("CSV"),
                   style = "fill",
                   color = "default",
                   size = "md",
                   block = TRUE,
                   no_outline = TRUE
      )
    })
    
    output$saveCSV <- downloadHandler(
      filename = function() {
        paste("iris.csv", sep = "")
      },
      content = function(file) {
        write.csv(dataset, file, row.names = FALSE)
        print("debug: download processed")
      }
    )
    
  })
}

# Define the UI for the Shiny app
ui <- bs4DashPage(
  header = bs4DashNavbar(),
  sidebar = bs4DashSidebar(
    sidebarMenu(
      menuItem("First Module", tabName = "firstModule", icon = icon("home"))
    )
  ),
  controlbar = bs4DashControlbar(),
  footer = bs4DashFooter(),
  title = "Minimal Viable Shiny App",
  body = bs4DashBody(
    tabItems(
      tabItem(
        tabName = "firstModule",
        firstModuleUI(id = "firstModule", tabName = "firstModuleTab")
      )
    )
  )
)

# Define the server for the Shiny app
## This isn't necessarily needed with the use of moduleServer()
## Included here in case the file is needed in the codebase
server <- function(input, output, session) {
  
  # Call the second module's server with the iris dataset
  #secondModuleServer(id = "dataDownload", dataset = iris)
  
  # Call the first module's server
  firstModuleServer(id = "firstModule")
  
}
YBS
  • 19,324
  • 2
  • 9
  • 27
  • This works thanks. uiOutput() and renderUI() are a bit much performance wise, and I'm not actually dynamically changing the UI, so what I did was pass downloadBttn() in the UI function and removed the renderUI from the server. – s-mabdurrazak May 02 '23 at 06:28
  • Yes, that is another way. – YBS May 02 '23 at 12:01