Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make it possible to edit values in table #480

Merged
merged 3 commits into from
Jan 18, 2018
Merged

Make it possible to edit values in table #480

merged 3 commits into from
Jan 18, 2018

Conversation

yihui
Copy link
Member

@yihui yihui commented Jan 17, 2018

Closes #28

Test

devtools::install_github('rstudio/DT')

Double click in a table cell to edit its value.

(1) client-side processing (static HTML application)

DT::datatable(iris, editable = TRUE)

(2) client-side processing in Shiny

library(shiny)
library(DT)
shinyApp(
  ui = fluidPage(
    DTOutput('x2')
  ),
  server = function(input, output, session) {
    x = iris
    x$Date = Sys.time() + seq_len(nrow(x))
    output$x2 = renderDT(x, selection = 'none', server = F, editable = T)
  }
)

(3) server-side processing

library(shiny)
library(DT)
shinyApp(
  ui = fluidPage(
    DTOutput('x1')
  ),
  server = function(input, output, session) {
    x = iris
    x$Date = Sys.time() + seq_len(nrow(x))
    output$x1 = renderDT(x, selection = 'none', editable = TRUE)
    
    proxy = dataTableProxy('x1')
    
    observeEvent(input$x1_cell_edit, {
      info = input$x1_cell_edit
      str(info)
      i = info$row
      j = info$col
      v = info$value
      x[i, j] <<- DT::coerceValue(v, x[i, j])
      replaceData(proxy, x, resetPaging = FALSE)  # important
    })
  }
)

(4) server-side processing (without row names)

library(shiny)
library(DT)
shinyApp(
  ui = fluidPage(
    DTOutput('x1')
  ),
  server = function(input, output, session) {
    x = iris
    x$Date = Sys.time() + seq_len(nrow(x))
    output$x1 = renderDT(x, selection = 'none', rownames = F, editable = T)
    
    proxy = dataTableProxy('x1')
    
    observeEvent(input$x1_cell_edit, {
      info = input$x1_cell_edit
      str(info)
      i = info$row
      j = info$col + 1  # column index offset by 1
      v = info$value
      x[i, j] <<- DT::coerceValue(v, x[i, j])
      replaceData(proxy, x, resetPaging = FALSE, rownames = FALSE)
    })
  }
)

@yihui yihui added this to the v0.3 milestone Jan 17, 2018
@yihui yihui merged commit 5360f0e into master Jan 18, 2018
@yihui yihui deleted the feature/editor branch January 18, 2018 04:04
yihui added a commit that referenced this pull request Jan 18, 2018
@tamluong
Copy link

tamluong commented Jan 25, 2018

Hi,

I have a question on this feature. Let's say I have some other analysis that depends on the data in the table. How do link those analysis with the updated data from the edited datatable? For example, if I changed one cell in the table, will the analysis be updated automatically?

I have a example code below (not working)

library(shiny)
library(DT)

ui <- fluidPage(
    DT::dataTableOutput('x1'),
    textOutput("text")
)

server <- function(input, output, session) {
    x = iris
    y <- reactive(x)
    x$Date = Sys.time() + seq_len(nrow(x))
    output$x1 = DT::renderDataTable(x, selection = 'none', editable = TRUE)
    
    proxy = dataTableProxy('x1')

    observeEvent(input$x1_cell_edit, {
        info = input$x1_cell_edit
        str(info)
        i = info$row
        j = info$col
        v = info$value
        x[i, j] <<- DT::coerceValue(v, x[i, j])
        replaceData(proxy, x, resetPaging = FALSE)  # important
    })
    
    output$text <- renderText({
        y()[1, 1]
    })
}

So in this case, I wanted the text output "text" to be automatically updated when I change cell [1,1]. However the code above did not work, what is your thought on this?

Thanks!

@yihui
Copy link
Member Author

yihui commented Jan 25, 2018

@tamluong Your output$text should be dependent on input$x1_cell_edit, otherwise when you edit a cell, it won't be automatically updated. This should work:

    output$text <- renderText({
        input$x1_cell_edit
        x[1, 1]
    })

Note you don't need y(), because x has been updated via <<-.

@tamluong
Copy link

@yihui Thank you! That works perfectly. One thing I noticed is that although y is an reactive expression based on x, y is not automatically updated when x is changed. Was that because Shiny does not recognize x as an input value?

@yihui
Copy link
Member Author

yihui commented Jan 25, 2018

@tamluong That is because although y() was created by reactive(), it is not literally reactive to any inputs. You meant to make it reactive to the edit event, but you didn't state the dependency in its definition. For example, you can associate input$x1_cell_edit with y():

server <- function(input, output, session) {
    x = iris
    y <- reactive({
      input$x1_cell_edit
      x
    })
    x$Date = Sys.time() + seq_len(nrow(x))
    output$x1 = DT::renderDataTable(x, selection = 'none', editable = TRUE)
    
    proxy = dataTableProxy('x1')

    observeEvent(input$x1_cell_edit, {
        info = input$x1_cell_edit
        str(info)
        i = info$row
        j = info$col
        v = info$value
        x[i, j] <<- DT::coerceValue(v, x[i, j])
        replaceData(proxy, x, resetPaging = FALSE)  # important
    })
    
    output$text <- renderText({
        y()[1, 1]
    })
}

Or make output$text reactive to input$x1_cell_edit this way (note y does not need to be reactive):

server <- function(input, output, session) {
    x = iris
    y <- function() x
    x$Date = Sys.time() + seq_len(nrow(x))
    output$x1 = DT::renderDataTable(x, selection = 'none', editable = TRUE)
    
    proxy = dataTableProxy('x1')

    observeEvent(input$x1_cell_edit, {
        info = input$x1_cell_edit
        str(info)
        i = info$row
        j = info$col
        v = info$value
        x[i, j] <<- DT::coerceValue(v, x[i, j])
        replaceData(proxy, x, resetPaging = FALSE)  # important
    })
    
    output$text <- renderText({
        input$x1_cell_edit
        y()[1, 1]
    })
}

@tamluong
Copy link

@yihui that makes perfect sense! Thanks so much!

@KaterinaKou
Copy link

Hello!
Is there a workaround to make only certain columns editable?

@zackbatist
Copy link

zackbatist commented Jun 26, 2018

The coerceValue command is giving me the following error, since according to the docs it only works with integer, double, date, time and factor values:

Warning in DT::coerceValue(v, x[i, j]) :
The data type is not supported: character

The data that i want the user to edit are character values. Any suggestions for a workaround would be immensely appreciated!

@yihui
Copy link
Member Author

yihui commented Jun 26, 2018

@zackbatist #542

devtools::install_github('rstudio/DT')

@adiguzelomer
Copy link

Is there any update on KaterinaKou' s question ?
"
Hello!Is there a workaround to make only certain columns editable?
"
I am looking forward to it too. O.O

@yihui
Copy link
Member Author

yihui commented Jun 26, 2018

@adiguzelomer No: #550.

@KaterinaKou
Copy link

KaterinaKou commented Jun 26, 2018

@adiguzelomer
if you're still looking for a workaround,

fooreactive<-reactiveValues(foovalue={
       #Some kind of dataframe you show on a DT table
}
output$DToutput<-DT::renderDataTable({
        fooreactive[["foovalue"]]
}, editable = TRUE)  #Your DT output
      
proxy = DT::dataTableProxy('DToutput')  #Your DT proxy
      
observeEvent(input$DToutput_cell_edit, {
        info = input$DToutput_cell_edit
        str(info)
        i = info$row
        j = info$col + 1  # column index offset by 1
        v = info$value
        if(j == 3){ #This is the column you want to have as editable
          fooreactive[["foovalue"]]$nameofeditcolumn[i] <- DT::coerceValue(v, fooreactive[["foovalue"]]$nameofeditcolumn[i])
          rep<-fooreactive[["foovalue"]]
          DT::replaceData(proxy, rep, resetPaging = FALSE, rownames = FALSE)
        }
})

@philibe
Copy link

philibe commented Jun 27, 2018

Thank you for this functionality @yihui of this merged branch :)

Is there functionality to delete and insert, in the Shiny point of view like editable = TRUE ?

From part (4), I've done this little code for SQL database, (only for update). (SQL Server Windows from ODBC Linux)

library(DT)
library("shiny")
library("RODBCext")

# CREATE  TABLE A_TABLE (
#   id INT ,
#   Col_int INT NULL DEFAULT NULL,
#   col_text VARCHAR(50) NULL DEFAULT NULL,
#   col_date DATETIME NULL DEFAULT NULL,
#   col_numeric NUMERIC(7,4) NULL DEFAULT NULL,
#   CONSTRAINT pk_A_TABLE PRIMARY KEY (id)
# )


updateSQL <- function(table_name,df, pk_name, cell_edited,connexion_bdd ) { 
  r <- FALSE
  
  colnames_list= as.list(colnames(df))
  colname_pk_id=as.character(subset(df, select=c(pk_name ))[cell_edited$row,])
  
  if (is.numeric(cell_edited$value)) {
    sep=""
  } else sep="'"
  
  sql= paste0("UPDATE ",table_name," SET ",  colnames_list[cell_edited$col + 1],
              "=",sep,as.character(cell_edited$value),sep," where ",pk_name,"=",colname_pk_id)
  tryCatch({
     sqlExecute(connexion_bdd, sql,fetch= FALSE, errors = TRUE)
     r <- TRUE
  },error=function(cond) {
    r <- FALSE
  }) 
  return(r)
}

shinyApp(
  ui = fluidPage(
    DTOutput('x1')
  ),
  server = function(input, output, session) {
    
    connexion_bdd <- odbcDriverConnect(connexion_bdd_odbc_txt)  
    
    cancel.onSessionEnded <- session$onSessionEnded(function() {
      odbcClose(connexion_bdd)
    })  

     df <- function(){
       return(as.data.frame(
         sqlQuery( connexion_bdd,  "SELECT  * FROM A_TABLE"   ),
           stringsAsFactors = F
         ))
     }
    
    output$x1 = renderDT(df(), selection = 'none', rownames = F, editable = T)
    proxy = dataTableProxy('x1')    
    
    observeEvent(input$x1_cell_edit, {
      cell_edited = input$x1_cell_edit
      updateSQL ("A_TABLE",df(), 'id', cell_edited,connexion_bdd )  
      replaceData(proxy, df(), resetPaging = FALSE, rownames = FALSE)
    })
    
  }
)

@yihui
Copy link
Member Author

yihui commented Jun 27, 2018

@philibe Currently it is not straightforward, and it may require deeper understanding of the implementation of server-side processing in this package. I did think about wider support for SQL long time ago (see #194), but haven't found time to actually do the work. It will definitely be amazing if all basic SQL operations can be supported in DT (in particular, querying/filtering should be much much faster). Your example is a very good start.

@philibe
Copy link

philibe commented Jun 28, 2018

👍 :)

@adiguzelomer
Copy link

@KaterinaKou thank you alot ! I am gonna try it.

@zackbatist
Copy link

Thanks @yihui, you're of great help. I also agree with @philibe, such SQL functionality would be extremely useful!

@ddelarosa70
Copy link

is there a way to use the all the extensions that DT have a long with the editing. ie. fixed columns, hiding columns etc.

As far as I can tell and test there is only
output$x <- DT::renderDT( data, selection = 'none', server = F, editable = T )

if I try to use an expression to calculate the data and return datatable with the extensions, the editing is ignored , and viceversa

@astranovasky
Copy link

Hi! Is there any way that editable= TRUE for specific columns and not for the whole datatable?

@adityaraj04
Copy link

how to write/save the file after making changes to it?

@Bidossessih
Copy link

Hi @yihui ,
I would like to know how to enable the value edition in small device browser. I tried your example (https://yihui.shinyapps.io/DT-edit/) in mobile phone but I can't edit the table. It works perfectly when accessing from my computer. I need this functionality in an app optimized for small device. Thanks.

@rajkumarmaniraja57
Copy link

Hi @yihui,
Is there way to apply edit table and as well as filter table both in single DT? If yes,then how should I use updated DT to my further analysis.

@nschwamm
Copy link

nschwamm commented Feb 1, 2019

@tamluong That is because although y() was created by reactive(), it is not literally reactive to any inputs. You meant to make it reactive to the edit event, but you didn't state the dependency in its definition. For example, you can associate input$x1_cell_edit with y():

server <- function(input, output, session) {
    x = iris
    y <- reactive({
      input$x1_cell_edit
      x
    })
    x$Date = Sys.time() + seq_len(nrow(x))
    output$x1 = DT::renderDataTable(x, selection = 'none', editable = TRUE)
    
    proxy = dataTableProxy('x1')

    observeEvent(input$x1_cell_edit, {
        info = input$x1_cell_edit
        str(info)
        i = info$row
        j = info$col
        v = info$value
        x[i, j] <<- DT::coerceValue(v, x[i, j])
        replaceData(proxy, x, resetPaging = FALSE)  # important
    })
    
    output$text <- renderText({
        y()[1, 1]
    })
}

Or make output$text reactive to input$x1_cell_edit this way (note y does not need to be reactive):

server <- function(input, output, session) {
    x = iris
    y <- function() x
    x$Date = Sys.time() + seq_len(nrow(x))
    output$x1 = DT::renderDataTable(x, selection = 'none', editable = TRUE)
    
    proxy = dataTableProxy('x1')

    observeEvent(input$x1_cell_edit, {
        info = input$x1_cell_edit
        str(info)
        i = info$row
        j = info$col
        v = info$value
        x[i, j] <<- DT::coerceValue(v, x[i, j])
        replaceData(proxy, x, resetPaging = FALSE)  # important
    })
    
    output$text <- renderText({
        input$x1_cell_edit
        y()[1, 1]
    })
}

For this example, how would you replicate it, if the datatable itself was based on a reactive value. Let's say we just binded iris and input$test as a dataframe. Where selectInput("test",choices=c(TRUE,FALSE), so it's just a binary choice and the last column of Iris is an editable column that's initial values are based on an input?

Obviously a more complicated example wouldn't just spit out the input but I am trying to understand how you can use editable DT as well as the values from it to persist when the underlying data is dependent on inputs.

@anapaularg
Copy link

anapaularg commented Apr 10, 2019

@adiguzelomer
if you're still looking for a workaround,

fooreactive<-reactiveValues(foovalue={
       #Some kind of dataframe you show on a DT table
}
output$DToutput<-DT::renderDataTable({
        fooreactive[["foovalue"]]
}, editable = TRUE)  #Your DT output
      
proxy = DT::dataTableProxy('DToutput')  #Your DT proxy
      
observeEvent(input$DToutput_cell_edit, {
        info = input$DToutput_cell_edit
        str(info)
        i = info$row
        j = info$col + 1  # column index offset by 1
        v = info$value
        if(j == 3){ #This is the column you want to have as editable
          fooreactive[["foovalue"]]$nameofeditcolumn[i] <- DT::coerceValue(v, fooreactive[["foovalue"]]$nameofeditcolumn[i])
          rep<-fooreactive[["foovalue"]]
          DT::replaceData(proxy, rep, resetPaging = FALSE, rownames = FALSE)
        }
})

Hi @KaterinaKou, this worked for you? I tried to replicate your solution but what I see is that the "user" still could click and edit a cell that is not in the column 3, and when doing "enter" the new value stays in the table. Only when trying to edit column 3, the other values previously changed return to their original value. This is really weird for the user. Maybe I'm doing something wrong. Did you get a solution where the user can't double click and edit the cell?

yihui added a commit that referenced this pull request Apr 30, 2019
also close #657

a few users also wanted this feature in #480
@yihui
Copy link
Member Author

yihui commented Apr 30, 2019

For those who want to disable editing on certain columns, I just pushed an implementation through 832714f. Thanks!

BTW, you can also see Table 10 in the example https://yihui.shinyapps.io/DT-edit/

@PeterDieter
Copy link

Hello:) As @rajkumarmaniraja57 already asked, is there a way how I can change a value in a data table that can be filtered based on inputs. So is filtering and editing possible at the same time?
Thanks @yihui

@yihui
Copy link
Member Author

yihui commented May 10, 2019

@PeterDieter No, that is currently not possible. Sorry.

@antoine-sachet
Copy link

Is it still "important" to run replaceData in the observer?

The example seems to work just fine without replaceData(proxy, x, resetPaging = FALSE), and it actually avoids a flicker.

If it can be safely avoided, it makes it easier to use a proxy in a shiny module (no need to pass the global session to the proxy).

library(shiny)
library(DT)
shinyApp(
  ui = fluidPage(
    DTOutput('x1')
  ),
  server = function(input, output, session) {
    x = iris
    x$Date = Sys.time() + seq_len(nrow(x))
    output$x1 = renderDT(x, selection = 'none', editable = TRUE)
    
    proxy = dataTableProxy('x1')
    
    observeEvent(input$x1_cell_edit, {
      info = input$x1_cell_edit
      str(info)
      i = info$row
      j = info$col
      v = info$value
      x[i, j] <<- DT::coerceValue(v, x[i, j])
      # Code works just fine without the line below
      # replaceData(proxy, x, resetPaging = FALSE)  # important
    })
  }
)

@yihui
Copy link
Member Author

yihui commented Jun 11, 2019

@antoine-sachet Without replaceData(), the new value only lives in your browser. If you navigate to a different page of the table, then come back, the edited value will be lost (you will see the old value). This is because renderDT() does not know x has been modified, and will still render the old copy of x.

@Stephonomon
Copy link

Hi, this is great! Everything works except that after the data are saved, the table displaying the information goes blank and DT says No Matching Records Found, however, if I reload the page, the saved data is there.

shinyServer(function(input, output) {
  ds <<- redcap_read(redcap_uri=uri, token=token)$data
  
  output$redcaptable = renderDataTable({
    ds <<- redcap_read(redcap_uri=uri, token=token)$data
    ds <- datatable(ds,
                    editable = TRUE
                    )
  })
  
  proxy = dataTableProxy('redcaptable')
  
  observeEvent(input$redcaptable_cell_edit, {
    info = input$redcaptable_cell_edit
    str(info)
    i = info$row
    j = info$col
    v = info$value 
    ds[i, j] <<- DT::coerceValue(v, ds[i, j])
    replaceData(proxy, ds, resetPaging = TRUE, rownames = FALSE)
    redcap_write(ds,redcap_uri=uri,token=token)
    ds
#ds <<- redcap_read(redcap_uri=uri, token=token)$data
  })
#tried with and without the read_cap read (pulls from API), but no luck

@jumanbar
Copy link

jumanbar commented Jul 2, 2019

This functionality is great.

In my case, I have a small problem regarding the characters ">" and "<". When using client-side processing example, I cannot change the content of a cell from "setosa" to "< LD", for example, because that means the contents are no longer visible (I know this example doesn't make much sense, but trust me, it's important for my app). This of course happens because "<" and ">" are reserved HTML characters (if I write "&lt; LD" the cell contents are shown as intended).

I've noticed that this does not happen when using the example provided for server-side processing. But in that case I have other problems, since my file is provided by the user... Would it be possible to have the server side functionality even in the case of a file uploaded by the user (using fileInput)?

I attempted some code based on the above example (pasted below) and failed, with the following error message:

Warning: Error in <<-: invalid (NULL) left side of assignment
  72: observeEventHandler [/home/juan/Desktop/example.R#46]
   1: runApp

Here is my code:

library(shiny)
library(DT)

shinyApp(
  ui = fluidPage(
    fileInput(
      inputId = "upload", 
      label = span(icon("upload"), "Choose csv file:"),
      multiple = FALSE, 
      accept = c("text/csv",
                 "text/comma-separated-values,text/plain",
                 ".csv")),
    DTOutput('x1')
  ),
  server = function(input, output, session) {
    upfile <- reactive({
      req(input$upload)
      tryCatch({
        out <- read.csv(input$upload$datapath, as.is = TRUE)
        out$Date <- Sys.time() + seq_len(nrow(out))
      }, # return a safeError if a parsing error occurs
      error = function(e) stop(safeError(e)) 
      )
      return(out)
    })
    
    output$x1 = renderDT(upfile(), selection = 'none', 
                         server = TRUE, editable = TRUE)
    
    proxy = dataTableProxy('x1')
    
    observeEvent(input$x1_cell_edit, {
      info = input$x1_cell_edit
      str(info)
      i = info$row
      j = info$col
      v = info$value
      # Here I basically changed x with upfile() in all instances:
      upfile()[[j]][i] <<- DT::coerceValue(v, upfile()[[j]][i])
      replaceData(proxy, upfile(), resetPaging = FALSE)  # important
    })
  }
)

The file used for my tests was created with:

write.csv(iris, "iris.csv", row.names = FALSE)

Thanks for your work and your time!

@Zemnii
Copy link

Zemnii commented Aug 23, 2019

Thank you very much it works really fine , however when i affect the table to a reactive one using :
data=reactive({input$x1_cell_edit
return(x)}
it works fine but i get this warning when editing :
Capture

@shrektan
Copy link
Collaborator

Please open a new issue if you need help or encounter bugs. Leaving comments on a closed PR will be easily missed because it’s difficult to find the entry later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Is there an Editor extension available for the DT package