From 3e5fa264eb2d8a484c671b79f0ad34d96f35211b Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sat, 18 Oct 2025 22:44:00 +0200 Subject: [PATCH 1/8] chore: deprecate clearStack --- libs/ngrx-toolkit/src/lib/with-undo-redo.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/ngrx-toolkit/src/lib/with-undo-redo.ts b/libs/ngrx-toolkit/src/lib/with-undo-redo.ts index 3527530..4c9571e 100644 --- a/libs/ngrx-toolkit/src/lib/with-undo-redo.ts +++ b/libs/ngrx-toolkit/src/lib/with-undo-redo.ts @@ -105,6 +105,7 @@ export function withUndoRedo( canRedo: canRedo.asReadonly(), })), withMethods((store) => ({ + __clearUndoRedo__: () => ({}), undo(): void { const item = undoStack.pop(); @@ -135,6 +136,7 @@ export function withUndoRedo( updateInternal(); }, + /** @deprecated Use {@link clearUndoRedo} instead. */ clearStack(): void { undoStack.splice(0); redoStack.splice(0); From de008d5d7a5aa17776af432b832c6cc9f500502e Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sat, 18 Oct 2025 23:19:32 +0200 Subject: [PATCH 2/8] feat(undoRedo): introduce standalone helper to clear undoRedoStacks --- libs/ngrx-toolkit/src/index.ts | 2 +- .../src/lib/undo-redo/clear-undo-redo.spec.ts | 28 ++++++++++++++ .../src/lib/undo-redo/clear-undo-redo.ts | 37 +++++++++++++++++++ .../{ => undo-redo}/with-undo-redo.spec.ts | 3 +- .../src/lib/{ => undo-redo}/with-undo-redo.ts | 11 +++++- 5 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.spec.ts create mode 100644 libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.ts rename libs/ngrx-toolkit/src/lib/{ => undo-redo}/with-undo-redo.spec.ts (98%) rename libs/ngrx-toolkit/src/lib/{ => undo-redo}/with-undo-redo.ts (92%) diff --git a/libs/ngrx-toolkit/src/index.ts b/libs/ngrx-toolkit/src/index.ts index 8b8bba8..e93029f 100644 --- a/libs/ngrx-toolkit/src/index.ts +++ b/libs/ngrx-toolkit/src/index.ts @@ -18,11 +18,11 @@ export { withRedux, } from './lib/with-redux'; +export * from './lib/undo-redo/with-undo-redo'; export * from './lib/with-call-state'; export * from './lib/with-data-service'; export * from './lib/with-pagination'; export { setResetState, withReset } from './lib/with-reset'; -export * from './lib/with-undo-redo'; export { withImmutableState } from './lib/immutable-state/with-immutable-state'; export { withIndexedDB } from './lib/storage-sync/features/with-indexed-db'; diff --git a/libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.spec.ts b/libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.spec.ts new file mode 100644 index 0000000..d08f2fe --- /dev/null +++ b/libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.spec.ts @@ -0,0 +1,28 @@ +import { TestBed } from '@angular/core/testing'; +import { signalStore, withState } from '@ngrx/signals'; +import { clearUndoRedo } from './clear-undo-redo'; +import { withUndoRedo } from './with-undo-redo'; + +describe('withUndoRedo', () => { + describe('clearUndoRedo', () => { + it('should throw an error if the store is not configured with withUndoRedo()', () => { + const Store = signalStore({ providedIn: 'root' }, withState({})); + const store = TestBed.inject(Store); + + expect(() => clearUndoRedo(store)).toThrow( + 'Cannot clear undoRedo, since store is not configured with withUndoRedo()', + ); + }); + + it('should not throw no error if the store is configured with withUndoRedo()', () => { + const Store = signalStore( + { providedIn: 'root' }, + withState({}), + withUndoRedo(), + ); + const store = TestBed.inject(Store); + + expect(() => clearUndoRedo(store)).not.toThrow(); + }); + }); +}); diff --git a/libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.ts b/libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.ts new file mode 100644 index 0000000..763c22d --- /dev/null +++ b/libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.ts @@ -0,0 +1,37 @@ +import { StateSource } from '@ngrx/signals'; + +export type ClearUndoRedoOptions = { + lastRecord: TState[keyof TState] | null; +}; + +export type ClearUndoRedoFn = ( + opts?: ClearUndoRedoOptions, +) => void; + +export function clearUndoRedo( + store: StateSource, + opts?: ClearUndoRedoOptions, +): void { + if (canClearUndoRedo(store)) { + store.__clearUndoRedo__(opts); + } else { + throw new Error( + 'Cannot clear undoRedo, since store is not configured with withUndoRedo()', + ); + } +} + +function canClearUndoRedo( + store: StateSource, +): store is StateSource & { + __clearUndoRedo__: ClearUndoRedoFn; +} { + if ( + '__clearUndoRedo__' in store && + typeof store.__clearUndoRedo__ === 'function' + ) { + return true; + } else { + return false; + } +} diff --git a/libs/ngrx-toolkit/src/lib/with-undo-redo.spec.ts b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.spec.ts similarity index 98% rename from libs/ngrx-toolkit/src/lib/with-undo-redo.spec.ts rename to libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.spec.ts index 8edaeb0..5f49e6a 100644 --- a/libs/ngrx-toolkit/src/lib/with-undo-redo.spec.ts +++ b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.spec.ts @@ -9,7 +9,7 @@ import { withState, } from '@ngrx/signals'; import { addEntity, withEntities } from '@ngrx/signals/entities'; -import { withCallState } from './with-call-state'; +import { withCallState } from '../with-call-state'; import { withUndoRedo } from './with-undo-redo'; const testState = { test: '' }; @@ -32,6 +32,7 @@ describe('withUndoRedo', () => { 'canRedo', 'undo', 'redo', + '__clearUndoRedo__', 'clearStack', ]); }); diff --git a/libs/ngrx-toolkit/src/lib/with-undo-redo.ts b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts similarity index 92% rename from libs/ngrx-toolkit/src/lib/with-undo-redo.ts rename to libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts index 4c9571e..c7c9f10 100644 --- a/libs/ngrx-toolkit/src/lib/with-undo-redo.ts +++ b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts @@ -10,7 +10,8 @@ import { withHooks, withMethods, } from '@ngrx/signals'; -import { capitalize } from './with-data-service'; +import { capitalize } from '../with-data-service'; +import { ClearUndoRedoOptions } from './clear-undo-redo'; export type StackItem = Record; @@ -69,6 +70,7 @@ export function withUndoRedo( methods: { undo: () => void; redo: () => void; + /** @deprecated Use {@link clearUndoRedo} instead. */ clearStack: () => void; }; } @@ -105,7 +107,6 @@ export function withUndoRedo( canRedo: canRedo.asReadonly(), })), withMethods((store) => ({ - __clearUndoRedo__: () => ({}), undo(): void { const item = undoStack.pop(); @@ -136,6 +137,12 @@ export function withUndoRedo( updateInternal(); }, + __clearUndoRedo__(opts?: ClearUndoRedoOptions): void { + undoStack.splice(0); + redoStack.splice(0); + previous = null; + updateInternal(); + }, /** @deprecated Use {@link clearUndoRedo} instead. */ clearStack(): void { undoStack.splice(0); From dbd3b820b050e3fc113a0f526da461ddf59d7733 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sat, 18 Oct 2025 23:33:02 +0200 Subject: [PATCH 3/8] feat(undoRedo): call __clearUndoRedo__ from when deprecated clearStack is executed --- libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts index c7c9f10..62d8513 100644 --- a/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts +++ b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts @@ -143,12 +143,11 @@ export function withUndoRedo( previous = null; updateInternal(); }, + })), + withMethods((store) => ({ /** @deprecated Use {@link clearUndoRedo} instead. */ clearStack(): void { - undoStack.splice(0); - redoStack.splice(0); - previous = null; - updateInternal(); + store.__clearUndoRedo__(); }, })), withHooks({ From fcf394496d298c7d9a7d950e7f50719d20f9c347 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sat, 18 Oct 2025 23:45:27 +0200 Subject: [PATCH 4/8] feat(undo/redo): allow setting lastRecord --- .../src/lib/undo-redo/clear-undo-redo.ts | 15 ++++----- .../src/lib/undo-redo/models/stack-item.ts | 1 + .../src/lib/undo-redo/with-undo-redo.spec.ts | 3 +- .../src/lib/undo-redo/with-undo-redo.ts | 33 ++++++++++--------- 4 files changed, 27 insertions(+), 25 deletions(-) create mode 100644 libs/ngrx-toolkit/src/lib/undo-redo/models/stack-item.ts diff --git a/libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.ts b/libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.ts index 763c22d..70da65d 100644 --- a/libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.ts +++ b/libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.ts @@ -1,16 +1,15 @@ import { StateSource } from '@ngrx/signals'; +import { StackItem } from './models/stack-item'; -export type ClearUndoRedoOptions = { - lastRecord: TState[keyof TState] | null; +export type ClearUndoRedoOptions = { + lastRecord: StackItem | null; }; -export type ClearUndoRedoFn = ( - opts?: ClearUndoRedoOptions, -) => void; +export type ClearUndoRedoFn = (opts?: ClearUndoRedoOptions) => void; export function clearUndoRedo( store: StateSource, - opts?: ClearUndoRedoOptions, + opts?: ClearUndoRedoOptions, ): void { if (canClearUndoRedo(store)) { store.__clearUndoRedo__(opts); @@ -23,9 +22,7 @@ export function clearUndoRedo( function canClearUndoRedo( store: StateSource, -): store is StateSource & { - __clearUndoRedo__: ClearUndoRedoFn; -} { +): store is StateSource & { __clearUndoRedo__: ClearUndoRedoFn } { if ( '__clearUndoRedo__' in store && typeof store.__clearUndoRedo__ === 'function' diff --git a/libs/ngrx-toolkit/src/lib/undo-redo/models/stack-item.ts b/libs/ngrx-toolkit/src/lib/undo-redo/models/stack-item.ts new file mode 100644 index 0000000..500d9ae --- /dev/null +++ b/libs/ngrx-toolkit/src/lib/undo-redo/models/stack-item.ts @@ -0,0 +1 @@ +export type StackItem = Record; diff --git a/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.spec.ts b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.spec.ts index 5f49e6a..5997dc2 100644 --- a/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.spec.ts +++ b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.spec.ts @@ -10,6 +10,7 @@ import { } from '@ngrx/signals'; import { addEntity, withEntities } from '@ngrx/signals/entities'; import { withCallState } from '../with-call-state'; +import { clearUndoRedo } from './clear-undo-redo'; import { withUndoRedo } from './with-undo-redo'; const testState = { test: '' }; @@ -261,7 +262,7 @@ describe('withUndoRedo', () => { store.update('Gordon'); - store.clearStack(); + clearUndoRedo(store, { lastRecord: null }); // After clearing the undo/redo stack, there is no previous item anymore. // The following update becomes the first value. diff --git a/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts index 62d8513..71ba4ba 100644 --- a/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts +++ b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts @@ -12,8 +12,7 @@ import { } from '@ngrx/signals'; import { capitalize } from '../with-data-service'; import { ClearUndoRedoOptions } from './clear-undo-redo'; - -export type StackItem = Record; +import { StackItem } from './models/stack-item'; export type NormalizedUndoRedoOptions = { maxStackSize: number; @@ -75,7 +74,7 @@ export function withUndoRedo( }; } > { - let previous: StackItem | null = null; + let lastRecord: StackItem | null = null; let skipOnce = false; const normalized = { @@ -110,14 +109,14 @@ export function withUndoRedo( undo(): void { const item = undoStack.pop(); - if (item && previous) { - redoStack.push(previous); + if (item && lastRecord) { + redoStack.push(lastRecord); } if (item) { skipOnce = true; patchState(store, item); - previous = item; + lastRecord = item; } updateInternal(); @@ -125,22 +124,26 @@ export function withUndoRedo( redo(): void { const item = redoStack.pop(); - if (item && previous) { - undoStack.push(previous); + if (item && lastRecord) { + undoStack.push(lastRecord); } if (item) { skipOnce = true; patchState(store, item); - previous = item; + lastRecord = item; } updateInternal(); }, - __clearUndoRedo__(opts?: ClearUndoRedoOptions): void { + __clearUndoRedo__(opts?: ClearUndoRedoOptions): void { undoStack.splice(0); redoStack.splice(0); - previous = null; + + if (opts) { + lastRecord = opts.lastRecord; + } + updateInternal(); }, })), @@ -182,22 +185,22 @@ export function withUndoRedo( // if the component sends back the undone filter // to the store. // - if (JSON.stringify(cand) === JSON.stringify(previous)) { + if (JSON.stringify(cand) === JSON.stringify(lastRecord)) { return; } // Clear redoStack after recorded action redoStack.splice(0); - if (previous) { - undoStack.push(previous); + if (lastRecord) { + undoStack.push(lastRecord); } if (redoStack.length > normalized.maxStackSize) { undoStack.unshift(); } - previous = cand; + lastRecord = cand; // Don't propogate current reactive context untracked(() => updateInternal()); From d97a470bbfa3a8b57e2ba21640e467100afc5987 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sat, 18 Oct 2025 23:57:41 +0200 Subject: [PATCH 5/8] refactor(undo/redo): enhance ClearUndoRedoOptions to support actual state types --- .../src/lib/undo-redo/clear-undo-redo.ts | 19 ++++++++------- .../src/lib/undo-redo/with-undo-redo.spec.ts | 24 +++++++++++++++++++ .../src/lib/undo-redo/with-undo-redo.ts | 2 +- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.ts b/libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.ts index 70da65d..7a3652f 100644 --- a/libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.ts +++ b/libs/ngrx-toolkit/src/lib/undo-redo/clear-undo-redo.ts @@ -1,15 +1,16 @@ import { StateSource } from '@ngrx/signals'; -import { StackItem } from './models/stack-item'; -export type ClearUndoRedoOptions = { - lastRecord: StackItem | null; +export type ClearUndoRedoOptions = { + lastRecord: Partial | null; }; -export type ClearUndoRedoFn = (opts?: ClearUndoRedoOptions) => void; +export type ClearUndoRedoFn = ( + opts?: ClearUndoRedoOptions, +) => void; -export function clearUndoRedo( - store: StateSource, - opts?: ClearUndoRedoOptions, +export function clearUndoRedo( + store: StateSource, + opts?: ClearUndoRedoOptions, ): void { if (canClearUndoRedo(store)) { store.__clearUndoRedo__(opts); @@ -22,7 +23,9 @@ export function clearUndoRedo( function canClearUndoRedo( store: StateSource, -): store is StateSource & { __clearUndoRedo__: ClearUndoRedoFn } { +): store is StateSource & { + __clearUndoRedo__: ClearUndoRedoFn; +} { if ( '__clearUndoRedo__' in store && typeof store.__clearUndoRedo__ === 'function' diff --git a/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.spec.ts b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.spec.ts index 5997dc2..e99d78b 100644 --- a/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.spec.ts +++ b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.spec.ts @@ -272,5 +272,29 @@ describe('withUndoRedo', () => { expect(store.canUndo()).toBe(false); expect(store.canRedo()).toBe(false); }); + + it('can undo after setting lastRecord', () => { + const Store = signalStore( + { providedIn: 'root' }, + withState(testState), + withMethods((store) => ({ + update: (value: string) => patchState(store, { test: value }), + })), + withUndoRedo({ keys: testKeys }), + ); + + const store = TestBed.inject(Store); + + store.update('Alan'); + + store.update('Gordon'); + + clearUndoRedo(store, { lastRecord: { test: 'Joan' } }); + + store.update('Hugh'); + + expect(store.canUndo()).toBe(true); + expect(store.canRedo()).toBe(false); + }); }); }); diff --git a/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts index 71ba4ba..e566090 100644 --- a/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts +++ b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts @@ -136,7 +136,7 @@ export function withUndoRedo( updateInternal(); }, - __clearUndoRedo__(opts?: ClearUndoRedoOptions): void { + __clearUndoRedo__(opts?: ClearUndoRedoOptions): void { undoStack.splice(0); redoStack.splice(0); From b1aa2832a2c0ddcf97913e8337835f929cf395fa Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sun, 19 Oct 2025 00:04:14 +0200 Subject: [PATCH 6/8] docs(undo/redo): update documentation to reflect usage of clearUndoRedo function --- docs/docs/with-undo-redo.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/docs/with-undo-redo.md b/docs/docs/with-undo-redo.md index f47d489..527fbfe 100644 --- a/docs/docs/with-undo-redo.md +++ b/docs/docs/with-undo-redo.md @@ -24,6 +24,8 @@ const SyncStore = signalStore( ``` ```typescript +import { clearUndoRedo } from '@angular-architects/ngrx-toolkit'; + @Component(...) public class UndoRedoComponent { private syncStore = inject(SyncStore); @@ -43,7 +45,7 @@ public class UndoRedoComponent { } clearStack(): void { - this.store.clearStack(); + clearUndoRedo(this.store); } } ``` From 2c77af3031789137e6172477c562eab08850f9a0 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sun, 19 Oct 2025 00:07:15 +0200 Subject: [PATCH 7/8] refactor(undo/redo): move StackItem type definition to with-undo-redo.ts --- libs/ngrx-toolkit/src/lib/undo-redo/models/stack-item.ts | 1 - libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 libs/ngrx-toolkit/src/lib/undo-redo/models/stack-item.ts diff --git a/libs/ngrx-toolkit/src/lib/undo-redo/models/stack-item.ts b/libs/ngrx-toolkit/src/lib/undo-redo/models/stack-item.ts deleted file mode 100644 index 500d9ae..0000000 --- a/libs/ngrx-toolkit/src/lib/undo-redo/models/stack-item.ts +++ /dev/null @@ -1 +0,0 @@ -export type StackItem = Record; diff --git a/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts index e566090..ab7e840 100644 --- a/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts +++ b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts @@ -12,7 +12,8 @@ import { } from '@ngrx/signals'; import { capitalize } from '../with-data-service'; import { ClearUndoRedoOptions } from './clear-undo-redo'; -import { StackItem } from './models/stack-item'; + +export type StackItem = Record; export type NormalizedUndoRedoOptions = { maxStackSize: number; From cf6d3ce34d7488a41ed02397a76c877ef16d05c9 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sun, 19 Oct 2025 00:08:00 +0200 Subject: [PATCH 8/8] feat(undo/redo): export clearUndoRedo function for improved state management --- libs/ngrx-toolkit/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/ngrx-toolkit/src/index.ts b/libs/ngrx-toolkit/src/index.ts index e93029f..280b85f 100644 --- a/libs/ngrx-toolkit/src/index.ts +++ b/libs/ngrx-toolkit/src/index.ts @@ -18,7 +18,9 @@ export { withRedux, } from './lib/with-redux'; +export { clearUndoRedo } from './lib/undo-redo/clear-undo-redo'; export * from './lib/undo-redo/with-undo-redo'; + export * from './lib/with-call-state'; export * from './lib/with-data-service'; export * from './lib/with-pagination';