0

I am relying on the code chunk found here to create a Shiny app to upload a table, edit the table, then download the table. I have managed to edit a table that is already loaded in memory (iris) but how do I edit a table that is to be uploaded in Shiny?.

I have tried the code in the link above and verified that it works. I have also tried the code below and this works too. What I have not been able to achieve is to convert the data frame x into a reactive object that is assigned to an uploaded file, and edit all the references to x accordingly.

# This code works, but lacks a fileinput object 
# and needs to be amended for a reactive dataframe...
library(shiny)
library(DT)
shinyApp(
  ui = fluidPage(
    fluidRow(
    # ~~ add fileInput("file1", "Choose file") here ~~
    downloadButton("download")
    ),
    fluidRow(
    DT::dataTableOutput('x1')
    )
  ),
  server = function(input, output, session) {
    # Do I make x reactive?
    x = iris
    x$Date = Sys.time() + seq_len(nrow(x))
    output$x1 = DT::renderDataTable(x, selection = 'none', rownames = FALSE, edit = TRUE)

    proxy = dataTableProxy('x1')

    observeEvent(input$x1_cell_edit, {
      info = input$x1_cell_edit
      str(info)
      i = info$row
      j = info$col + 1
      v = info$value
      x[i, j] <<- DT:::coerceValue(v, x[i, j])
      replaceData(proxy, x, resetPaging = FALSE, rownames = FALSE)
    })

    output$download <- downloadHandler("example.csv", 
                                       content = function(file){
                                         write.csv(x, file)
                                       },
                                       contentType = "text/csv")

    }
)

Previous attempts at this have thrown errors, mostly to do with operations not being allowed without an active reactive context.

The code below shows what I want to achieve, but throws an error: "argument "expr" is missing, with no default"

library(shiny)
library(DT)
shinyApp(
  ui = fluidPage(
    fluidRow(
      fileInput("upload", "Choose CSV File",
                multiple = FALSE,
                accept = c("text/csv",
                           "text/comma-separated-values,text/plain",
                           ".csv")),
    downloadButton("download")
    ),
    fluidRow(
    DT::dataTableOutput('x1')
    )
  ),
  server = function(input, output, session) {
    #x = iris

    # In this edited example x is now a reactive expression, dependent on input$upload
    x <- eventReactive({


      # input$file1 will be NULL initially. After the user selects
      # and uploads a file, head of that data file by default,
      # or all rows if selected, will be shown.

     req(input$upload)

      # when reading semicolon separated files,
      # having a comma separator causes `read.csv` to error
      tryCatch(
        {
          x <- read.csv(input$upload$datapath,
                         header = TRUE,
                         sep = ",",
                         stringsAsFactors = TRUE,
                         row.names = NULL)

        },
        error = function(e) {
          # return a safeError if a parsing error occurs
          stop(safeError(e))
         }
      )
    })

    #x$Date = Sys.time() + seq_len(nrow(x))
    output$x1 = DT::renderDataTable(x(), selection = 'none', rownames = FALSE, edit = TRUE)

    proxy = dataTableProxy('x1')

    observeEvent(input$x1_cell_edit, {
      info = input$x1_cell_edit
      str(info)
      i = info$row
      j = info$col + 1
      v = info$value
      x()[[i, j]] <<- DT:::coerceValue(v, x()[[i, j]])
      newdf <- x()
      replaceData(proxy, newdf, resetPaging = FALSE, rownames = FALSE)
    })

    output$download <- downloadHandler("example.csv", 
                                       content = function(file){
                                         write.csv(x(), file)
                                       },
                                       contentType = "text/csv")

    }
)
RDavey
  • 564
  • 3
  • 15

2 Answers2

2

With thanks to Stephane, and inspiration from this related question, I think I have an answer.

Key is to use reactiveValues as a workaround to DT:::coerceValue not liking reactive expressions. I've included a verbatimTextOutput to illustrate the stored changes to the table after you edit the data table. The download button allows you to download the edited table too.

library(shiny)
library(DT)
shinyApp(
  ui = fluidPage(
    fluidRow(
      fileInput("upload", "Choose CSV File",
                multiple = FALSE,
                accept = c("text/csv",
                           "text/comma-separated-values,text/plain",
                           ".csv")),
    downloadButton("download")
    ),
    fluidRow(
    DT::dataTableOutput('x1'),
    verbatimTextOutput("print")
    )
  ),
  server = function(input, output, session) {

    # In this edited example x is now a reactive expression, dependent on input$upload

    # Key to the solution is the use of reactiveValues, stored as vals
    vals <- reactiveValues(x = NULL)

    observe({


      # input$upload will be NULL initially. After the user selects
      # and uploads a file, head of that data file by default,
      # or all rows if selected, will be shown.

     req(input$upload)

      # when reading semicolon separated files,
      # having a comma separator causes `read.csv` to error
      tryCatch(
        {
          x <- read.csv(input$upload$datapath,
                         header = TRUE,
                         sep = ",",
                         stringsAsFactors = TRUE,
                         row.names = NULL)

        },
        error = function(e) {
          # return a safeError if a parsing error occurs
          stop(safeError(e))
         }
      )
      # Reactive values updated from x
      vals$x <- x
    })

    output$print <- renderPrint({
      vals$x
    })
    output$x1 = DT::renderDataTable(vals$x, selection = 'none', rownames = FALSE, edit = TRUE)

    proxy = dataTableProxy('x1')

    observeEvent(input$x1_cell_edit, {
      info = input$x1_cell_edit
      str(info)
      i = info$row
      j = info$col + 1
      v = info$value
      # Below is the crucial spot where the reactive value is used where a reactive expression cannot be used
      vals$x[i, j] <<- DT:::coerceValue(v, vals$x[i, j])
      replaceData(proxy, vals$x, resetPaging = FALSE, rownames = FALSE)
    })

    output$download <- downloadHandler("example.csv", 
                                       content = function(file){
                                         write.csv(vals$x, file, row.names = F)
                                       },
                                       contentType = "text/csv")

    }
)
RDavey
  • 564
  • 3
  • 15
0

See ?eventReactive.

You should do:

x <- eventReactive(input$upload, { # 'input$upload' was the "expr missing"
       ......

or

x <- reactive({
        req(input$upload)
        ......
Stéphane Laurent
  • 48,421
  • 14
  • 86
  • 170
  • Woops! That was a silly mistake. Replacing with your first suggestion, I now have the following error: _"Warning: Error in < – RDavey Jan 30 '19 at 11:58
  • @RDavey That's because your reactive conductor `x` returns nothing, I think. Add `x` (or `return(x)`) before closing it. Moreover, I think you cannot change a reactive value by doing `x()[[i, j]] – Stéphane Laurent Jan 30 '19 at 12:05