Search code examples
rshinyshinydashboardflexdashboard

Can't get flexdashboard modal Shiny reactivity to deal with NULL state


First off, here's a minimal reproducible example:

Suppose I have the following flexdashboard app example.Rmd:

---
title: "Test App"
output:
  flexdashboard::flex_dashboard:
    theme: ["lumen"]
    orientation: ["columns"]
runtime: shiny
---

```{r setup, include=FALSE}
source('modules.R')
```

Test
==================

Inputs {.sidebar data-width=250}
------------------

```{r}

## Modules:
globalInputs <- callModule(setup, "inputs")  # Note: you only need to call this one time!
callModule(chart, "modalChart", globalInputs)
## UI:
sidebar("inputs")

```

Column
-------------------------------------

### ModalTest

```{r}
chartUI2("modalChart")
```

Here's my modules.R file:

sidebar <- function(id) {
  ns <- NS(id)
  tagList(
    helpText("Press the button below to pull up a modal:"),
    actionButton(ns("settings"), "Settings", icon = icon("cogs"), width = '100%')
  )
}

# setup function
setup <- function(input, output, session) {
  return(input)  ## Note, input in this case is a reactive list of values that you can index against with $ or [[""]]
}

# UI module for the popup Settings modal
modalUI <- function(id) {
  ns <- NS(id)
  withTags({  # UI elements for the modal go in here
    ## Note: you can use the fluidPage() and fluidRow() functions to define a column-based UI for the chart inputs below:
    fluidPage(
      fluidRow(sliderInput(ns("bins"), "Number of bins:",
                           min = 1,  max = 50, value = 30),
               textInput(ns("plotTitle"), label = "Plot Title", value = "This is a test")
      ))
  })
}


## UI module for the 2 buttons in the modal:
modalFooterUI <- function(id) {
  ns <- NS(id)
  tagList(
    modalButton("Cancel", icon("remove")),
    actionButton(ns("modalApply"), "Apply", icon = icon("check"))
  )
}

## chart module ----------------------------------------------------------------
chartUI2 <- function(id) {
  ns <- NS(id)
  plotOutput(ns("distPlot"))
}

chart <- function(input, output, session, setup) {

  observeEvent(setup$settings, {
    showModal(modalDialog(
      modalUI("inputs"),  # Call UI function defined in './modules/modal.R'; note the namespace 'inputs' is the same globally for the app
      title = "Settings",
      footer = modalFooterUI("inputs"),
      size = "l",
      easyClose = TRUE,
      fade = TRUE)
    )
  })

  output$distPlot <- renderPlot({
    if (setup$settings == 0)
      return(
        hist(faithful[, 2],
             breaks = 5,  # if the button hasn't been pressed, default
             main = 'This is the default title',
             col = 'darkgray',
             border = 'white')
      )
    isolate({
      x <- faithful[, 2]
      bins <- setup$bins
      hist(x,
           breaks = bins,
           main = setup$plotTitle,
           col = 'darkgray',
           border = 'white')
    })

  })
}

Here's what I want to accomplish:

  1. When the page loads render distPlot with default parameters. This should always happen.
  2. When I click the 'Settings' actionButton, it pops up the modal overlay (but keeps the plot in the background in its original state)
  3. Allow the user to change the parameters in the pop-up modal, but do not re-render the plot until the user hits the 'Apply' button.

I think I have #1 figured out, but when I click the 'Settings' button I get an error Invalid breakpoints produced by 'breaks(x)': NULL. Then, I change the inputs and it does nothing (except until I hit the Settings actionButton again (which in turn renders the plot with the given inputs).

What am I doing wrong?

Thanks!!


Solution

  • There might be a timing issue. When you press the settings button, setup$settings becomes 1 so the renderPlot tries to replot. If this happens before the slider is set up, setup$bins is NULL and you get an error.

    You could add a condition in the first if of your renderPlot to plot with setup$bins only when the user presses the Apply button in the modal:

    output$distPlot <- renderPlot({
        if (setup$settings == 0 | !isTruthy(setup$modalApply)) {
            hist(faithful[, 2],
                 breaks = 5,  # if the button hasn't been pressed, default
                 main = 'This is the default title',
                 col = 'darkgray',
                 border = 'white')
          } else {
        isolate({
          x <- faithful[, 2]
          bins <- setup$bins
          hist(x,
               breaks = bins,
               main = setup$plotTitle,
               col = 'darkgray',
               border = 'white')
        })
    

    !isTruthy will be TRUE if setup$modalApply is NULL (modal hasn't been created) or if it is 0 (user hasn't pressed the button yet). Once the user presses the Apply button, the plot is updated.

    Edit:

    To make it easier, you can use a reactiveValues to hold all your plot parameters, including the default ones. It can be updated when the user clicks the apply button, which in turn updates the plot:

     plot_params = reactiveValues(bins=5,title='This is the default title')
    
    
      output$distPlot <- renderPlot({
            hist(faithful[, 2],
                 breaks = plot_params$bins,
                 main = plot_params$title,
                 col = 'darkgray',
                 border = 'white')
      })
    
      observeEvent(setup$modalApply,{
        plot_params$bins = setup$bins
        plot_params$title = setup$plotTitle
      })