Skip to content

Commit 313789d

Browse files
GregOnNetrainerhahnekampCopilot
committed
feat(undo-redo): introduce clearUndoRedo in favor of store.clearStack (#253) [backport v19,v20]
deprecates `clearStack` in favor of a new standalone function `clearUndoRedo`, which does a soft reset (not setting the state to null) by default. The hard reset can be set via options --------- Co-authored-by: Rainer Hahnekamp <rainer.hahnekamp@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent b87b7e4 commit 313789d

File tree

6 files changed

+126
-18
lines changed

6 files changed

+126
-18
lines changed

docs/docs/with-undo-redo.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const SyncStore = signalStore(
2424
```
2525

2626
```typescript
27+
import { clearUndoRedo } from '@angular-architects/ngrx-toolkit';
28+
2729
@Component(...)
2830
public class UndoRedoComponent {
2931
private syncStore = inject(SyncStore);
@@ -43,7 +45,7 @@ public class UndoRedoComponent {
4345
}
4446

4547
clearStack(): void {
46-
this.store.clearStack();
48+
clearUndoRedo(this.store);
4749
}
4850
}
4951
```

libs/ngrx-toolkit/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ export {
1818
withRedux,
1919
} from './lib/with-redux';
2020

21+
export { clearUndoRedo } from './lib/undo-redo/clear-undo-redo';
22+
export * from './lib/undo-redo/with-undo-redo';
23+
2124
export * from './lib/with-call-state';
2225
export * from './lib/with-data-service';
2326
export * from './lib/with-pagination';
2427
export { setResetState, withReset } from './lib/with-reset';
25-
export * from './lib/with-undo-redo';
2628

2729
export { withImmutableState } from './lib/immutable-state/with-immutable-state';
2830
export { withIndexedDB } from './lib/storage-sync/features/with-indexed-db';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { signalStore, withState } from '@ngrx/signals';
3+
import { clearUndoRedo } from './clear-undo-redo';
4+
import { withUndoRedo } from './with-undo-redo';
5+
6+
describe('withUndoRedo', () => {
7+
describe('clearUndoRedo', () => {
8+
it('should throw an error if the store is not configured with withUndoRedo()', () => {
9+
const Store = signalStore({ providedIn: 'root' }, withState({}));
10+
const store = TestBed.inject(Store);
11+
12+
expect(() => clearUndoRedo(store)).toThrow(
13+
'Cannot clear undoRedo, since store is not configured with withUndoRedo()',
14+
);
15+
});
16+
17+
it('should not throw an error if the store is configured with withUndoRedo()', () => {
18+
const Store = signalStore(
19+
{ providedIn: 'root' },
20+
withState({}),
21+
withUndoRedo(),
22+
);
23+
const store = TestBed.inject(Store);
24+
25+
expect(() => clearUndoRedo(store)).not.toThrow();
26+
});
27+
});
28+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { StateSource } from '@ngrx/signals';
2+
3+
export type ClearUndoRedoOptions<TState extends object> = {
4+
lastRecord: Partial<TState> | null;
5+
};
6+
7+
export type ClearUndoRedoFn<TState extends object> = (
8+
opts?: ClearUndoRedoOptions<TState>,
9+
) => void;
10+
11+
export function clearUndoRedo<TState extends object>(
12+
store: StateSource<TState>,
13+
opts?: ClearUndoRedoOptions<TState>,
14+
): void {
15+
if (canClearUndoRedo(store)) {
16+
store.__clearUndoRedo__(opts);
17+
} else {
18+
throw new Error(
19+
'Cannot clear undoRedo, since store is not configured with withUndoRedo()',
20+
);
21+
}
22+
}
23+
24+
function canClearUndoRedo<TState extends object>(
25+
store: StateSource<TState>,
26+
): store is StateSource<TState> & {
27+
__clearUndoRedo__: ClearUndoRedoFn<TState>;
28+
} {
29+
if (
30+
'__clearUndoRedo__' in store &&
31+
typeof store.__clearUndoRedo__ === 'function'
32+
) {
33+
return true;
34+
} else {
35+
return false;
36+
}
37+
}

libs/ngrx-toolkit/src/lib/with-undo-redo.spec.ts renamed to libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.spec.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
withState,
1010
} from '@ngrx/signals';
1111
import { addEntity, withEntities } from '@ngrx/signals/entities';
12-
import { withCallState } from './with-call-state';
12+
import { withCallState } from '../with-call-state';
13+
import { clearUndoRedo } from './clear-undo-redo';
1314
import { withUndoRedo } from './with-undo-redo';
1415

1516
const testState = { test: '' };
@@ -32,6 +33,7 @@ describe('withUndoRedo', () => {
3233
'canRedo',
3334
'undo',
3435
'redo',
36+
'__clearUndoRedo__',
3537
'clearStack',
3638
]);
3739
});
@@ -271,7 +273,7 @@ describe('withUndoRedo', () => {
271273
store.update('Gordon');
272274
tick(1);
273275

274-
store.clearStack();
276+
clearUndoRedo(store, { lastRecord: null });
275277
tick(1);
276278

277279
// After clearing the undo/redo stack, there is no previous item anymore.
@@ -283,5 +285,30 @@ describe('withUndoRedo', () => {
283285
expect(store.canUndo()).toBe(false);
284286
expect(store.canRedo()).toBe(false);
285287
}));
288+
289+
it('can undo after setting lastRecord', fakeAsync(() => {
290+
const Store = signalStore(
291+
{ providedIn: 'root' },
292+
withState(testState),
293+
withMethods((store) => ({
294+
update: (value: string) => patchState(store, { test: value }),
295+
})),
296+
withUndoRedo({ keys: testKeys }),
297+
);
298+
299+
const store = TestBed.inject(Store);
300+
301+
store.update('Alan');
302+
303+
store.update('Gordon');
304+
305+
clearUndoRedo(store, { lastRecord: { test: 'Joan' } });
306+
307+
store.update('Hugh');
308+
tick();
309+
310+
expect(store.canUndo()).toBe(true);
311+
expect(store.canRedo()).toBe(false);
312+
}));
286313
});
287314
});

libs/ngrx-toolkit/src/lib/with-undo-redo.ts renamed to libs/ngrx-toolkit/src/lib/undo-redo/with-undo-redo.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
withHooks,
1010
withMethods,
1111
} from '@ngrx/signals';
12-
import { capitalize } from './with-data-service';
12+
import { capitalize } from '../with-data-service';
13+
import { ClearUndoRedoOptions } from './clear-undo-redo';
1314

1415
export type StackItem = Record<string, unknown>;
1516

@@ -68,11 +69,12 @@ export function withUndoRedo<Input extends EmptyFeatureResult>(
6869
methods: {
6970
undo: () => void;
7071
redo: () => void;
72+
/** @deprecated Use {@link clearUndoRedo} instead. */
7173
clearStack: () => void;
7274
};
7375
}
7476
> {
75-
let previous: StackItem | null = null;
77+
let lastRecord: StackItem | null = null;
7678
let skipOnce = false;
7779

7880
const normalized = {
@@ -107,40 +109,50 @@ export function withUndoRedo<Input extends EmptyFeatureResult>(
107109
undo(): void {
108110
const item = undoStack.pop();
109111

110-
if (item && previous) {
111-
redoStack.push(previous);
112+
if (item && lastRecord) {
113+
redoStack.push(lastRecord);
112114
}
113115

114116
if (item) {
115117
skipOnce = true;
116118
patchState(store, item);
117-
previous = item;
119+
lastRecord = item;
118120
}
119121

120122
updateInternal();
121123
},
122124
redo(): void {
123125
const item = redoStack.pop();
124126

125-
if (item && previous) {
126-
undoStack.push(previous);
127+
if (item && lastRecord) {
128+
undoStack.push(lastRecord);
127129
}
128130

129131
if (item) {
130132
skipOnce = true;
131133
patchState(store, item);
132-
previous = item;
134+
lastRecord = item;
133135
}
134136

135137
updateInternal();
136138
},
137-
clearStack(): void {
139+
__clearUndoRedo__(opts?: ClearUndoRedoOptions<Input['state']>): void {
138140
undoStack.splice(0);
139141
redoStack.splice(0);
140-
previous = null;
142+
143+
if (opts) {
144+
lastRecord = opts.lastRecord;
145+
}
146+
141147
updateInternal();
142148
},
143149
})),
150+
withMethods((store) => ({
151+
/** @deprecated Use {@link clearUndoRedo} instead. */
152+
clearStack(): void {
153+
store.__clearUndoRedo__();
154+
},
155+
})),
144156
withHooks({
145157
onInit(store) {
146158
effect(() => {
@@ -173,22 +185,22 @@ export function withUndoRedo<Input extends EmptyFeatureResult>(
173185
// if the component sends back the undone filter
174186
// to the store.
175187
//
176-
if (JSON.stringify(cand) === JSON.stringify(previous)) {
188+
if (JSON.stringify(cand) === JSON.stringify(lastRecord)) {
177189
return;
178190
}
179191

180192
// Clear redoStack after recorded action
181193
redoStack.splice(0);
182194

183-
if (previous) {
184-
undoStack.push(previous);
195+
if (lastRecord) {
196+
undoStack.push(lastRecord);
185197
}
186198

187199
if (redoStack.length > normalized.maxStackSize) {
188200
undoStack.unshift();
189201
}
190202

191-
previous = cand;
203+
lastRecord = cand;
192204

193205
// Don't propogate current reactive context
194206
untracked(() => updateInternal());

0 commit comments

Comments
 (0)