Skip to content

Commit e85c7f4

Browse files
committed
feat: demonstration of adding "warn"
1 parent 701fc91 commit e85c7f4

File tree

6 files changed

+234
-40
lines changed

6 files changed

+234
-40
lines changed

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

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

3-
export { Form, useFieldError, useFieldErrors, useFieldState, useForm } from 'skyroc-form';
3+
export { Form, useFieldError, useFieldErrors, useFieldState, useForm,useFieldsState } from 'skyroc-form';
44

55
export { default as FormField } from './FormField';
66

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
'use client';
2+
3+
import { useEffect } from 'react';
4+
import { Button, Card, Form, FormField, useFieldsState, useForm } from 'soybean-react-ui';
5+
6+
import { DemoInput } from './DemoComponents';
7+
import { showToastCode } from './toast';
8+
9+
type Inputs = {
10+
email: string;
11+
username: string;
12+
};
13+
14+
const ValidateOnlyDemo = () => {
15+
const [form] = useForm<Inputs>();
16+
17+
const fieldsState = useFieldsState(form, ['email', 'username'], { warnings: true });
18+
19+
async function validateOnly() {
20+
await form.validateFields();
21+
const result = form.getFieldsWarning();
22+
23+
showToastCode('warning result', result);
24+
}
25+
26+
useEffect(() => {
27+
const warnings = fieldsState
28+
.filter(field => field.warnings.length > 0)
29+
.reduce(
30+
(acc, field) => {
31+
acc[field.name] = field.warnings;
32+
return acc;
33+
},
34+
{} as Record<string, string[]>
35+
);
36+
37+
showToastCode('warnings', warnings);
38+
}, [fieldsState]);
39+
40+
return (
41+
<Card title="Validate Only Demo">
42+
<Form
43+
className="w-[480px] max-sm:w-full space-y-4"
44+
form={form}
45+
>
46+
<FormField
47+
label="Username"
48+
name="username"
49+
rules={[
50+
{ message: 'Username is required', required: true, warningOnly: true },
51+
{ message: 'Username must be at least 4 characters', minLength: 4, warningOnly: true }
52+
]}
53+
>
54+
<DemoInput
55+
name="username"
56+
placeholder="Username"
57+
/>
58+
</FormField>
59+
60+
<FormField
61+
label="Age"
62+
name="age"
63+
rules={[
64+
{ message: 'Age is required', required: true },
65+
{ message: 'Age must be at least 18', min: 18, type: 'number', warningOnly: true },
66+
{ max: 35, message: 'Age must be less than 35', type: 'number', warningOnly: true }
67+
]}
68+
>
69+
<DemoInput
70+
name="age"
71+
placeholder="Age"
72+
/>
73+
</FormField>
74+
75+
<FormField
76+
label="Email"
77+
name="email"
78+
rules={[
79+
{ message: 'Please enter a valid email', type: 'email', warningOnly: true },
80+
{ message: 'Email is required', required: true }
81+
]}
82+
>
83+
<DemoInput
84+
name="email"
85+
placeholder="Email"
86+
/>
87+
</FormField>
88+
89+
<div className="flex gap-4">
90+
<Button
91+
type="button"
92+
onClick={validateOnly}
93+
>
94+
Validate Only
95+
</Button>
96+
97+
<Button type="submit">Submit (Normal)</Button>
98+
</div>
99+
</Form>
100+
</Card>
101+
);
102+
};
103+
104+
export default ValidateOnlyDemo;

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 Default from './modules/Default';
22
import FieldChange from './modules/FieldChange';
33
import Validate from './modules/validate';
4+
import ValidateWarning from './modules/validateWaring';
45

56
const FormPage = () => {
67
return (
@@ -10,6 +11,8 @@ const FormPage = () => {
1011
<FieldChange />
1112

1213
<Validate />
14+
15+
<ValidateWarning />
1316
</div>
1417
);
1518
};

primitives/filed-form /src/FieldContext.ts

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
'use client';
22
/* eslint-disable no-bitwise */
33

4-
import { createContext, useContext, useEffect, useRef, useState } from 'react';
4+
import { createContext, useContext, useEffect, useState } from 'react';
55
import { flushSync } from 'react-dom';
66
import type { AllPaths, PathToDeepType, ShapeFromPaths } from 'skyroc-type-utils';
77

88
import type { ChangeMask, SubscribeMaskOptions } from './form-core/event';
9-
import { ChangeTag, toMask } from './form-core/event';
9+
import { toMask } from './form-core/event';
1010
import type { Action, Middleware } from './form-core/middleware';
1111
import type { FieldEntity } from './form-core/types';
1212
import type { ValidateMessages } from './form-core/validate';
13-
import type { Rule } from './form-core/validation';
13+
import type { Rule, ValidateOptions } from './form-core/validation';
1414
import type { FormState } from './types';
1515
import type { Meta } from './types/shared-types';
1616

@@ -24,6 +24,7 @@ export interface ValuesOptions<Values = any> {
2424
export interface StateOptions<Values = any> {
2525
getField: <T extends AllPaths<Values>>(name: T) => Meta<T, PathToDeepType<Values, T>>;
2626
getFieldError: (name: AllPaths<Values>) => string[];
27+
getFields: (names?: AllPaths<Values>[]) => Meta<AllPaths<Values>, PathToDeepType<Values, AllPaths<Values>>>[];
2728
getFieldsError: (...name: AllPaths<Values>[]) => Record<AllPaths<Values>, string[]>;
2829
getFieldsWarning: (...name: AllPaths<Values>[]) => Record<AllPaths<Values>, string[]>;
2930
getFieldWarning: (name: AllPaths<Values>) => string[];
@@ -37,14 +38,28 @@ export interface StateOptions<Values = any> {
3738
cb: (value: PathToDeepType<Values, T>, key: T, all: Values, fired: ChangeMask) => void,
3839
opt?: { includeChildren?: boolean; mask?: ChangeMask }
3940
) => () => void;
41+
subscribeFields: (
42+
names: AllPaths<Values>[],
43+
cb: (
44+
value: PathToDeepType<Values, AllPaths<Values>>,
45+
key: AllPaths<Values>,
46+
all: Values,
47+
fired: ChangeMask
48+
) => void,
49+
opt?: { includeChildren?: boolean; mask?: ChangeMask }
50+
) => () => void;
51+
}
52+
53+
export interface ValidateFieldsOptions extends ValidateOptions {
54+
dirty?: boolean;
4055
}
4156

4257
export interface OperationOptions<Values = any> {
4358
resetFields: (...names: AllPaths<Values>[]) => void;
4459
submit: () => void;
4560
use: (mw: Middleware) => void;
4661
validateField: (name: AllPaths<Values>) => Promise<boolean>;
47-
validateFields: (...names: AllPaths<Values>[]) => Promise<boolean>;
62+
validateFields: (names?: AllPaths<Values>[], opts?: ValidateFieldsOptions) => Promise<boolean>;
4863
}
4964

5065
export interface ValidateErrorEntity<Values = any> {
@@ -157,27 +172,78 @@ export const useFieldState = <Values = any>(
157172
return state;
158173
};
159174

175+
export const useFieldsState = <Values = any>(
176+
form: FormInstance<Values>,
177+
names: AllPaths<Values>[],
178+
mask: SubscribeMaskOptions = {
179+
errors: true,
180+
touched: true,
181+
validated: true,
182+
validating: true,
183+
warnings: true
184+
}
185+
) => {
186+
const state = form.getFields(names);
187+
188+
// eslint-disable-next-line react/hook-use-state
189+
const [_, forceUpdate] = useState({});
190+
191+
useEffect(() => {
192+
let unregister: () => void;
193+
if (!names || names.length === 0) {
194+
// 监听所有字段
195+
unregister = form.subscribeField(
196+
'' as AllPaths<Values>,
197+
() => {
198+
flushSync(() => {
199+
forceUpdate({});
200+
});
201+
},
202+
{
203+
includeChildren: true,
204+
mask: toMask(mask)
205+
}
206+
);
207+
} else {
208+
unregister = form.subscribeFields(
209+
names,
210+
() => {
211+
flushSync(() => {
212+
forceUpdate({});
213+
});
214+
},
215+
{
216+
mask: toMask(mask)
217+
}
218+
);
219+
}
220+
221+
// 只监听传入的字段
222+
return unregister;
223+
}, []);
224+
225+
return state;
226+
};
227+
160228
export const useFieldError = <Values = any>(name: AllPaths<Values>) => {
161229
const state = useFieldState<Values>(name, { errors: true });
162230

163231
return state.errors;
164232
};
165233

166-
export const useFieldErrors = <Values = any>(form: FormInstance<Values>): Record<AllPaths<Values>, string[]> => {
167-
const [errors, setErrors] = useState({});
168-
169-
useEffect(() => {
170-
return form.subscribeField(
171-
'' as AllPaths<Values>,
172-
() => {
173-
setErrors(form.getFieldsError());
174-
},
175-
{
176-
includeChildren: true,
177-
mask: ChangeTag.Errors
178-
}
179-
);
180-
}, [form]);
234+
export const useFieldErrors = <Values = any>(
235+
form: FormInstance<Values>,
236+
names: AllPaths<Values>[] = []
237+
): Record<AllPaths<Values>, string[]> => {
238+
const state = useFieldsState<Values>(form, names, { errors: true });
239+
240+
const errors = state.reduce(
241+
(acc, field) => {
242+
acc[field.name] = field.errors;
243+
return acc;
244+
},
245+
{} as Record<AllPaths<Values>, string[]>
246+
);
181247

182248
return errors as Record<AllPaths<Values>, string[]>;
183249
};

0 commit comments

Comments
 (0)