@gira-de/svelte-undo
Provides low-level utility functions to use Svelte Stores in combination with redo/undo capabilities. Relies on immer.js.
Example App
Features
- UndoStack contains the history of all changes
- Undo/Redo/Goto reverts the model to a specific state
- Transactions map multiple changes in one undo step
- Commit Messages for every undo stack entry
- immer.js keeps the undo stack small by using patches
- Snapshots export/import the undo stack to/from JSON
- Typescript support
Installation
# using npm
npm install -D @gira-de/svelte-undo
# or using pnpm
pnpm install -D @gira-de/svelte-undo
# or using yarn
yarn add -D @gira-de/svelte-undo
Usage examples
Push actions to undo stack
import { undoStack, SetAction } from '@gira-de/svelte-undo';
// create undo stack
const myUndoStack = undoStack('first stack message');
const msgStore = writable('old value');
// create a new action to update the store value
const action = new SetAction(msgStore, 'new value', 'set new value');
// apply the action
action.apply();
console.log($msgStore); // 'new value'
// push action onto the undo stack
myUndoStack.push(action);
// call undo() to revert the changes
myUndoStack.undo();
console.log($msgStore); // 'old value'
Use transactions
Creating actions and manually pushing them to the undo stack might create a lot of boilerplate code. The use of a Transaction simplifies this.
import { undoStack, transactionCtrl } from '@gira-de/svelte-undo';
// create a store as usual
const personStore = writable({ name: 'John', age: '23' });
// create undo stack and a transaction controller
const myUndoStack = undoStack('created');
const myTransactionCtrl = transactionCtrl(myUndoStack);
// apply model changes on the draft state
let personDraft = myTransactionCtrl.draft(personStore);
personDraft['age'] = 24;
// apply all changes made to the drafts
myTransactionCtrl.commit('happy birthday');
console.log($personStore); // { name: 'John', age: '24' }
// call undo() to revert the changes
myUndoStack.undo();
console.log($personStore); // { name: 'John', age: '23' }
Limitations: The transaction controller can only be used with Svelte stores that hold an Object-like value (object, array, map, set).
Save from & load to undo stack
import { undoStack, transactionCtrl } from '@gira-de/svelte-undo';
// push an undo step to the undo stack
const myUndoStack = undoStack('created');
const myTransactionCtrl = transactionCtrl(myUndoStack);
const personStore = writable({ name: 'John', age: '23' });
myTransactionCtrl.draft(personStore)['age'] = 24;
myTransactionCtrl.commit('happy birthday');
// provide a store id for each store used in the undo stack
const stores = {
person: personStore,
};
// create a snapshot
const undoStackSnapshot = myUndoStack.createSnapshot(stores);
// snapshot can easily be stringified to json
const json = JSON.stringify(undoStackSnapshot);
console.log(json);
// {
// index: 1,
// actions: [
// { type: 'init', msg: 'created' },
// { type: 'mutate', msg: 'happy birthday', storeId: 'person', data: ... }
// ]
// }
// later: load the undo stack snapshot
myUndoStack.loadSnapshot(JSON.parse(json), stores);
Documentation
undoStack
The undoStack is basically a Svelte store with various properties and functions. It always contains at least one undo step (action).
Instantiation examples:
const myUndoStack = undoStack('first undo stack message');
const myUndoStack = undoStack({ user: 'xyz', msg: 'project created' });
Properties
- $myUndoStack.actions
- a list of all actions that are currently on the undo stack
- $myUndoStack.selectedAction
- the current active step whose changes are applied to the model
- $myUndoStack.canUndo
- true if the selectedAction is not the first action on the undo stack
- false otherwise
- $myUndoStack.canRedo
- true if the selectedAction is not the last action on the undo stack
- false otherwise
- $myUndoStack.index
- the index of the current selectedAction
- e.g. is 0 if canUndo is false
Functions
- myUndoStack.push(action)
- pushes a new action onto the undo stack and selects the action
- does not apply the action state, this has to be done manually
- it's recommended to use the transaction controller instead of pushing actions manually
- myUndoStack.undo()
- reverts the state of the selected action and selects the previous action
- does nothing if there's no previous action on the undo stack
- myUndoStack.redo()
- selects the next action and applies its state
- does nothing if there's no next action on the undo stack
- myUndoStack.goto(seqNbr)
- selects the action for the specified sequence number (action.seqNbr) and applies/reverts all actions between
- has basically the same effect as calling undo/redo in a loop
- myUndoStack.clear()
- removes all actions from the undo stack and creates a new init action
- has the same effect as if a new undo stack has been created
- myUndoStack.clearRedo()
- Removes all redoable actions from the stack
- This is the same as would happen if a new action is pushed while not being at the latest state,
just without pushing the action
- myUndoStack.clearUndo()
- Deletes all previous undo actions
- If the current position is the top of the stack, this effectively deletes the whole undo stack
- If not at the top of the stack, undo actions are still available and undo is possible
- myUndoStack.createSnapshot(stores)
- creates and returns a snapshot of the undo stack
- the snapshot can be easily serialized since it does not contain any store references etc.
- myUndoStack.loadSnapshot(undoStackSnapshot, stores)
- clears the undo stack and then loads the specified snapshot
- myUndoStack.erase(seqNbr?)
- removes undo/redo capability from actions to reduce the size of the undo stack
- all actions starting from the specified sequence number are erased
- starts erasing from the top of the stack if seqNbr is undefined
- erased actions are still included on the undo stack in form of log entries
- this function is currently experimental and might not always yield the expected state
transactionCtrl
Instantiation
const myTransactionCtrl = transactionCtrl(myUndoStack);
Functions
- myTransactionCtrl.draft(store)
- returns a new draft object for the specified store
- all changes to the draft object must be followed by a commit, otherwise they will not visible in the store
- myTransactionCtrl.commit(msg)
- applies all draft changes to the corresponding stores
- calling commit without having any draft changes has no effect
- myTransactionCtrl.rollback()
- discards all draft changes since the last commit
- calling commit after a rollback has no effect
Known issues
- Working with arrays (pushing/removing items) might create very large undo stack entries.
Contributing
Contributions are always welcome! Please have a look at our CONTRIBUTING.md