
{undomanager} lets you track the history of any R object and move through it with undo and redo operations. Anything can be stored, from a single number to a data frame or an entire application state. A manager can restrict its history to specific classes and cap how many items it keeps, and it can be reactive to integrate with ‘Shiny’.
Need Shiny help? I’m
available for consulting.
If you find
{undomanager} useful, please consider supporting my work!
❤
NULLAfter creating a new manager with undomanager(), the
main actions are $do(item), $undo(),
$redo().
nums <- undomanager()
nums$do(5)
nums$do(7)
nums$do(10)
nums$do(12)
nums$undo()
nums$undo()
nums$redo()
print(nums)<UndoManager> of arbitrary items with 2 undos and 1 redo
### Current item ###
[1] 10
### Undo stack ###
1.
[1] 7
2.
[1] 5
### Redo stack ###
1.
[1] 12
You can also chain all the operations; the above is equivalent to:
undomanager()$do(5)$do(7)$do(10)$do(12)$undo()$undo()$redo()Use $value to get the current item from the manager.
nums$value
#> [1] 10
nums$undo()
nums$value
#> [1] 7Developed, in part, as a component of stanpumpR (see https://github.com/StevenLShafer/stanpumpR).
Become a sponsor for {undomanager}!
For most users: To install the stable CRAN version:
install.packages("undomanager")For advanced users: To install the latest development version from GitHub:
install.packages("remotes")
remotes::install_github("daattali/undomanager")By default an UndoManager accepts any object. Pass a
type to undomanager() to restrict it to one or
more classes:
nums <- undomanager("numeric")
nums$do(5)
nums$do("a")
#> Error: do: The provided item must have class <numeric>The type is matched against the same classes that R’s S3 dispatch
would use. That means "numeric" also accepts integers and
numeric matrices, while rejecting things like logicals, factors, or
dates. Pass several classes to allow any of them:
items <- undomanager(c("numeric", "character"))
items$do(5)
items$do("a")
items$do(TRUE)
#> Error: do: The provided item must have class <numeric>|<character>undo() and redo() accept an n
argument to move more than one step at a time. If n is
larger than the number of available operations, they stop at the end of
the history. Use Inf to go all the way.
nums <- undomanager()$do(5)$do(7)$do(10)$do(12)
nums$undo(2)$value
#> [1] 7
nums$redo(Inf)$value
#> [1] 12NULL
NULL is a value like any other, so it can be stored and
undone:
nums <- undomanager()$do(5)$do(NULL)$do(10)
nums$undo()$value
#> NULL
nums$undo()$value
#> [1] 5Because an empty manager also reports a NULL value, use
is_empty to tell the two apart:
undomanager()$value
#> NULL
undomanager()$is_empty
#> [1] TRUE
undomanager()$do(NULL)$value
#> NULL
undomanager()$do(NULL)$is_empty
#> [1] FALSEWhen a type is set, storing NULL also
requires setting allow_null:
nums <- undomanager("numeric", allow_null = TRUE)
nums$do(5)
nums$do(NULL)Items are stored exactly as they are given, without being copied. For most common objects (vectors, lists, data frames), R’s copy-on-modify behaviour means the history is effectively a snapshot, so changing your own copy afterwards doesn’t affect it.
Environments and R6 objects are different: they’re stored by reference, which means modifying one after adding it also changes what the history holds.
env <- new.env()
env$val <- "before"
hist <- undomanager()$do(env)$do("something else")
env$val <- "after"
hist$undo()$value$val
#> [1] "after"If you want the history to be a true snapshot of a reference object, store a copy of it yourself:
hist$do(as.environment(as.list(env, all.names = TRUE))) # environments
hist$do(obj$clone(deep = TRUE)) # R6 objects{undomanager} can also be fully reactive and integrate with shiny
smoothly. You just need to call $reactive() on the
UndoManager object and use it as a reactive variable:
library(shiny)
ui <- fluidPage(
shinyjs::useShinyjs(),
numericInput("num", "Choose a number", 5),
actionButton("save", "Save"),
actionButton("undo", NULL, icon = icon("undo"), title = "Undo"),
actionButton("redo", NULL, icon = icon("redo"), title = "Redo"),
actionButton("clear", NULL, icon = icon("refresh"), title = "Clear"),
verbatimTextOutput("stack")
)
server <- function(input, output, session) {
undoredo <- undomanager(type = c("numeric"))$reactive()
observeEvent(input$save, {
req(input$num)
undoredo()$do(input$num)
})
observeEvent(input$undo, {
undoredo()$undo()
})
observeEvent(input$redo, {
undoredo()$redo()
})
observeEvent(input$clear, {
undoredo()$clear()
})
observe({
shinyjs::toggleState("undo", undoredo()$can_undo)
shinyjs::toggleState("redo", undoredo()$can_redo)
})
output$stack <- renderPrint({
undoredo()
})
}
shinyApp(ui, server)