Skip to content

Commit 6817871

Browse files
committed
feat(form): add computed field and related form components to enhance functionality and reusability
1 parent 55e2901 commit 6817871

File tree

12 files changed

+207
-57
lines changed

12 files changed

+207
-57
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use client';
2+
3+
import { ComputedField } from 'skyroc-form';
4+
5+
import FormField from './FormField';
6+
import type { FormComputedFieldProps } from './types';
7+
8+
const FormComputedField = <Values = any,>(props: FormComputedFieldProps<Values>) => {
9+
return (
10+
<FormField
11+
component={ComputedField}
12+
{...props}
13+
/>
14+
);
15+
};
16+
17+
export default FormComputedField;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { cn } from '@/lib/utils';
2+
3+
import { formVariants } from './form-variants';
4+
import type { FormDescriptionProps } from './types';
5+
6+
const FormDescription = (props: FormDescriptionProps) => {
7+
const { className, ...rest } = props;
8+
9+
const { description } = formVariants();
10+
11+
const mergedClass = cn(description(), className);
12+
13+
return (
14+
<p
15+
className={mergedClass}
16+
data-slot="form-description"
17+
{...rest}
18+
/>
19+
);
20+
};
21+
22+
export default FormDescription;

packages/ui/src/components/form/FormField.tsx

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,24 @@ import { useId } from 'react';
44
import type { AllPathsKeys } from 'skyroc-form';
55
import { Field, useFieldError } from 'skyroc-form';
66

7-
import { cn } from '@/lib/utils';
8-
9-
import FormLabel from '../label/Label';
10-
11-
import { formVariants } from './form-variants';
7+
import FormDescription from './FormDescription';
8+
import FormItem from './FormItem';
9+
import FormLabel from './FormLable';
10+
import FormMessage from './FormMessage';
1211
import type { FormFieldProps } from './types';
1312

1413
const FormField = <Values = any,>(props: FormFieldProps<Values>) => {
15-
const { children, className, description, label, name, size, ...rest } = props;
14+
const {
15+
children,
16+
className,
17+
classNames,
18+
component: Component = Field,
19+
description,
20+
label,
21+
name,
22+
size,
23+
...rest
24+
} = props;
1625

1726
const id = useId();
1827

@@ -24,60 +33,48 @@ const FormField = <Values = any,>(props: FormFieldProps<Values>) => {
2433
const formDescriptionId = `${id}-form-item-description`;
2534
const formMessageId = `${id}-form-item-message`;
2635

27-
const {
28-
description: descriptionCls,
29-
item,
30-
label: labelCls,
31-
message
32-
} = formVariants({ error: errors.length > 0, size });
33-
34-
const mergedCls = {
35-
description: cn(descriptionCls(), className),
36-
item: cn(item(), className),
37-
label: cn(labelCls()),
38-
message: cn(message(), className)
39-
};
40-
4136
return (
42-
<div className={mergedCls.item}>
37+
<FormItem
38+
className={className}
39+
size={size}
40+
>
4341
<FormLabel
44-
className={mergedCls.label}
45-
data-error={hasError}
46-
data-slot="form-label"
47-
htmlFor={id}
42+
className={classNames?.label}
43+
error={hasError}
44+
htmlFor={formItemId}
4845
size={size}
4946
>
5047
{label}
5148
</FormLabel>
5249

53-
<Field
50+
<Component
5451
{...rest}
5552
aria-describedby={!hasError ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
5653
aria-invalid={hasError}
5754
id={id}
5855
name={name}
5956
>
6057
{children}
61-
</Field>
58+
</Component>
6259

6360
{description && (
64-
<p
65-
className={mergedCls.description}
61+
<FormDescription
62+
className={classNames?.description}
6663
id={formDescriptionId}
6764
>
6865
{description}
69-
</p>
66+
</FormDescription>
7067
)}
7168

7269
{hasError && (
73-
<p
74-
className={mergedCls.message}
70+
<FormMessage
71+
className={classNames?.message}
7572
id={formMessageId}
7673
>
7774
{errors[0]}
78-
</p>
75+
</FormMessage>
7976
)}
80-
</div>
77+
</FormItem>
8178
);
8279
};
8380

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { cn } from '@/lib/utils';
2+
3+
import { formVariants } from './form-variants';
4+
import type { FormItemProps } from './types';
5+
6+
const FormItem = (props: FormItemProps) => {
7+
const { className, size, ...rest } = props;
8+
9+
const { item } = formVariants({ size });
10+
11+
const mergedCls = cn(item(), className);
12+
13+
return (
14+
<div
15+
className={mergedCls}
16+
data-slot="form-item"
17+
{...rest}
18+
/>
19+
);
20+
};
21+
22+
export default FormItem;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { cn } from '@/lib/utils';
2+
3+
import Label from '../label/Label';
4+
5+
import { formVariants } from './form-variants';
6+
import type { FormLabelProps } from './types';
7+
8+
const FormLabel = (props: FormLabelProps) => {
9+
const { className, error, size, ...rest } = props;
10+
11+
const { label } = formVariants({ error, size });
12+
13+
const mergedCls = cn(label(), className);
14+
15+
return (
16+
<Label
17+
className={mergedCls}
18+
data-error={error}
19+
data-slot="form-label"
20+
size={size}
21+
{...rest}
22+
/>
23+
);
24+
};
25+
26+
export default FormLabel;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { cn } from '@/lib/utils';
2+
3+
import { formVariants } from './form-variants';
4+
import type { FormMessageProps } from './types';
5+
6+
const FormMessage = (props: FormMessageProps) => {
7+
const { className, error, ...rest } = props;
8+
9+
const { message } = formVariants();
10+
11+
const mergedCls = cn(message(), className);
12+
13+
const body = error?.length ? error[0] : props.children;
14+
15+
if (!body) return null;
16+
17+
return (
18+
<p
19+
className={mergedCls}
20+
data-slot="form-message"
21+
{...rest}
22+
>
23+
{body}
24+
</p>
25+
);
26+
};
27+
28+
export default FormMessage;
Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use client';
22

33
export {
4-
ComputedField as FormComputedField,
54
Form,
65
List as FormList,
6+
useArrayField,
77
useEffectField,
88
useFieldError,
99
useFieldState,
@@ -13,8 +13,20 @@ export {
1313
useWatch
1414
} from 'skyroc-form';
1515

16-
export type { Action as FormAction, FormInstance } from 'skyroc-form';
16+
export type {
17+
Action as FormAction,
18+
AllPathsKeys,
19+
ComputedFieldProps,
20+
FieldElement,
21+
FormInstance,
22+
FormProps,
23+
Meta,
24+
Rule,
25+
SubscribeMaskOptions,
26+
ValidateMessages
27+
} from 'skyroc-form';
1728

29+
export { default as FormComputedField } from './FormComputedField';
1830
export { default as FormField } from './FormField';
1931

20-
export type { FormFieldProps } from './types';
32+
export type { FormComputedFieldProps, FormFieldProps } from './types';
Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,36 @@
1-
import type { ReactNode } from 'react';
2-
import type { FieldProps } from 'skyroc-form';
1+
import type { ComponentProps, ElementType, ReactNode } from 'react';
2+
import type { ComputedFieldProps, FieldProps } from 'skyroc-form';
33

4-
import type { BaseProps } from '@/types/other';
4+
import type { BaseNodeProps, ClassValue } from '@/types/other';
5+
6+
import type { LabelProps } from '../label/types';
7+
8+
import type { FormSlots } from './form-variants';
9+
10+
type FormClassNames = Partial<Record<FormSlots, ClassValue>>;
11+
12+
export interface FormDescriptionProps extends BaseNodeProps<ComponentProps<'p'>> {}
13+
14+
export interface FormItemProps extends BaseNodeProps<ComponentProps<'div'>> {}
15+
16+
export interface FormLabelProps extends LabelProps {
17+
error?: boolean;
18+
}
19+
20+
export interface FormMessageProps extends BaseNodeProps<ComponentProps<'p'>> {
21+
error?: string[];
22+
}
23+
24+
type FormSharedProps = BaseNodeProps<{
25+
classNames?: FormClassNames;
26+
description?: string;
27+
error?: string;
28+
label?: ReactNode;
29+
}>;
530

631
export type FormFieldProps<Values = any> = FieldProps<Values> &
7-
BaseProps<{
8-
description?: string;
9-
error?: string;
10-
label?: ReactNode;
11-
required?: boolean;
12-
}>;
32+
FormSharedProps & {
33+
component?: ElementType;
34+
};
35+
36+
export type FormComputedFieldProps<Values = any> = ComputedFieldProps<Values> & FormSharedProps;

packages/ui/src/components/input/Input.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ import { inputVariants } from './input-variants';
66
import type { InputProps } from './types';
77

88
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
9-
const { className, size, ...rest } = props;
9+
const { className, disabled, size, ...rest } = props;
1010

1111
const mergedCls = cn(inputVariants({ size }), className);
1212

13+
const isDisabled = disabled || rest.readOnly;
14+
1315
return (
1416
<input
1517
className={mergedCls}
1618
data-slot="input"
19+
disabled={isDisabled}
1720
ref={ref}
1821
{...rest}
1922
/>

playground/src/app/(demo)/form/modules/ComputedDemo.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,25 @@ const ComputedDemo = () => {
3030
label="Price"
3131
name="price"
3232
>
33-
<Input />
33+
<Input placeholder="please input price" />
3434
</FormField>
3535

3636
<FormField
3737
label="Quantity"
3838
name="quantity"
3939
>
40-
<Input />
40+
<Input placeholder="please input quantity" />
4141
</FormField>
4242

4343
<FormComputedField
4444
deps={['price', 'quantity']}
45+
label="Total"
4546
name="total"
4647
compute={get => {
4748
return Number(get('price')) * Number(get('quantity')) || 0;
4849
}}
4950
>
50-
<Input />
51+
<Input placeholder="auto compute total" />
5152
</FormComputedField>
5253

5354
<Button

0 commit comments

Comments
 (0)