Skip to content

Commit baced5d

Browse files
committed
feat: add useUndoRedo hook and demo with UndoRedo component
1 parent f690495 commit baced5d

File tree

10 files changed

+320
-147
lines changed

10 files changed

+320
-147
lines changed

packages/ui/src/components/form/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export {
99
useFieldState,
1010
useForm,
1111
useSelector,
12+
useUndoRedo,
1213
useWatch
1314
} from 'skyroc-form';
1415

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
'use client';
2+
3+
import { Button, Card, Form, FormField, FormList, Input, useForm, useUndoRedo } from 'soybean-react-ui';
4+
5+
type Inputs = {
6+
email: string;
7+
tags: string[];
8+
username: string;
9+
};
10+
11+
const initialValues: Inputs = {
12+
email: 'test@example.com',
13+
tags: ['vue', 'react'],
14+
username: 'ohh'
15+
};
16+
17+
const UseFormWithUndoRedo = () => {
18+
const [form] = useForm<Inputs>();
19+
20+
const undoRedo = useUndoRedo(form);
21+
22+
function setUsername() {
23+
form.setFieldValue('username', 'new_user');
24+
}
25+
26+
function insertTag() {
27+
form.arrayOp('tags').insert(1, 'typescript');
28+
}
29+
30+
function undo() {
31+
undoRedo?.undo();
32+
}
33+
34+
function redo() {
35+
undoRedo?.redo();
36+
}
37+
38+
return (
39+
<Card title="UseForm with Undo/Redo">
40+
<Form
41+
className="w-[480px] max-sm:w-full space-y-4"
42+
form={form}
43+
initialValues={initialValues}
44+
>
45+
<FormField
46+
label="Username"
47+
name="username"
48+
>
49+
<Input />
50+
</FormField>
51+
52+
<FormField
53+
label="Email"
54+
name="email"
55+
>
56+
<Input />
57+
</FormField>
58+
59+
<FormList
60+
initialValue={['vue', 'react']}
61+
name="tags"
62+
>
63+
{fields => (
64+
<div>
65+
{fields.map(({ key, name }) => (
66+
<FormField
67+
key={key}
68+
label={`Tag ${key}`}
69+
name={name}
70+
>
71+
<Input />
72+
</FormField>
73+
))}
74+
</div>
75+
)}
76+
</FormList>
77+
78+
<div className="flex gap-2 flex-wrap">
79+
<Button
80+
type="button"
81+
onClick={setUsername}
82+
>
83+
Change Username
84+
</Button>
85+
86+
<Button
87+
type="button"
88+
onClick={insertTag}
89+
>
90+
Insert Tag
91+
</Button>
92+
93+
<Button
94+
disabled={!undoRedo?.canUndo}
95+
type="button"
96+
onClick={undo}
97+
>
98+
Undo
99+
</Button>
100+
101+
<Button
102+
disabled={!undoRedo?.canRedo}
103+
type="button"
104+
onClick={redo}
105+
>
106+
Redo
107+
</Button>
108+
109+
<Button type="submit">Submit</Button>
110+
</div>
111+
</Form>
112+
</Card>
113+
);
114+
};
115+
116+
export default UseFormWithUndoRedo;

playground/src/app/(demo)/form/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import List from './modules/List';
66
import Middleware from './modules/Middleware';
77
import Preserve from './modules/Preserve';
88
import Reset from './modules/Reset';
9+
import UndoRedo from './modules/UndoRedo';
910
import UseForm from './modules/UseForm';
1011
import UseSelector from './modules/UseSelector';
1112
import UseWatch from './modules/UseWatch';
@@ -39,6 +40,8 @@ const FormPage = () => {
3940

4041
<Middleware />
4142

43+
<UndoRedo />
44+
4245
<ClearDestroy />
4346
</div>
4447
);

primitives/filed-form /src/form-core/createStore.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type { NamePath, PathTuple } from '../utils/util';
1515
import { anyOn, collectDeepKeys, isOn, isUnderPrefix, keyOfName, microtask } from '../utils/util';
1616

1717
import { type ChangeMask, ChangeTag } from './event';
18-
import type { Action, Middleware, ValidateFieldsOptions } from './middleware';
18+
import type { Action, ArrayOpArgs, Middleware, ValidateFieldsOptions } from './middleware';
1919
import { compose } from './middleware';
2020
import type { ValidateMessages } from './types';
2121
import { runRulesWithMode } from './validation';
@@ -237,7 +237,7 @@ class FormStore {
237237
this.validateFields(a.name, a.opts);
238238
break;
239239
case 'arrayOp':
240-
this.arrayOp(a.name, a.op, a.args);
240+
this.arrayOp(a.name, a.args);
241241
break;
242242
case 'setExternalErrors': {
243243
// Handle external validation errors
@@ -973,7 +973,7 @@ class FormStore {
973973
return mgr;
974974
};
975975

976-
private arrayOp = (name: NamePath, op: 'insert' | 'move' | 'remove' | 'replace' | 'swap', args: any) => {
976+
private arrayOp(name: NamePath, args: ArrayOpArgs): void {
977977
const arr = this.getFieldValue(name);
978978
if (!Array.isArray(arr)) return;
979979
const next = arr.slice();
@@ -986,7 +986,7 @@ class FormStore {
986986

987987
const mark = (mask: ChangeMask = ChangeTag.Value) => this.enqueueNotify([name], mask);
988988

989-
switch (op) {
989+
switch (args.op) {
990990
case 'insert': {
991991
const { index, item } = args;
992992
next.splice(index, 0, item);
@@ -1016,11 +1016,11 @@ class FormStore {
10161016
break;
10171017
}
10181018
case 'swap': {
1019-
const { i, j } = args;
1020-
const tmp = next[i];
1021-
next[i] = next[j];
1022-
next[j] = tmp;
1023-
[keyMgr.keys[i], keyMgr.keys[j]] = [keyMgr.keys[j], keyMgr.keys[i]];
1019+
const { from, to } = args;
1020+
const tmp = next[from];
1021+
next[from] = next[to];
1022+
next[to] = tmp;
1023+
[keyMgr.keys[from], keyMgr.keys[to]] = [keyMgr.keys[to], keyMgr.keys[from]];
10241024
this._store = set(this._store, name, next);
10251025
this._validated.delete(ak);
10261026
mark();
@@ -1037,7 +1037,7 @@ class FormStore {
10371037
default:
10381038
break;
10391039
}
1040-
};
1040+
}
10411041

10421042
private getArrayFields = (name: NamePath, initialValue?: StoreValue[]) => {
10431043
const arr = (this.getFieldValue(name) as any[]) || initialValue || [];
@@ -1270,19 +1270,19 @@ class FormStore {
12701270
return {
12711271
arrayOp: (name: NamePath) => ({
12721272
insert: (index: number, item: any) => {
1273-
this.dispatch({ args: { index, item }, name, op: 'insert', type: 'arrayOp' });
1273+
this.dispatch({ args: { index, item, op: 'insert' }, name, type: 'arrayOp' });
12741274
},
12751275
move: (from: number, to: number) => {
1276-
this.dispatch({ args: { from, to }, name, op: 'move', type: 'arrayOp' });
1276+
this.dispatch({ args: { from, op: 'move', to }, name, type: 'arrayOp' });
12771277
},
12781278
remove: (index: number) => {
1279-
this.dispatch({ args: { index }, name, op: 'remove', type: 'arrayOp' });
1279+
this.dispatch({ args: { index, op: 'remove' }, name, type: 'arrayOp' });
12801280
},
12811281
replace: (index: number, val: any) => {
1282-
this.dispatch({ args: { index, item: val }, name, op: 'replace', type: 'arrayOp' });
1282+
this.dispatch({ args: { index, item: val, op: 'replace' }, name, type: 'arrayOp' });
12831283
},
1284-
swap: (i: number, j: number) => {
1285-
this.dispatch({ args: { i, j }, name, op: 'swap', type: 'arrayOp' });
1284+
swap: (index: number, j: number) => {
1285+
this.dispatch({ args: { from: index, op: 'swap', to: j }, name, type: 'arrayOp' });
12861286
}
12871287
}),
12881288
getField: this.getField,
@@ -1315,6 +1315,7 @@ class FormStore {
13151315

13161316
getInternalHooks = () => {
13171317
return {
1318+
arrayOp: this.arrayOp,
13181319
destroyForm: this.destroyForm,
13191320
dispatch: this.dispatch,
13201321
getArrayFields: this.getArrayFields,
@@ -1324,10 +1325,14 @@ class FormStore {
13241325
registerField: this.registerField,
13251326
setCallbacks: this.setCallbacks,
13261327
setFieldRules: this.setFieldRules,
1328+
setFieldsValue: this.setFieldsValue,
1329+
setFieldValue: this.setFieldValue,
13271330
setInitialValues: this.setInitialValues,
13281331
setPreserve: this.setPreserve,
13291332
setValidateMessages: this.setValidateMessages,
1330-
subscribeField: this.subscribeField
1333+
subscribeField: this.subscribeField,
1334+
transaction: this.transaction,
1335+
transactionAsync: this.transactionAsync
13311336
};
13321337
};
13331338
}

primitives/filed-form /src/form-core/middleware.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,27 @@ export interface ValidateFieldsOptions extends ValidateOptions {
77
dirty?: boolean;
88
}
99

10+
export type ArrayOp = 'insert' | 'move' | 'remove' | 'replace' | 'swap';
11+
12+
export type ArrayOpArgs =
13+
| { index: number; item: any; op: 'insert' }
14+
| { index: number; op: 'remove' }
15+
| { from: number; op: 'move'; to: number }
16+
| { from: number; op: 'swap'; to: number }
17+
| { index: number; item: any; op: 'replace' };
18+
19+
export type ArgsOf<T extends ArrayOp> = Extract<ArrayOpArgs, { op: T }>;
20+
21+
export type ArrayOpAction = { args: ArgsOf<ArrayOp>; name: NamePath; type: 'arrayOp' };
22+
1023
export type Action =
1124
| { name: NamePath; type: 'setFieldValue'; validate?: boolean; value: StoreValue }
1225
| { type: 'setFieldsValue'; validate?: boolean; values: Store }
1326
| { names?: NonNullable<NamePath>[]; type: 'reset' }
14-
| {
15-
name: NamePath;
16-
opts?: ValidateOptions;
17-
type: 'validateField';
18-
}
27+
| { name: NamePath; opts?: ValidateOptions; type: 'validateField' }
1928
| { name?: NamePath[]; opts?: ValidateFieldsOptions; type: 'validateFields' }
20-
| { args: any; name: NamePath; op: 'insert' | 'move' | 'remove' | 'replace' | 'swap'; type: 'arrayOp' }
21-
| { entries: Array<[string, string[]]>; type: 'setExternalErrors' };
29+
| { entries: Array<[string, string[]]>; type: 'setExternalErrors' }
30+
| ArrayOpAction;
2231

2332
export type MiddlewareCtx = {
2433
dispatch(a: Action): void;

primitives/filed-form /src/form-core/unodoRedo.ts

Lines changed: 0 additions & 109 deletions
This file was deleted.

0 commit comments

Comments
 (0)