rshinydtshinymodules

R shiny - Returning editable datatable from module


I've written an R shiny app which uses a module to create an editable datatable. The editable datatable is displayed correctly when the module runs using a reactiveValues I've named rved. However, when I try to return rved from the module after an actionButton is pressed, the module returns NA.

The app is intended to work as follows: A user first selects which rows to view. For each selected case, a module is run. This module creates inputs for how many variables to edit, which variables to edit, and outputs an editable datatable. The user is then meant write in a new value for the selected variables for the selected rows.

When the user presses a confirm actionButton, the module should return the editable datatable and save these results to a .rds file.

It appears the module takes the initial value when I initially define rved = reactiveValues(data = NA) , but does not appear to update rved outside of the observe() and observeEvent({}) environments.

Any help would be greatly appreciated.

library(shiny)
library(DT)

set.seed(2024)
data <- data.frame(rowID=1:4, var1=sample(1:4,4), var2=sample(1:4,4), var3=sample(1:4,4), var4=sample(1:4,4))

ui_module <- function(id, idx){
  ns <- NS(id)
  tagList(
    wellPanel(uiOutput(ns("num_changes")), uiOutput(ns("vars_to_change")), 
              dataTableOutput(ns("edit_table")),id=paste0("well", idx), class="wells"))
}

server_module <- function(id, rowID, returnedittable=F){
  moduleServer(
    id,
    function(input, output, session) {
      
      ns <- session$ns
      
      output$num_changes <- renderUI({
        numericInput(ns("num_changes"), "select number of variables to change:", value=1, min=1, max = 4, step=1)
      })
      
      output$vars_to_change <- renderUI({
        req(input$num_changes)
        vars_to_change_list <- lapply(1:input$num_changes, function(i) {
          name <- ns(paste0("vars_to_change_", i, sep=""))
          selectInput(name, "Select variable to change", names(data)[2:ncol(data)], selected="")
        })
        do.call(tagList, vars_to_change_list)
      })
      
      edit_table_func <- reactive({
        req(rowID)
        req(input$num_changes)
        df <- data.frame(matrix(nrow=input$num_changes, ncol=4))
        colnames(df) <- c("rowID", "var_name", "current_value", "new_value")
        df$rowID <- rowID
        df$var_name <- sapply(1:input$num_changes, FUN=function(i) {
          name <- paste0("vars_to_change_", i, sep="")
          input[[name]]
        })
        df$current_value <- sapply(1:input$num_changes, FUN=function(i) {
          data[data$rowID==rowID, as.character(df$var_name[i])]
        })
        return(df)
      })
      
      rved <- reactiveValues(data=NA)

      observe({
        req(rowID)
        req(input$num_changes)
        rved$data <- edit_table_func()
      })
      
      output$edit_table <- renderDataTable({
        req(rowID)
        req(input$num_changes)
        df <- edit_table_func()
        editable_columns = c("new_value")
        not_editable_columns = which(!colnames(df) %in% editable_columns) - 1
        datatable(rved$data, rownames = F,editable=list(target="cell", disable=list(columns=not_editable_columns)), selection = "none", options = list(iDisplayLength = 1000, dom = 'tir', columnDefs = list(list(className = 'dt-center', targets = "_all"))))
      })
      
      observeEvent(input$edit_table_cell_edit, {
        req(rowID)
        req(input$num_changes)
        for(i in 1:input$num_changes){
          name_input <- paste0("vars_to_change_", i, sep="")
          req(input[[name_input]])
        }
        rved$data <- data.frame(lapply(rved$data, as.character), stringsAsFactors=FALSE)
        req(input$edit_table_cell_edit)
        rved$data <<- editData(rved$data, input$edit_table_cell_edit, rownames = FALSE)
      })
      
      reactive(for (i in 1:input$num_changes) {
        local({
          name <- ns(paste0("vars_to_change_", i, sep=""))
          input[[name]]
        })
      })
      
      if(isTRUE(returnedittable)){
        return(reactive({rved$data}))
      }
    }
  )
}

ui <- fluidPage(
  titlePanel("Simple app w module"),
  sidebarLayout(
    sidebarPanel(width=2,
                                  uiOutput("rowID"),
                                  actionButton("confirm", "Confirm & save")
    ),
    mainPanel( width=10,
                           tabPanel("Resolve rowID", value='editor', 
                                    wellPanel(style = "background: powderBlue", id="fixed_panel")
                           )
    )
  )
)

server <- function(input, output, session) {
  get_rowID_options <- reactive({
    rowID_options <- unique(data$rowID)
    return(rowID_options)
  })
  
  output$rowID <- renderUI({
    selectInput("rowID", "Select rowID", get_rowID_options(), get_rowID_options(), multiple = T)
  })
  
  observeEvent(input$rowID, ignoreNULL = F, {
      choices <- get_rowID_options()
      num_choices <- length(choices)
      
      if(is.null(input$rowID)){
        removeUI(selector = ".wells", multiple = T)
        
      } else{
        matches <- match(input$rowID, choices)
        lapply(seq_along(matches), FUN=function(x) {
          id_name = paste0("id", matches[x])
          removeUI(selector = paste0("#well", matches[x]), multiple = T)
          if(x==1){
            well_id <- "#fixed_panel"
            insertUI(
              selector = well_id,
              where = "beforeEnd",
              ui = ui_module(id = id_name, idx=matches[x])
            )
          }
          if(x>1){
            well_id <- paste0("#well", matches[x-1])
            insertUI(
              selector = well_id,
              where = "afterEnd",
              ui = ui_module(id = id_name, idx=matches[x])
            )
          }
          rowID_idx = choices[matches[x]]
          server_module(id=id_name, rowID=rowID_idx)
        })
        
        if(length(input$rowID) < num_choices){
          lapply(which(!choices %in% input$rowID), FUN=function(i){
            id_idx <- paste0("id", i)
            removeUI(selector = paste0("#well", i), multiple = T)
          })
        }
      }
  })
  
  observeEvent(input$confirm, {
    lapply(1:length(input$rowID), FUN=function(i) {
      id_name = paste0("id", i)
      idx <- paste0("id",i, "-")
      rows = input[[paste0(idx, "num_changes")]]
      
      df <- data.frame(matrix(nrow=rows, ncol=5))
      colnames(df) <- c("rowID", "edit_date", "var_name", "current_value", "new_value")
      df[,] <- NA
      
      rved2 <- server_module(id=id_name, rowID=input$rowID[i], returnedittable = T)

      df$var_name <- rved2()$var_name
      df$current_value <- rved2()$current_value
      df$new_value <- rved2()$new_value
      
      df$rowID <- input$rowID[i]
      df$edit_date <- as.character(Sys.Date())

      #assuming changes.rds already exists
      new_df <- rbind(readRDS("changes.rds"), df)
      saveRDS(new_df, "changes.rds")
    }
    )
  })
}

shinyApp(ui = ui, server = server)

Solution

  • This seems to work. A notable change is that I execute the module server for every rowID, not in an observer.

    library(shiny)
    library(DT)
    
    set.seed(2024)
    data <- data.frame(
      rowID=1:4, 
      var1=sample(1:4,4), 
      var2=sample(1:4,4), 
      var3=sample(1:4,4), 
      var4=sample(1:4,4)
    )
    
    ui_module <- function(id, idx){
      ns <- NS(id)
      wellPanel(
        uiOutput(ns("ui_num_changes")), 
        uiOutput(ns("vars_to_change")), 
        DTOutput(ns("edit_table")),
        id = paste0("well", idx), 
        class = "wells"
      )
    }
    
    server_module <- function(id, rowID, returnedittable=FALSE){
      moduleServer(
        id,
        function(input, output, session) {
          
          ns <- session$ns
          
          output$ui_num_changes <- renderUI({
            numericInput(
              ns("num_changes"), "select number of variables to change:", 
              value=1, min=1, max = 4, step=1
            )
          })
          
          output$vars_to_change <- renderUI({
            req(input$num_changes)
            vars_to_change_list <- lapply(1:input$num_changes, function(i) {
              name <- ns(paste0("vars_to_change_", i, sep=""))
              selectInput(
                name, "Select variable to change", 
                names(data)[2:ncol(data)], selected=""
              )
            })
            do.call(tagList, vars_to_change_list)
          })
          
          observe({
            print(input$vars_to_change_1)
          })
          
          VarsToChange <- reactive({
            req(input$num_changes)
            vtc <- lapply(1:input$num_changes, FUN=function(i) {
              name <- paste0("vars_to_change_", i, sep="")
              input[[name]]
            })
            ok <- sapply(vtc, Negate(is.null))
            req(all(ok))
            unlist(vtc)
          })
          
          edit_table_func <- reactive({
            req(VarsToChange())
            #req(all(VarsToChange() != ""))
            df <- data.frame(matrix(nrow=input$num_changes, ncol=4))
            colnames(df) <- c("rowID", "var_name", "current_value", "new_value")
            df$rowID <- rowID
            df$var_name <- VarsToChange()
            df$current_value <- sapply(1:input$num_changes, FUN=function(i) {
              data[data$rowID==rowID, as.character(df$var_name[i])]
            })
            return(df)
          })
          
          rved <- reactiveValues(data=NA)
          
          observe({
            rved$data <- edit_table_func()
          })
          
          output$edit_table <- renderDT({
            df <- edit_table_func()
            editable_columns = c("new_value")
            not_editable_columns = which(!colnames(df) %in% editable_columns) - 1
            datatable(
              rved$data, rownames = FALSE, 
              editable=list(target="cell", disable=list(columns=not_editable_columns)), 
              selection = "none", 
              options = list(
                iDisplayLength = 1000, 
                dom = 'tir', 
                columnDefs = list(
                  list(className = 'dt-center', targets = "_all")
                )
              )
            )
          })
          
          observeEvent(input$edit_table_cell_edit, {
            rved$data <- editData(rved$data, input$edit_table_cell_edit, rownames = FALSE)
          })
          
          if(isTRUE(returnedittable)){
            return(reactive({rved$data}))
          }
        }
      )
    }
    
    ui <- fluidPage(
      titlePanel("Simple app w module"),
      sidebarLayout(
        sidebarPanel(width=2,
                     uiOutput("ui_rowID"),
                     actionButton("confirm", "Confirm & save")
        ),
        mainPanel(
          width=10,
          wellPanel(style = "background: powderBlue", id="fixed_panel")
        )
      )
    )
    
    server <- function(input, output, session) {
      get_rowID_options <- unique(data$rowID)
    
      output$ui_rowID <- renderUI({
        selectInput(
          "rowID", "Select rowID", 
          get_rowID_options, multiple = TRUE, selected = NULL
        )
      })
      
      observeEvent(input$rowID, ignoreNULL = TRUE, {
        choices <- get_rowID_options
        num_choices <- length(choices)
        
        if(is.null(input$rowID)){
          removeUI(selector = ".wells", multiple = T)
          
        } else{
          matches <- match(input$rowID, choices)
          lapply(seq_along(matches), FUN=function(x) {
            id_name = paste0("id", matches[x])
            removeUI(selector = paste0("#well", matches[x]), multiple = TRUE)
            if(x==1){
              well_id <- "#fixed_panel"
              insertUI(
                selector = well_id,
                where = "beforeEnd",
                ui = ui_module(id = id_name, idx=matches[x])
              )
            }
            if(x>1){
              well_id <- paste0("#well", matches[x-1])
              insertUI(
                selector = well_id,
                where = "afterEnd",
                ui = ui_module(id = id_name, idx=matches[x])
              )
            }
          })
          
          if(length(input$rowID) < num_choices){
            lapply(which(!choices %in% input$rowID), FUN=function(i){
              id_idx <- paste0("id", i)
              removeUI(selector = paste0("#well", i), multiple = T)
            })
          }
        }
      })
      
      Tables <- setNames(lapply(1:4, function(i) {
        id_name <- paste0("id", i)
        rowID_idx <- get_rowID_options[i]
        server_module(id=id_name, rowID=rowID_idx, returnedittable=TRUE)
      }), as.character(get_rowID_options))
      
      observeEvent(input$confirm, {
        lapply(input$rowID, FUN=function(rowid) {
          tabl <- Tables[[as.character(rowid)]]()
    
          df <- tabl
          df$edit_date <- as.character(Sys.Date())
          
          if(file.exists("changes.rds")) {
            df <- rbind(readRDS("changes.rds"), df)
          }
          saveRDS(df, "changes.rds")
        }
        )
      })
    }
    
    shinyApp(ui = ui, server = server)