Gathering detailed insights and metrics for ngrx-wieder
Gathering detailed insights and metrics for ngrx-wieder
Gathering detailed insights and metrics for ngrx-wieder
Gathering detailed insights and metrics for ngrx-wieder
npm install ngrx-wieder
Typescript
Module System
Node Version
NPM Version
TypeScript (97.41%)
JavaScript (2.59%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
MIT License
127 Stars
111 Commits
11 Forks
7 Watchers
5 Branches
3 Contributors
Updated on Jun 25, 2025
Latest Version
14.0.0
Package Id
ngrx-wieder@14.0.0
Unpacked Size
47.63 kB
Size
11.57 kB
File Count
11
NPM Version
10.8.2
Node Version
20.18.1
Published on
Apr 13, 2025
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
1
4
ngrx-wieder is a lightweight yet configurable solution for implementing undo-redo in Angular apps on top of NgRx. It's based on immer hence the name wieder (German for: again)
Make sure you're using immer to update your NgRx (sub-)state. That means you're using mutating APIs to update the state while immer provides a new state behind the scenes. If you're just starting out, install immer with this command:
1npm i immer
Install this library with the following command:
1npm i ngrx-wieder
Firstly, extend the UndoRedoState
with your custom state definition. This will prepare state properties for containing the undo-redo history. You can spread initialUndoRedoState
into your initial state to initialze these properties:
1// todo.state.ts 2import { UndoRedoState, initialUndoRedoState } from "ngrx-wieder"; 3 4export interface State extends UndoRedoState { 5 todos: Todo[]; 6} 7 8export const initialState: State = { 9 todos: [], 10 ...initialUndoRedoState, 11};
Then, you'll initialize ngrx-wieder and optionally pass a custom config. It'll return an object with the function createUndoRedoReducer
which you can use just like createReducer from NgRx, however, state
inside on
will be a immer draft of the last state. If you'd rather not return the state
in each on-reducer, you can use produceOn
instead.
Tip: Inside on
or produceOn
you can access the original state of an immer.js draft, therefore the last state, with the original function.
1// todo.reducer.ts 2import { undoRedo, produceOn } from "ngrx-wieder"; 3 4// initialize ngrx-wieder with custom config 5const { createUndoRedoReducer } = undoRedo({ 6 allowedActionTypes: [Actions.addTodo, Actions.removeTodo, Actions.toggleTodo], 7}); 8 9export const reducer = createUndoRedoReducer( 10 initialState, 11 on(Actions.addTodo, (state, { text }) => { 12 state.todos.push({ id: nextId(), text, checked: false }); 13 return state; 14 }), 15 on(Actions.toggleTodo, (state, { id }) => { 16 const todo = state.todos.find((t) => t.id === id); 17 todo.checked = !todo.checked; 18 return state; 19 }), 20 produceOn(Actions.removeTodo, (state, { id }) => { 21 state.todos.splice( 22 state.todos.findIndex((t) => t.id === id), 23 1 24 ); 25 }) 26);
Then whenever you'd like to undo or redo one of the passed allowedActionTypes
simply dispatch
the corresponding actions:
1this.store.dispatch({ type: "UNDO" }); 2this.store.dispatch({ type: "REDO" });
Option | Default | Description |
---|---|---|
allowedActionTypes | [] | Actions applicable for being undone/redone (leave empty to allow all actions) |
mergeActionTypes | [] | Types of actions whose state difference should be merged when they appear consecutively |
mergeRules | {} | Predicate dictionary for deciding whether differences from consecutive actions of the same type should be merged. Use action type as key and predicate as value. |
maxBufferSize | 32 | How many state differences should be buffered in either direction |
undoActionType | 'UNDO' | Override for the undo action's type |
redoActionType | 'REDO' | Override for the redo action's type |
breakMergeActionType | 'BREAK_MERGE' | Override for the break-merge action's type. |
clearActionType | 'CLEAR' | Override for the clear action's type |
trackActionPayload | false | Whether the action payloads should be kept in the history |
segmentationOverride | (action: Action) => undefined | Override for active segmentation based on key resolved from action |
You can access the undo-redo history through the included selectors after passing a selector that leads to your corresponding state feature to the createHistorySelectors
factory function (you can also pass a segmenter as a second parameter):
1// todo.selectors.ts 2import { createHistorySelectors } from "ngrx-wieder"; 3 4// in this example the whole state is undoable 5// otherwise return feature state 6const selectFeature = (state: State) => state; 7export const { 8 selectHistory, 9 selectCanUndo, 10 selectCanRedo, 11} = createHistorySelectors<State, State>(selectFeature);
The generated selectors could be used like this:
1import * as fromTodo from "../todo.selectors"; 2 3@Component({ 4 selector: "my-undo-redo", 5 template: ` 6 <button (click)="undo()" [disabled]="!(canUndo$ | async)">Undo</button> 7 <button (click)="undo()" [disabled]="!(canRedo$ | async)">Redo</button> 8 `, 9}) 10export class UndoRedoComponent { 11 canUndo$ = this.store.select(fromTodo.selectCanUndo()); 12 canRedo$ = this.store.select(fromTodo.selectCanRedo()); 13 14 constructor(private store: Store) {} 15}
If you're using segmentation, you can override the history key by passing an object with a key
property to the selector factory functions.
Sometimes you want to enable undo/redo in broader chunks than the ones you actually use for transforming your state. Take a range input for example:
1@Component({
2 selector: 'my-slider',
3 template: `
4 <input #rangeIn type="range" id="rangeIn" min="0" max="10" step="1"
5 (change)="rangeChange()" (input)="rangeInput(rangeIn.value)">
6 `
7})
8export class SliderComponent {
9
10 // ...
11
12 rangeChange() {
13 this.store.dispatch({ type: 'BREAK_MERGE' })
14 }
15
16 rangeInput(count: number) {
17 this.store.dispatch(new CountChange({ count })
18 }
19}
The method rangeInput
will be called for any step that the slider is moved by the user. This method
may also dispatch an action to update the state and thus display the result of moving the slider.
When the user now wants to revert changing the range input, he'd have to retrace every single step that
he moved the slider. Instead a more expectable redo behaviour would place the slider back where the
user picked it up before.
To facilitate this you can specify the CountChange
action as an action
whose state changes are merged consecutively by passing its type to the configuration property
mergeActionTypes
(you can even get more fine grained by using predicates through the mergeRules
property).
In order to break the merging at some point you can dispatch a special action of type BREAK_MERGE
.
A good place to do this for the range input would be inside the change input - which is called when the user drops the range knob (this is also covered in the example).
You can clear the stack for undoable and redoable actions by dispatching a special clearing action:
1this.store.dispatch({ type: "CLEAR" });
ngrx-wieder works by recording patches from immer and applying them based on dispatch of actions for perfoming undo and redo. While createUndoRedoReducer
handles interaction with immer, this is not possible when you're using a reducer that is based on a switch-statement. In that case the reducer on which you want to apply the undo-redo feature has to update the NgRx state directly through immer. In order to let ngrx-wieder record the changes your reducer has to be adapted so that it can forward the patches from immer:
Before
1import { produce } from "immer"; 2 3const reducer = (state: State, action: Actions): State => 4 produce(state, (nextState) => { 5 switch ( 6 action.type 7 /* action handling */ 8 ) { 9 } 10 });
After
1import { produce, PatchListener } from "immer"; 2 3const reducer = ( 4 state: State, 5 action: Actions, 6 patchListener?: PatchListener 7): State => 8 produce( 9 state, 10 (nextState) => { 11 switch ( 12 action.type 13 /* action handling */ 14 ) { 15 } 16 }, 17 patchListener 18 );
Next you'll configure the undo-redo behaviour by instantiating undoRedo
and wrapping
your custom reducer with the wrapReducer
function:
1import { undoRedo } from "ngrx-wieder";
2
3// initialize ngrx-wieder
4const { wrapReducer } = undoRedo({
5 allowedActionTypes: [Actions.addTodo, Actions.removeTodo, Actions.toggleTodo],
6});
7
8// wrap reducer inside meta-reducer to make it undoable
9const undoableReducer = wrapReducer(reducer);
10
11// wrap into exported function to keep Angular AOT working
12export function myReducer(state = initialState, action) {
13 return undoableReducer(state, action);
14}
Segmentation provides distinct undo-redo stacks for multiple instances of the same kind of state. For example, this allows you to implement an application that can have multiple documents open at the same time in multiple tabs as illustrated by this state:
1interface State { 2 activeDocument: string; 3 documents: { [id: string]: Document }; 4 canUndo: boolean; 5 canRedo: boolean; 6}
Now, when the user is viewing one document, he probably doesn't want to undo changes in a different one. In order to make this work, you need to inform ngrx-wieder about your segmentation by using createSegmentedUndoRedoReducer
providing a segmenter. Note that any actions that change the result of the segmenter must not be undoable (here it's documentSwitch
). Moreover, when tracking is active, canUndo
and canRedo
will reflect the active undo-redo stack.
1// helper function for manipulating active document in reducer 2const activeDocument = (state: TestState): Document => 3 state.documents[state.activeDocument]; 4 5const { createSegmentedUndoRedoReducer } = undoRedo({ 6 allowedActionTypes: [nameChange.type], 7 track: true, 8}); 9 10const reducer = createSegmentedUndoRedoReducer( 11 initialState, 12 (state) => state.activeDocument, // segmenter identifying undo-redo stack 13 produceOn(nameChange, (state, action) => { 14 activeDocument(state).name = action.name; 15 }), 16 produceOn(documentSwitch, (state, action) => { 17 state.activeDocument = action.document; 18 }) 19);
When you're using a switch-based reducer, simply pass the segmenter as a second argument to wrapReducer
:
1const {wrapReducer} = undoRedo({...}) 2const reducer = (state = initialState, action: Actions, listener?: PatchListener): State => 3 produce(state, next => { 4 switch (action.type) { 5 case nameChange.type: 6 activeDocument(next).name = action.name 7 return 8 case documentSwitch.type: 9 next.activeDocument = action.document 10 return 11 } 12 }, listener) 13return wrapReducer(reducer, state => state.activeDocument)
You can override the segmentation based on an action by providing segmentationOverride
to the config. This way you can target a specific - possibly non-active - segment with actions. For example, the actions from above could contain an optional property targetDocument
which you'd resolve with the following segmentationOverride
:
1const {createSegmentedUndoRedoReducer} = undoRedo({ 2 ... 3 segmentationOverride: action => action.targetDocument 4})
No vulnerabilities found.
Reason
no dangerous workflow patterns detected
Reason
no binaries found in the repo
Reason
license file detected
Details
Reason
6 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 5
Reason
dependency not pinned by hash detected -- score normalized to 4
Details
Reason
Found 1/28 approved changesets -- score normalized to 0
Reason
detected GitHub workflow tokens with excessive permissions
Details
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
security policy file not detected
Details
Reason
project is not fuzzed
Details
Reason
branch protection not enabled on development/release branches
Details
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
Reason
17 existing vulnerabilities detected
Details
Score
Last Scanned on 2025-07-07
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More