{"id":23588145,"url":"https://github.com/stilliard/js-state-rewind","last_synced_at":"2025-11-03T17:30:24.507Z","repository":{"id":57369939,"uuid":"252978804","full_name":"stilliard/js-state-rewind","owner":"stilliard","description":"JS StateRewind - simple state management with the ability to undo, redo \u0026 squash history","archived":false,"fork":false,"pushed_at":"2020-05-08T22:44:42.000Z","size":57,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-28T06:16:38.996Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","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/stilliard.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"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":"stilliard","patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2020-04-04T11:28:26.000Z","updated_at":"2020-05-08T22:44:45.000Z","dependencies_parsed_at":"2022-09-09T13:12:36.040Z","dependency_job_id":null,"html_url":"https://github.com/stilliard/js-state-rewind","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stilliard%2Fjs-state-rewind","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stilliard%2Fjs-state-rewind/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stilliard%2Fjs-state-rewind/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stilliard%2Fjs-state-rewind/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stilliard","download_url":"https://codeload.github.com/stilliard/js-state-rewind/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239418547,"owners_count":19635203,"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":[],"created_at":"2024-12-27T05:12:32.072Z","updated_at":"2025-11-03T17:30:24.469Z","avatar_url":"https://github.com/stilliard.png","language":"JavaScript","funding_links":["https://github.com/sponsors/stilliard"],"categories":[],"sub_categories":[],"readme":"# JS StateRewind [![Build Status](https://travis-ci.org/stilliard/js-state-rewind.svg?branch=master)](https://travis-ci.org/stilliard/js-state-rewind)\n\nSimple state management with the ability to undo, redo \u0026 squash history.\n\nWant to skip to a full working example? https://jsfiddle.net/p4rebqw2/15/\n\n-----------------------\n\n## Install\n\n```sh\nnpm install state-rewind\n```\n\n## Usage\n\nInit a state object\n```js\nconst state = new StateRewind;\n```\ninit with console logs for debug\n```js\nconst state = new StateRewind({ log: true });\n```\n\nSet the state\n```js\nstate.set(5);\n```\nor `exec` to run/execute a function at the same time that can later be undone or redone\n```js\nstate.exec(5, function () { console.log('foward'); }, function () { console.log('backward'); });\n```\n\nState can be anything, strings, numbers, arrays, objects, anything, e.g. (works with `set` \u0026 `exec`)\n```js\nstate.set({\n    x: \"test\",\n    y: [1, 2, 3]\n});\n```\n\nGet the current state\n```js\nstate.get();\n```\n\nUndo/rewind state change\n```js\nstate.undo();\n```\n\n\u0026 then redo/fast-forward current state\n```js\nstate.redo();\n```\n\nYou can also check if undo or redo are available with:\n```js\nstate.canUndo(); // boolean\nstate.canRedo(); // boolean\n```\n\nGet all recorded state changes\n```js\nstate.getAll();\n```\n\n### Callbacks\n\nOptionally listen in for changes to the state:\n```js\nstate.onChange(function () { ... });\n```\n\nEach set/exec can pass a forward and backward callback e.g. to handle the changes visually etc.\n\nYou can also set a default function that's given a direction of either forward or backward and the change where you can instruct how it should handle this.\ne.g.\n```js\nstate.setDefaultForwardBackwardCallback(function (direction, change) {\n    if (direction == 'forward') {\n        // do/redo it (e.g. on exec() or redo() or initial load() calls)\n    } else if (direction == 'backward') {\n        // undo it (e.g. on undo() calls)\n    }\n});\n```\n\n\n### Editing history\n\nSquash history, e.g. to remove duplicates or squash down similar objects such as changes to text, if the same elements text changes multiple times you might want to squash that down to just the latest change\n```js\nstate.squash(function (prev, next) {\n    return prev == next;\n});\n// or for an object\nstate.squash(function (prev, next) {\n    return prev.selector == next.selector \u0026\u0026 prev.type == next.type;\n});\n```\nor just run squash against the last set value\n```js\nstate.squashLast(function (prev, next) {\n    // same compare function as above\n    return prev == next;\n});\n```\nThe squash functions can both also take a 2nd callback to modify the data as it squashes down,\ne.g. if these are text changes to the same thing, you'd probably want the original \"from text\", but the latest \"to text\".\n```js\nstate.squashLast(function (prev, next) {\n    return prev.selector == next.selector \u0026\u0026 prev.type == next.type;\n}, function (prev, next) {\n    next.change.from = prev.change.from; // keep the initial \"from text\" as we squash down to the latest \"to text\"\n    return next;\n});\n```\n\nRemove specific entries from the history.\n```js\nstate.removeIndex(2); // index =\u003e starting at 0\n```\n\nClear/reset all history.\n```js\nstate.clear();\n```\n\n### Starting from stored data\n\nYou can load in initial data with the load() command:\n```js\nstate.load(data);\n```\n\nThis can be used in combination with setDefaultForwardBackwardCallback to have it auto run the callbacks such as visually changing the page to adapt to the loaded history.\n\n```js\nstate.setDefaultForwardBackwardCallback(function (direction, change) { ... });\n\nstate.load([{ text: \"x\" }, { text: \"y\" }], { exec: true });\n```\n\n### Initial locked states\n\nYou can also start the state with an initial value. By default it starts as `undefined` but you can set `initialState` as an option when creating the `StateRewind` instance. This lets you have a default state available via get() that cannot be undone and will always show at the start of the getAll() array.\n\nSee the `test.js` file for a unit test showing this in use \u0026 many other example uses.\n\n-----------------------\n\n### Tips\n\n#### Keyboard shortcuts\n\nYou may want to setup keyboard shortcuts for undo \u0026 redo.\n\nThis could be done like so, for ctrl+z (undo) \u0026 ctrl+y (redo)\n```js\ndocument.addEventListener('keydown', function (e) {\n    if (e.ctrlKey || e.metaKey) { // support either ctrl (win \u0026 linux) or cmd (mac)\n        if (e.key == 'y' || (e.key == 'Z' \u0026\u0026 e.shiftKey)) {\n            state.redo();\n        } else if (e.key == 'z') {\n            state.undo();\n        }\n    }\n});\n```\n\n#### Debounce\n\nDepending on how you're saving data, if it's user based such as on input, you may want to use this with a debounce function to not save constantly, [find out more here](https://davidwalsh.name/javascript-debounce-function) or here's a package for [debounce on npm](https://www.npmjs.com/package/debounce).\n\n#### Chaining\n\nAlmost all functions are chainable (except get, getAll, canUndo and canRedo).\n\nE.g.\n```js\nstate.set(3).set(5).undo().get()\n```\n\n#### Change events\n\nYou may want to hook into the change event of the state, we expose a onChange() function for this.\n\nThis is especially useful for setting up undo and redo buttons, e.g.\n```html\n\u003cbutton id=\"undo-btn\" disabled\u003eUndo\u003c/button\u003e\n\u003cbutton id=\"redo-btn\" disabled\u003eRedo\u003c/button\u003e\n```\n```js\nlet $undoBtn = document.querySelector('#undo-btn');\nlet $redoBtn = document.querySelector('#redo-btn');\nstate.onChange(function () {\n    $undoBtn.disabled = ! state.canUndo();\n    $redoBtn.disabled = ! state.canRedo();\n});\n$undoBtn.addEventListener('click', function (e) {\n    e.preventDefault();\n    state.undo();\n});\n$redoBtn.addEventListener('click', function (e) {\n    e.preventDefault();\n    state.redo();\n});\n```\n\n-----------------------\n\n### Example workflow\n\nE.g. here's an example where you could track changes to elements on a page with timestamps:\n\n```js\nconst state = new StateRewind;\n\n// first change\nstate.exec({\n    timestamp: (new Date).toISOString(),\n    selector: '#el span',\n    type: 'text',\n    change: {\n        from: 'ABC',\n        to: 'XYZ'\n    }\n}, function () { console.log('A'); }, function () { console.log('B'); });\n\n// another change\nstate.exec({\n    timestamp: (new Date).toISOString(),\n    selector: '.another strong',\n    type: 'text',\n    change: {\n        from: 'Hello world',\n        to: 'one thing'\n    }\n}, function () { console.log('C'); }, function () { console.log('D'); });\n\nstate.undo();\n\nstate.redo();\n\nstate.exec({\n    timestamp: (new Date).toISOString(),\n    selector: '.another strong',\n    type: 'text',\n    change: {\n        from: 'one thing',\n        to: 'Something else!'\n    }\n}, function () { console.log('E'); }, function () { console.log('F'); });\n\nstate.squashLast(function (prev, next) {\n    return prev.selector == next.selector \u0026\u0026 prev.type == next.type;\n});\n\nstate.getAll(); // should only show first and last due to the `squashLast` replacing the last 2\n```\n\n-----------------------\n\n## Local development\n\n### Run tests\n\n```sh\nnpm test\n```\n\n### Publish new version\n\nUpdate the `package.json`'s `version`, commit, push and then:\n```sh\nnpm publish\n```\n\n### Contributing\n\nPlease run the tests locally and add new tests for new features/options added.\nThank you.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstilliard%2Fjs-state-rewind","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstilliard%2Fjs-state-rewind","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstilliard%2Fjs-state-rewind/lists"}