I have a shiny app with an integrated rhandsontable, I have a "undo" button which undoes all changes at once (so for example if I changed 5 values, then click "undo" all 5 values are reverted simultaneously)
Is there any possibility to implement a second version of the undo button so that the changes are reverted one by one, so undo would only revert the single most recent change, i.e. after changing 3 values, I would have to click the undo button 3 times to revert all changes?
library(shiny)
library(rhandsontable)
change_hook <- "function(el,x) {
var hot = this.hot;
var cellChanges = [];
var changefn = function(changes,source) {
if (source === 'edit' || source === 'undo' || source === 'autofill' || source === 'paste') {
row = changes[0][0];
col = changes[0][1];
oldval = changes[0][2];
newval = changes[0][3];
if (oldval !== newval) {
var cell = hot.getCell(row, col);
cell.style.background = 'cyan';
cellChanges.push({'rowid':row, 'colid':col});
}
}
}
var renderfn = function(isForced) {
for(i = 0; i < cellChanges.length; i++)
{
var rowIndex = cellChanges[i]['rowid'];
var columnIndex = cellChanges[i]['colid'];
var cell = hot.getCell(rowIndex, columnIndex);
cell.style.background = 'cyan';
}
}
var loadfn = function(initialLoad) {
for(i = 0; i < cellChanges.length; i++)
{
var rowIndex = cellChanges[i]['rowid'];
var columnIndex = cellChanges[i]['colid'];
var cell = hot.getCell(rowIndex, columnIndex);
cell.style.background = 'white';
}
cellChanges = []
}
hot.addHook('afterChange', changefn);
hot.addHook('afterRender', renderfn);
hot.addHook('afterLoadData', loadfn);
} "
ui <- div(actionButton(inputId = "undo_button",label = "undo")
,rHandsontableOutput(outputId="mtcars"))
server <- function(input, output, session) {
undo <- reactiveVal(0)
output$mtcars <- renderRHandsontable({
r = undo()
rht = rhandsontable(mtcars,reset=r,stretchH="all",height=300)
undo(0)
htmlwidgets::onRender(rht,change_hook)
})
observeEvent(input$undo_button,
{
undo(1)
})
}
shinyApp(ui, server)
I don't think you need such a button as the functionality is already implemented by {rhandsontable}
out of the box:
Ctrl + z
: UndoCtrl + y
: RedoWhat I suggest, however, is a rethinking of what to render and when to re-render.
In this example, I show how you can achieve both of the functionalities you're after:
Ctrl + z
& Ctrl + y
, respectively.library(rhandsontable)
library(shiny)
ui <- fluidPage(
titlePanel("Handsontable"),
rHandsontableOutput("hot", height = 500),
actionButton(
inputId = "undo_all",
label = "Undo all changes",
icon = icon(
name = "arrow-rotate-left",
class = "fa-sharp fa-light"
),
style = "margin-top: 20px;"
)
)
server <- function(input, output, session) {
# reactive containing the original dataset:
r_original <- reactive({ iris })
# reactive value to track changes to the dataset:
rv_current <- reactiveVal(value = NULL)
# in case the original dataset is changed, update 'rv_current':
observeEvent(r_original(), {
rv_current(r_original())
})
# in case the rhandsontable is edited, update 'rv_current':
observeEvent(input$hot, {
current <- hot_to_r(input$hot)
rv_current(current)
})
output$hot <- renderRHandsontable({
rhandsontable(
data = r_original(), # NOTE: Render 'r_original()', NOT 'rv_current()'
readOnly = FALSE,
useTypes = TRUE,
stretchH = "all"
)
}) |>
bindEvent(r_original(), input$undo_all)
# The bindEvent tells shiny to only re-render the table if 'r_original()'
# changes or when 'undo_all' button is clicked.
# You can now use 'rv_current()' for any calculations on the newest state of
# the table
}
shinyApp(ui, server)