rshinymoduleshinyjsbs4dash

Modularized Shiny app: How to download dataset passed between modules


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.


Solution

  • 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")
      
    }