undomanager

↩︎️ Manage the history of any object with undo/redo operations

by Dean Attali

R build status CRAN version


{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! ❤

Table of contents

How to use

After 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] 7

Sponsors 🏆

Developed, in part, as a component of stanpumpR (see https://github.com/StevenLShafer/stanpumpR).

Become a sponsor for {undomanager}!

Installation

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")

Restricting the type of items

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>

Undoing or redoing multiple steps

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] 12

Storing NULL

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] 5

Because 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] FALSE

When a type is set, storing NULL also requires setting allow_null:

nums <- undomanager("numeric", allow_null = TRUE)
nums$do(5)
nums$do(NULL)

Objects with reference semantics

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

Using with shiny

{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)