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); } } ``` diff --git a/libs/ngrx-toolkit/src/index.ts b/libs/ngrx-toolkit/src/index.ts index 8b8bba8..280b85f 100644 --- a/libs/ngrx-toolkit/src/index.ts +++ b/libs/ngrx-toolkit/src/index.ts @@ -18,11 +18,13 @@ 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'; 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..7a3652f --- /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: Partial | 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 90% 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..e99d78b 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,8 @@ 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 { clearUndoRedo } from './clear-undo-redo'; import { withUndoRedo } from './with-undo-redo'; const testState = { test: '' }; @@ -32,6 +33,7 @@ describe('withUndoRedo', () => { 'canRedo', 'undo', 'redo', + '__clearUndoRedo__', 'clearStack', ]); }); @@ -260,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. @@ -270,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/with-undo-redo.ts b/libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts similarity index 82% 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 3527530..ab7e840 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,11 +70,12 @@ export function withUndoRedo( methods: { undo: () => void; redo: () => void; + /** @deprecated Use {@link clearUndoRedo} instead. */ clearStack: () => void; }; } > { - let previous: StackItem | null = null; + let lastRecord: StackItem | null = null; let skipOnce = false; const normalized = { @@ -108,14 +110,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(); @@ -123,25 +125,35 @@ 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(); }, - clearStack(): void { + __clearUndoRedo__(opts?: ClearUndoRedoOptions): void { undoStack.splice(0); redoStack.splice(0); - previous = null; + + if (opts) { + lastRecord = opts.lastRecord; + } + updateInternal(); }, })), + withMethods((store) => ({ + /** @deprecated Use {@link clearUndoRedo} instead. */ + clearStack(): void { + store.__clearUndoRedo__(); + }, + })), withHooks({ onInit(store) { watchState(store, () => { @@ -174,22 +186,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());