Skip to content

Commit 8b23419

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 9f9b30a commit 8b23419

File tree

6 files changed

+125
-18
lines changed

6 files changed

+125
-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
@@ -19,11 +19,13 @@ export {
1919
withRedux,
2020
} from './lib/with-redux';
2121

22+
export { clearUndoRedo } from './lib/undo-redo/clear-undo-redo';
23+
export * from './lib/undo-redo/with-undo-redo';
24+
2225
export * from './lib/with-call-state';
2326
export * from './lib/with-data-service';
2427
export * from './lib/with-pagination';
2528
export { setResetState, withReset } from './lib/with-reset';
26-
export * from './lib/with-undo-redo';
2729

2830
export { withImmutableState } from './lib/immutable-state/with-immutable-state';
2931
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: 28 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
});
@@ -260,7 +262,7 @@ describe('withUndoRedo', () => {
260262

261263
store.update('Gordon');
262264

263-
store.clearStack();
265+
clearUndoRedo(store, { lastRecord: null });
264266

265267
// After clearing the undo/redo stack, there is no previous item anymore.
266268
// The following update becomes the first value.
@@ -270,5 +272,29 @@ describe('withUndoRedo', () => {
270272
expect(store.canUndo()).toBe(false);
271273
expect(store.canRedo()).toBe(false);
272274
});
275+
276+
it('can undo after setting lastRecord', () => {
277+
const Store = signalStore(
278+
{ providedIn: 'root' },
279+
withState(testState),
280+
withMethods((store) => ({
281+
update: (value: string) => patchState(store, { test: value }),
282+
})),
283+
withUndoRedo({ keys: testKeys }),
284+
);
285+
286+
const store = TestBed.inject(Store);
287+
288+
store.update('Alan');
289+
290+
store.update('Gordon');
291+
292+
clearUndoRedo(store, { lastRecord: { test: 'Joan' } });
293+
294+
store.update('Hugh');
295+
296+
expect(store.canUndo()).toBe(true);
297+
expect(store.canRedo()).toBe(false);
298+
});
273299
});
274300
});

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
@@ -10,7 +10,8 @@ import {
1010
withHooks,
1111
withMethods,
1212
} from '@ngrx/signals';
13-
import { capitalize } from './with-data-service';
13+
import { capitalize } from '../with-data-service';
14+
import { ClearUndoRedoOptions } from './clear-undo-redo';
1415

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

@@ -69,11 +70,12 @@ export function withUndoRedo<Input extends EmptyFeatureResult>(
6970
methods: {
7071
undo: () => void;
7172
redo: () => void;
73+
/** @deprecated Use {@link clearUndoRedo} instead. */
7274
clearStack: () => void;
7375
};
7476
}
7577
> {
76-
let previous: StackItem | null = null;
78+
let lastRecord: StackItem | null = null;
7779
let skipOnce = false;
7880

7981
const normalized = {
@@ -108,40 +110,50 @@ export function withUndoRedo<Input extends EmptyFeatureResult>(
108110
undo(): void {
109111
const item = undoStack.pop();
110112

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

115117
if (item) {
116118
skipOnce = true;
117119
patchState(store, item);
118-
previous = item;
120+
lastRecord = item;
119121
}
120122

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

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

130132
if (item) {
131133
skipOnce = true;
132134
patchState(store, item);
133-
previous = item;
135+
lastRecord = item;
134136
}
135137

136138
updateInternal();
137139
},
138-
clearStack(): void {
140+
__clearUndoRedo__(opts?: ClearUndoRedoOptions<Input['state']>): void {
139141
undoStack.splice(0);
140142
redoStack.splice(0);
141-
previous = null;
143+
144+
if (opts) {
145+
lastRecord = opts.lastRecord;
146+
}
147+
142148
updateInternal();
143149
},
144150
})),
151+
withMethods((store) => ({
152+
/** @deprecated Use {@link clearUndoRedo} instead. */
153+
clearStack(): void {
154+
store.__clearUndoRedo__();
155+
},
156+
})),
145157
withHooks({
146158
onInit(store) {
147159
watchState(store, () => {
@@ -174,22 +186,22 @@ export function withUndoRedo<Input extends EmptyFeatureResult>(
174186
// if the component sends back the undone filter
175187
// to the store.
176188
//
177-
if (JSON.stringify(cand) === JSON.stringify(previous)) {
189+
if (JSON.stringify(cand) === JSON.stringify(lastRecord)) {
178190
return;
179191
}
180192

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

184-
if (previous) {
185-
undoStack.push(previous);
196+
if (lastRecord) {
197+
undoStack.push(lastRecord);
186198
}
187199

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

192-
previous = cand;
204+
lastRecord = cand;
193205

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

0 commit comments

Comments
 (0)