Skip to content

Commit 6820c44

Browse files
committed
feat: add form list demo
1 parent a6a5e62 commit 6820c44

File tree

6 files changed

+271
-3
lines changed

6 files changed

+271
-3
lines changed

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
'use client';
22

3-
export { Form, useFieldError, useFieldErrors, useFieldState, useForm,useFieldsState ,useWatch} from 'skyroc-form';
3+
export {
4+
Form,
5+
List as FormList,
6+
useFieldError,
7+
useFieldErrors,
8+
useFieldsState,
9+
useFieldState,
10+
useForm,
11+
useWatch
12+
} from 'skyroc-form';
413

514
export { default as FormField } from './FormField';
615

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { ReactNode } from 'react';
2-
import type { AllPaths, InternalFieldProps } from 'skyroc-form';
2+
import type { InternalFieldProps } from 'skyroc-form';
33

44
import type { BaseProps } from '@/types/other';
55

6-
export type FormFieldProps<Values = any,> = InternalFieldProps<Values> &
6+
export type FormFieldProps<Values = any> = InternalFieldProps<Values> &
77
BaseProps<{
88
description?: string;
99
error?: string;
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
'use client';
2+
3+
import { ButtonIcon, Card, Form, FormField, FormList, Input, useForm } from 'soybean-react-ui';
4+
5+
type FormValues = {
6+
users: {
7+
age: number;
8+
name: string;
9+
}[];
10+
};
11+
12+
const List = () => {
13+
const [form] = useForm<FormValues>();
14+
15+
return (
16+
<Card
17+
split
18+
title="List"
19+
>
20+
<Form
21+
className="w-[480px] max-sm:w-full space-y-4"
22+
form={form}
23+
>
24+
<FormList
25+
name="users"
26+
initialValue={[
27+
{ age: 20, name: 'John' },
28+
{ age: 21, name: 'Jane' }
29+
]}
30+
>
31+
{(fields, ops) => (
32+
<div>
33+
{fields.map(({ key, name }, index) => (
34+
<div
35+
className="flex gap-x-2 items-center"
36+
key={key}
37+
>
38+
<FormField
39+
className="flex-1"
40+
label={`Name ${key}`}
41+
name={`${name}.name`}
42+
>
43+
<Input placeholder={`Enter ${name}.name`} />
44+
</FormField>
45+
46+
<FormField
47+
className="flex-1"
48+
label={`Age ${key}`}
49+
name={`${name}.age`}
50+
>
51+
<Input placeholder={`Enter ${name}.age`} />
52+
</FormField>
53+
54+
<div className="flex gap-x-2 mt-6">
55+
<ButtonIcon
56+
icon="ant-design:plus-outlined"
57+
variant="ghost"
58+
onClick={() => ops.insert(index + 1, { age: 11, name: '' })}
59+
/>
60+
61+
<ButtonIcon icon="ant-design:minus-outlined" />
62+
</div>
63+
</div>
64+
))}
65+
66+
<div className="flex gap-x-2 mt-4 items-center">
67+
<div className="flex gap-x-2px items-center">
68+
<div className="text-sm text-gray-500">replace 0:</div>
69+
<ButtonIcon
70+
icon="ant-design:swap-outlined"
71+
variant="ghost"
72+
onClick={() => ops.replace(0, { age: 99, name: 'Replaced' })}
73+
/>
74+
</div>
75+
76+
<div className="flex gap-x-2px items-center">
77+
<div className="text-sm text-gray-500">move 0 to 1: </div>
78+
<ButtonIcon
79+
icon="ant-design:arrow-up-outlined"
80+
variant="ghost"
81+
onClick={() => ops.move(0, 1)}
82+
/>
83+
</div>
84+
85+
<div className="flex gap-x-2px items-center">
86+
<div className="text-sm text-gray-500">swap 0 and 1: </div>
87+
<ButtonIcon
88+
icon="ant-design:retweet-outlined"
89+
variant="ghost"
90+
onClick={() => ops.swap(0, 1)}
91+
/>
92+
</div>
93+
</div>
94+
</div>
95+
)}
96+
</FormList>
97+
98+
<FormList
99+
initialValue={['company1', 'company2']}
100+
name="companies"
101+
>
102+
{(fields, ops) => (
103+
<div>
104+
{fields.map(({ key, name }, index) => (
105+
<div
106+
className="flex gap-x-2 items-center"
107+
key={key}
108+
>
109+
<FormField
110+
label={`Company ${key}`}
111+
name={name}
112+
>
113+
<Input placeholder={`Enter ${name}`} />
114+
</FormField>
115+
<div className="flex gap-x-2 mt-6">
116+
<ButtonIcon
117+
icon="ant-design:plus-outlined"
118+
variant="ghost"
119+
onClick={() => ops.insert(index + 1, `company${index + 1}`)}
120+
/>
121+
122+
<ButtonIcon icon="ant-design:minus-outlined" />
123+
</div>
124+
</div>
125+
))}
126+
</div>
127+
)}
128+
</FormList>
129+
</Form>
130+
</Card>
131+
);
132+
};
133+
134+
export default List;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ClearDestroy from './modules/ClearDestroy';
22
import Default from './modules/Default';
33
import FieldChange from './modules/FieldChange';
4+
import List from './modules/List';
45
import Preserve from './modules/Preserve';
56
import Reset from './modules/Reset';
67
import UseWatch from './modules/UseWatch';
@@ -12,6 +13,8 @@ const FormPage = () => {
1213
<div className="flex-c gap-4">
1314
<Default />
1415

16+
<List />
17+
1518
<FieldChange />
1619

1720
<Validate />
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
'use client';
2+
/* eslint-disable react-hooks/exhaustive-deps */
3+
/* eslint-disable react/hook-use-state */
4+
/* eslint-disable no-plusplus */
5+
import React, { useEffect, useRef, useState } from 'react';
6+
import type { AllPaths } from 'skyroc-type-utils';
7+
8+
import type { InternalFormInstance } from './FieldContext';
9+
import { useFieldContext } from './FieldContext';
10+
import type { StoreValue } from './types/formStore';
11+
import { keyOfName } from './utils/util';
12+
13+
export type ListRenderItem = {
14+
key: string;
15+
name: string;
16+
};
17+
18+
export type ListProps<Values = any> = {
19+
// array path
20+
children: (
21+
fields: ListRenderItem[],
22+
ops: {
23+
insert: (index: number, item: any) => void;
24+
move: (from: number, to: number) => void;
25+
remove: (index: number) => void;
26+
replace: (index: number, val: any) => void;
27+
swap: (i: number, j: number) => void;
28+
}
29+
) => React.ReactNode;
30+
initialValue?: StoreValue[];
31+
name: AllPaths<Values>;
32+
};
33+
34+
function move<T>(arr: T[], from: number, to: number): T[] {
35+
const clone = arr.slice();
36+
const item = clone.splice(from, 1)[0];
37+
clone.splice(to, 0, item);
38+
return clone;
39+
}
40+
41+
function List<Values = any>(props: ListProps<Values>) {
42+
const { children, initialValue, name } = props;
43+
44+
const fieldContext = useFieldContext<Values>();
45+
46+
const keyRef = useRef({ id: 0, keys: [] as number[] });
47+
const keyManager = keyRef.current;
48+
49+
const {
50+
getFieldsValue,
51+
getFieldValue,
52+
getInternalHooks,
53+
validateTrigger: fieldValidateTrigger
54+
} = fieldContext as unknown as InternalFormInstance<Values>;
55+
56+
const { dispatch, registerField } = getInternalHooks();
57+
58+
const [_, forceUpdate] = useState({});
59+
60+
const arr = (getFieldValue(name) as any[]) || initialValue || [];
61+
62+
const fields = arr.map((___, i) => {
63+
let key = keyManager.keys[i];
64+
if (key === undefined) {
65+
keyManager.keys[i] = keyManager.id++;
66+
key = keyManager.keys[i];
67+
}
68+
return {
69+
key: String(key), // 稳定 key
70+
name: `${keyOfName(name)}.${i}`
71+
};
72+
});
73+
74+
const unregisterRef = useRef<() => void>(null);
75+
76+
if (!unregisterRef.current) {
77+
unregisterRef.current = registerField({
78+
changeValue: (newValue, __, ___, mask) => {
79+
forceUpdate({});
80+
},
81+
initialValue,
82+
name,
83+
preserve: true
84+
});
85+
}
86+
87+
useEffect(() => {
88+
return () => {
89+
unregisterRef.current?.();
90+
};
91+
}, []);
92+
93+
const ops = {
94+
insert: (index: number, item: any) => {
95+
dispatch({ args: { index, item }, name, op: 'insert', type: 'arrayOp' });
96+
keyManager.keys.splice(index, 0, keyManager.id++);
97+
},
98+
move: (from: number, to: number) => {
99+
dispatch({ args: { from, to }, name, op: 'move', type: 'arrayOp' });
100+
keyManager.keys = move(keyManager.keys, from, to);
101+
},
102+
remove: (index: number) => {
103+
dispatch({ args: { index }, name, op: 'remove', type: 'arrayOp' });
104+
keyManager.keys.splice(index, 1);
105+
},
106+
replace: (index: number, val: any) => {
107+
dispatch({ args: { index, item: val }, name, op: 'replace', type: 'arrayOp' });
108+
},
109+
swap: (i: number, j: number) => {
110+
dispatch({ args: { i, j }, name, op: 'swap', type: 'arrayOp' });
111+
[keyManager.keys[i], keyManager.keys[j]] = [keyManager.keys[j], keyManager.keys[i]];
112+
}
113+
};
114+
115+
return <>{children(fields, ops)}</>;
116+
}
117+
118+
export default List;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
export type { AllPaths, FieldElement } from 'skyroc-type-utils';
22

33
export { default as Field } from './Field';
4+
45
export { useFieldError, useFieldErrors, useFieldsState, useFieldState, useWatch } from './FieldContext';
6+
57
export { default as Form } from './Form';
68

9+
export { default as List } from './List';
10+
711
export type { InternalFieldProps } from './types/field';
812

913
export { default as useForm } from './useForm';

0 commit comments

Comments
 (0)