{"id":13788488,"url":"https://github.com/Day8/re-frame-undo","last_synced_at":"2025-05-12T02:33:22.542Z","repository":{"id":52612552,"uuid":"63834250","full_name":"day8/re-frame-undo","owner":"day8","description":"An undo library for re-frame","archived":false,"fork":false,"pushed_at":"2023-01-21T08:44:58.000Z","size":184,"stargazers_count":84,"open_issues_count":0,"forks_count":7,"subscribers_count":13,"default_branch":"master","last_synced_at":"2025-04-09T21:25:15.535Z","etag":null,"topics":["re-frame","undo-redo"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/day8.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":"mike-thompson-day8"}},"created_at":"2016-07-21T03:34:16.000Z","updated_at":"2025-03-22T20:27:29.000Z","dependencies_parsed_at":"2023-02-12T08:45:33.440Z","dependency_job_id":null,"html_url":"https://github.com/day8/re-frame-undo","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/day8%2Fre-frame-undo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/day8%2Fre-frame-undo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/day8%2Fre-frame-undo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/day8%2Fre-frame-undo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/day8","download_url":"https://codeload.github.com/day8/re-frame-undo/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253662800,"owners_count":21944133,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["re-frame","undo-redo"],"created_at":"2024-08-03T21:00:47.835Z","updated_at":"2025-05-12T02:33:22.180Z","avatar_url":"https://github.com/day8.png","language":"Clojure","funding_links":["https://github.com/sponsors/mike-thompson-day8"],"categories":["front-end","Tools"],"sub_categories":["Utils"],"readme":"\u003e However hard you try to look behind you, \u003cbr\u003e\n\u003e there’s a behind you, behind you, \u003cbr\u003e \n\u003e where you aren’t looking. \u003cbr\u003e\n\u003e -- Terry Pratchett, Going Postal\n\n\u003c!--\n[![CI](https://github.com/day8/re-frame-undo/workflows/ci/badge.svg)](https://github.com/day8/re-frame-undo/actions?workflow=ci)\n[![CD](https://github.com/day8/re-frame-undo/workflows/cd/badge.svg)](https://github.com/day8/re-frame-undo/actions?workflow=cd)\n[![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/day8/re-frame-undo?style=flat)](https://github.com/day8/re-frame-undo/tags)\n[![GitHub pull requests](https://img.shields.io/github/issues-pr/day8/re-frame-undo)](https://github.com/day8/re-frame-undo/pulls)\n--\u003e\n[![Clojars Project](https://img.shields.io/clojars/v/day8.re-frame/undo?style=for-the-badge\u0026logo=clojure\u0026logoColor=fff)](https://clojars.org/day8.re-frame/undo)\n[![GitHub issues](https://img.shields.io/github/issues-raw/day8/re-frame-undo?style=for-the-badge)](https://github.com/day8/re-frame-undo/issues)\n[![License](https://img.shields.io/github/license/day8/re-frame-undo?style=for-the-badge)](LICENSE)\n\n## Undos in re-frame\n\nThis library provides undo/redo capabilities for [re-frame](https://github.com/day8/re-frame).\n\n## Quick Start Guide\n\n### Step 1. Add Dependency\n\nAdd the following project dependency: \u003cbr\u003e\n[![clojars](https://img.shields.io/clojars/v/day8.re-frame/undo?style=for-the-badge\u0026logo=clojure\u0026logoColor=fff)](https://clojars.org/day8.re-frame/undo)\n\nYou'll also need re-frame \u003e= 0.8.0\n\n### Step 2. Registration and Use\n\nIn the namespace where you register your event handlers, perhaps \ncalled `events.cljs`, add this \"require\" to the `ns`:\n```clj\n(ns my-app.events\n  (:require\n    ...\n    [day8.re-frame.undo :as undo :refer [undoable]]  \n    ...))\n```\n\nTo make an event handler undoable, use the `undoable` interceptor factory, like this:\n```clj\n(re-frame.core/reg-event-db         \n  :event-id\n  (undoable \"setting flag\")         ;; use \"undoable\" interceptor factory.  Provide string description\n  (fn [db event]                    ;; just a basic \"db\" handler. Note reg-event-db used to register it\n     (assoc db :flag true))\n```\nThis is a very convenient method. It removes any undo-related noise from the event handler itself. It is \nprobably the recommended way. It can be used with `-fx` event handlers in this form too. \n\n\n**The other way** is to use the `:undo` effect. \n```clj\n(re-frame.core/reg-event-fx         ;; effectful handler, so register using -fx variety \n  :event-id\n  (undoable)                        ;; you don't have to supply an explanation\n  (fn [{:key [db]} event]           ;; first parameter will be `coeffects`\n    {:db (assoc db :flag true)      ;; return effects\n     :undo \"setting flag\"}))        ;; provide the explanation via this effect\n```\n\nThis 2nd form is potentially more powerful, because you \ncan construct very customized  `:undo`  explanations in the handler.  But that's at the expense of\nmaking the handler itself more complicated.\n\nMany times you'll be happy with providing a static string description, in which case the first \ninterceptor \"lifts\" the undo related code out of the handler, keeping it simple. \n\n**Note 1:** if you do use the 2nd form, know the undo check-pointing always happens even if the \n`:undo` effect is absent.  That `undo` effect just allows you to give a fancy explanation - it is\n*not* used to indicate if the checkpoint should happen or not.  \n\n**Note 2:** always use a function call `(undoable)`. Don't think you can just use a bare `undoable`. \n\n## Tutorial \n\nEvent handlers cause change - they mutate `app-db`. Before an event handler runs, \n`app-db` is in one state and, after it has run, `app-db` will be in a new state.\n\nUndoing a user's action means reversing a mutation to `app-db`. For example, if\nthis happened `(dispatch [:delete-item 42])` we'd need to know how to reverse \nthe mutation it caused. But how?\n\nIn an OO system, you'd use the Command Pattern to store the reverse of each\nmutation. You'd remember that the reverse action for `(dispatch [:delete-item 42])` \nwas to put a certain item back into a certain collection, at a certain place. Of course, \nthat's often not enough.  What happens if there were error states associated with the \npresence of that item. They would have to be remembered, and reinstated too, etc.  \nThere can be a fragile conga line of dependencies.\n\nLuckily, our re-frame world is more simple than that. First, all the state is \nin one place (not distributed!). Second, **to reverse a mutation, we need only \nreinstate the version of `app-db` which existed prior to the mutation occurring**; \nprior to the event handler running.\n\nThat's it. Holus-Bolus we step the application state back one event.\n\nWhen the prior state is reinstated, the GUIs will rerender and, bingo, the app is back to where it was before the `(dispatch [:delete-item 42])` was processed.\n\nBecause the action is Holus-Bolus we are reinstating all derivative (conga line) state too, like calculated errors etc.\n\nSo, in summary, to create an undoable event:\n  1. immediately before the event handler runs, we save a copy of what's in `app-db`\n  2. run the event handler, presumably causing mutations to `app-db`\n  3. later, if we need to undo this event, we reinstate the saved-away `app-db` state.\n\n### Won't that Be Expensive?\n\nWon't saving copies of `app-db` take a lot of RAM?  If the user has performed 50 \nundoable actions,  I'd need 50 copies of app-db saved away! Yikkes.\n\nThis is unlikely to be a problem.  The value in `app-db` is a map, which is an immutable data structure. \nWhen an immutable data structure is changed (mutated), it shares as much state as possible with the \nprevious version of itself via \"structural sharing\".  Under the covers, our 50 copies will be sharing \na lot of state because they are 50 incremental revisions of the same map.\n\nSo, no, storing 50 copies is unlikely to be expensive. (Unless you replace huge swathes of app-db with each event)\n\n### Just Tell Me How To Do It\n\nYou add an interceptor to certain of your event handlers. That's it. The interceptor saves (checkpoints) the state\nof `app-db` allowing the mutations they perform by the event handlers to be easily undone.\n\nAs explained in the Quick Start guide, you will use the `undoable` function to create an interceptor. You must \ncall it: `(undoable \"explanation here\")`.  \n\nIf there is an `:undo` effect returned by the handler then the explanation it provides is always used. \n\n### Widgets\n\nThere's going to be widgets, right? There's got to be a way for the user to undo and redo. How do I write these widgets?\n\nInitially, to make it easier, let's assume our widgets are simple buttons: an undo button, and a redo button.\n\n\n\n##### Disabling The Buttons\n\nOur two buttons should be disabled if there's nothing to redo, or undo. To help, we provide two subscriptions:\n```clj\n;; Boolean. Is there anything to undo?\n(subscribe [:undos?])\n\n;; Boolean. Is there anything to redo? Ie. has the user undone one or more times?\n(subscribe [:redos?])\n```\n\n##### Button click\n\nAnd when our two buttons get clicked, how do we make it happen? We provide handlers for these two built in events:\n```clj\n(dispatch [:undo])\n(dispatch [:redo])\n```\n\n##### The Result\n\nUsing these built in features, here's how an undo button might be coded:\n```clj\n(defn undo-button\n  []\n  (let [undos? (subscribe [:undos?])]       ;; only enable the button when there's undos\n    (fn []\n      [:input {:type \"button\"\n       :value \"undo\"\n       :disabled (not @undos?)\n       :on-click #(dispatch [:undo])}]])))  ;; clicking will undo the latest action\n```\n\n### Navigating Undos\n\nSometimes buttons are not enough, and a more sophisticated user experience is needed.\n\nSome GUIs present to the user a list of their recent undoable actions, allow them to navigate this list, and then choose \"a point in time\" within these undos to which they'd like to undo. If they clicked 5 actions back in the list, then that will involve 5 undos.\n\nHow would we display a meaningful list? For example you might want to show:\n  - added new item at the top\n  - changed background color\n  - increased font size on title\n  - deleted 4th item\n  - etc\n\nAs you saw above, when you use `undoable` middleware, you can (optionally) \nprovide a string \"explanation\" for the action being performed. re-frame \nremembers and manages these explanations for you, and can provide them \nvia built-in subscriptions:\n```clj\n;; a vector of strings. Explanations ordered oldest to most recent\n(subscribe [:undo-explanations])\n\n;; a vector of strings. Ordered from most recent undo onward\n(subscribe [:redo-explanations])\n```\n\nIf the user chooses to undo multiple actions in one go, notice that you can supply an integer parameter in these events:\n```clj\n(dispatch [:undo 10])   ;; undo 10 actions\n(dispatch [:redo 10])   ;; redo 10 actions\n```\n\n###  Not Everything Should Be Undone\n\nIn my experience, you won't want every event handler to be `undoable`.\n\nFor example, `(dispatch [:undo])` itself should not be undoable. And when a \nhandler causes an HTTP GET, and then another handler processes the on-success\nresult, you'd probably only want the initial handler to be undoable, not the \non-success handler. Etc. To a user, the two step dispatch is one atomic operation \nand we only want to checkpoint app-db before the first.\n\nAnyway, this is easy; just don't put undoable middleware on event handlers which \nshould not checkpoint.\n\n\n### Harvesting And Re-Instating\n\nSometimes your `app-db` will contain state from remote sources (databases?) \nAND some local client-specific state. In such cases, you don't want to undo \nthe cached state from remote sources. The remote source \"owns\" that state, \nand it isn't something that the user can undo.\n\nInstead, you'd like to undo/redo **only part of app-db** (perhaps everything \nbelow a certain path) and leave the rest alone.\n\nGenerally, you only ever call this configuration function once, during startup:\n```clj\n(day8.re-frame.undo/undo-config! {:harvest-fn h :reinstate-fn r})\n```\n\n`h` is a function which \"obtains\" that state which should be remembered for \nundo/redo purposes. It takes one argument, which is `app-db`. The default \nimplementation of `h` is `deref` which, of course, simply harvests the entire value in `app-db`.\n\n`r` is a function which \"re-instates\" some state (previously harvested). It \ntakes two arguments, a ratom (`app-db`) and the `value` to be re-instated.\nThe default implementation of `r` is `reset!`.\n\nWith the following configuration, only the `[:a :b]` path within `app-db` will \nbe undone/re-done:\n```clj\n(day8.re-frame.undo/undo-config!\n  {:harvest-fn   (fn [ratom] (some-\u003e @ratom :a :b))\n   :reinstate-fn (fn [ratom value] (swap! ratom assoc-in [:a :b] value))})\n```\n\nAnd, with this configuration, only the `:c` and `:d` keys within `app-db` will be undone/redone:\n```clj\n(day8.re-frame.undo/undo-config!\n  {:harvest-fn  (fn [ratom] (select-keys @ratom [:c :d]))\n   :reinstate-fn (fn [ratom value] (swap! ratom merge value))})\n```\n\nIn a more complicated world, you could even choose \nto harvest state outside of `app-db`. The state of a sibling DataScript \ndatabase? Another ratom? Yes, these two `fn`s are given `app-db` but \nthey could pull data from further afield if necessary. Whatever you \nreturn from your `harvest-fn` will be stored (a vector?, a map?, anything), \nand then later it is expected that your `reinstate-fn` will know how to \nput the harvested values (maps?, vectors?, anything) back in the right places.\n\nSo this sort of flexibility is possible:\n```clj\n(day8.re-frame.undo/undo-config!\n  {:harvest-fn  (fn [ratom] [@cache @ratom])    ;; harvesting a vec of 2\n   :reinstate-fn (fn [ratom [v1 v2]] (reset! cache v1) (reset! ratom v2))})\n```\n\n### Multiple client db's\n\nIn certain situations, you may find yourself with an app with more than one `app-db`. While this is not typical of a re-frame app, there are certain situations where all of the state for whatever reason does not live in the same `app-db`. Undo/redo becomes more complicated in these situations, but can still be handled provided that:\n\n1. The event to be undone only affects one `app-db`.\n2. The affected `app-db` is known when the event is registered (i.e. when the `reg-event-db/fx` call is made).\n \nFor these situtations, a second argument to `undoable` can be provided which specifies the `app-db` to capture the undo state from. The undo handler for such events will remember which `app-db` the state change corresponds to and will restore it to the appropriate one.\n\n```clj\n(def my-db (atom {:hello 1}))\n\n(reg-event-fx ::add-1-to-my-db\n  (undoable \"Adding 1\" my-db)   ;; Optional second arg is a ratom or derefable like the regular re-frame app-db\n  (fn [_ _]\n    (swap! my-db update :hello inc))))\n\n;;@my-db -\u003e {:hello 1}\n(re-frame/dispatch-sync [::add-1-to-my-db])\n;;@my-db -\u003e {:hello 2}\n(re-frame/dispatch-sync [:undo])\n;;@my-db -\u003e {:hello 1}\n```\n\nWhen the second argument is not supplied, the undos only apply to the regular `app-db` as described above. \n\n\n### Further Configuration\n\nHow many undo steps do you want to keep? Defaults to 50.\n\nAgain, it is expected that you'd only ever call the configuration \nfunction once, during startup:\n```clj\n(day8.re-frame.undo/undo-config! {:max-undos 100})\n```\n\n\n### Fancy Explanations With `undoable`\n\nNormally, the `undoable` interceptor is simply called with a static explanation string like this:\n```clj\n(undoable \"change font size\")\n```\nbut you can get fancy if you want.\n\nYou can supply a function instead, and it will be called to generate the explanation.\n```clj\n(undoable my-string-generating-fn)\n```\nYour function will be called with arguments `db` and `event-vec` and it is expected to return a string explanation.\n\nOf course, as noted above, if a handler returns an `:undo` effect, the explanation it provides trumps all\nother methods of supplying the explanation.\n\n\n### App Triggered Undo\n\nApparently, some people's apps throw exceptions in production, sometimes.\nTo gracefully handle this, I've heard that they write an Unhandled Exception \nHandler which triggers an undo:\n```clj\n(dispatch [:undo])\n```\nThey want their application to step back to the last-known-good state. \nPotentially from there, the user can continue on.\n\nOf course, my programs never exception, so I don't need to worry about all this.\n\nI've also heard it said that some do this straight after the undo:\n```clj\n(dispatch [:purge-redos])\n```\nbecause they want to get rid for the redo caused by that undo.\n\nSo I've heard.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FDay8%2Fre-frame-undo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FDay8%2Fre-frame-undo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FDay8%2Fre-frame-undo/lists"}