Skip to content

Commit 3f7381c

Browse files
committed
feat(form): enhance form functionality with field disable/hidden states and improved field management logic
1 parent ccda02c commit 3f7381c

File tree

7 files changed

+87
-87
lines changed

7 files changed

+87
-87
lines changed

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

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -450,9 +450,12 @@ class FormStore {
450450
const listeners = this._exactListeners.get(name);
451451

452452
if (listeners) {
453-
listeners.add({ cb: entity.changeValue, mask: ChangeTag.Value });
453+
listeners.add({ cb: entity.changeValue, mask: ChangeTag.Value | ChangeTag.Disabled | ChangeTag.Hidden });
454454
} else {
455-
this._exactListeners.set(name, new Set([{ cb: entity.changeValue, mask: ChangeTag.Value }]));
455+
this._exactListeners.set(
456+
name,
457+
new Set([{ cb: entity.changeValue, mask: ChangeTag.Value | ChangeTag.Disabled | ChangeTag.Hidden }])
458+
);
456459
}
457460

458461
return () => {
@@ -478,15 +481,27 @@ class FormStore {
478481
*/
479482
private setDisabled = (name: NamePath, disabled: boolean) => {
480483
const k = keyOfName(name);
484+
485+
const before = this._disabledKeys.has(k);
486+
if (before === disabled) return; // 状态没变,避免多余通知
487+
481488
disabled ? this._disabledKeys.add(k) : this._disabledKeys.delete(k);
489+
490+
this.enqueueNotify([k], ChangeTag.Disabled);
482491
};
483492

484493
/**
485-
* Sets the visibility state of a field
494+
* Sets the hidden state of a field
486495
*/
487496
private setHidden = (name: NamePath, hidden: boolean) => {
488497
const k = keyOfName(name);
498+
499+
const before = this._hiddenKeys.has(k);
500+
if (before === hidden) return; // 状态没变
501+
489502
hidden ? this._hiddenKeys.add(k) : this._hiddenKeys.delete(k);
503+
504+
this.enqueueNotify([k], ChangeTag.Hidden);
490505
};
491506

492507
/**
@@ -1103,7 +1118,7 @@ class FormStore {
11031118
}
11041119
}
11051120

1106-
private getArrayFields = (name: NamePath, initialValue?: StoreValue[]) => {
1121+
private getArrayFields = (name: NamePath, initialValue?: StoreValue[], disabled?: boolean) => {
11071122
const arr = (this.getFieldValue(name) as any[]) || initialValue || [];
11081123

11091124
const ak = keyOfName(name);
@@ -1115,6 +1130,7 @@ class FormStore {
11151130
keyMgr.keys[i] = keyMgr.id++;
11161131
}
11171132
return {
1133+
disabled,
11181134
key: String(keyMgr.keys[i]),
11191135
name: `${ak}.${i}`
11201136
};
@@ -1140,13 +1156,6 @@ class FormStore {
11401156

11411157
// ===== Submit =====
11421158

1143-
/**
1144-
* Registers a pre-submit transform function
1145-
*/
1146-
usePreSubmit(fn: (values: Store) => Store) {
1147-
this._preSubmit.push(fn);
1148-
}
1149-
11501159
/**
11511160
* Removes disabled and hidden fields from values before submission
11521161
*/
@@ -1211,7 +1220,7 @@ class FormStore {
12111220
* Submits the form after validation
12121221
* Calls onFinish if validation passes, onFinishFailed if it fails
12131222
*/
1214-
private submit = () => {
1223+
private submit = (prune: boolean = true) => {
12151224
this._isSubmitted = true;
12161225
this._isSubmitting = true;
12171226
this._submitCount++;
@@ -1220,7 +1229,8 @@ class FormStore {
12201229
this._isSubmitting = false;
12211230
if (ok) {
12221231
this._isSubmitSuccessful = true;
1223-
this._callbacks.onFinish?.(this._store);
1232+
const values = prune ? this._pruneForSubmit(this._store) : this._store;
1233+
this._callbacks.onFinish?.(values);
12241234
} else {
12251235
this._isSubmitSuccessful = false;
12261236
this._callbacks.onFinishFailed?.(this.buildFailedPayload());
@@ -1396,10 +1406,14 @@ class FormStore {
13961406
getFieldWarning: this.getFieldWarning,
13971407
getFormState: this.getFormState,
13981408
getInternalHooks: this.getInternalHooks,
1409+
isDisabled: this.isDisabled,
1410+
isHidden: this.isHidden,
13991411
resetFields: (names: string[] = []) => this.dispatch({ names, type: 'reset' }),
1412+
setDisabled: this.setDisabled,
14001413
setFieldsValue: (values: Store, validate = false) => this.dispatch({ type: 'setFieldsValue', validate, values }),
14011414
setFieldValue: (name: string, value: StoreValue, validate = false) =>
14021415
this.dispatch({ name, type: 'setFieldValue', validate, value }),
1416+
setHidden: this.setHidden,
14031417
submit: this.submit,
14041418
use: this.use,
14051419
validateField: (name: string, opts?: ValidateOptions) => this.dispatch({ name, opts, type: 'validateField' }),

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,13 @@ export enum ChangeTag {
2727
Validated = 0b1000000,
2828
/** Field has been reset */
2929
Reset = 0b10000000,
30+
/** Field disabled state has changed */
31+
Disabled = 0b100000000,
32+
/** Field hidden state has changed */
33+
Hidden = 0b1000000000,
34+
3035
/** Combination of all validation status flags */
31-
Status = Errors | Warnings | Validated | Validating,
36+
Status = Errors | Warnings | Validated | Validating | Disabled | Hidden,
3237
/** All possible change flags */
3338
All = 0x7fffffff
3439
}
@@ -41,9 +46,6 @@ export type ChangeMask = number;
4146

4247
/**
4348
* Checks if a change mask contains a specific tag
44-
* @param mask - The change mask to check
45-
* @param tag - The tag to look for
46-
* @returns True if the mask contains the tag
4749
*/
4850
export const hasTag = (mask: ChangeMask, tag: ChangeTag) => (mask & tag) !== 0;
4951

@@ -66,8 +68,12 @@ export interface SubscribeMaskOptions {
6668
all?: boolean;
6769
/** Subscribe to dirty state changes */
6870
dirty?: boolean;
71+
/** Subscribe to disabled state changes */
72+
disabled?: boolean;
6973
/** Subscribe to validation error changes */
7074
errors?: boolean;
75+
/** Subscribe to hidden state changes */
76+
hidden?: boolean;
7177
/** Subscribe to field reset events */
7278
reset?: boolean;
7379
/** Subscribe to touched state changes */
@@ -98,6 +104,8 @@ export const toMask = (opt: SubscribeMaskOptions = {}): ChangeMask => {
98104
if (opt.validated) tags.push(ChangeTag.Validated);
99105
if (opt.touched) tags.push(ChangeTag.Touched);
100106
if (opt.dirty) tags.push(ChangeTag.Dirty);
107+
if (opt.disabled) tags.push(ChangeTag.Disabled);
108+
if (opt.hidden) tags.push(ChangeTag.Hidden);
101109
if (opt.reset) tags.push(ChangeTag.Reset);
102110

103111
// Combine all selected tags into a single mask

primitives/filed-form /src/react/components/ComputedField.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,17 @@ function ComputedField<Values = any>({
7777
const fieldContext = useFieldContext<Values>();
7878

7979
// Extract form instance methods
80-
const { getFieldValue, getInternalHooks } = fieldContext as unknown as InternalFormInstance<Values>;
80+
const { getFieldValue, getInternalHooks, isDisabled, isHidden } =
81+
fieldContext as unknown as InternalFormInstance<Values>;
8182

8283
// Local state to track computed value for re-rendering
83-
const [value, updateValue] = useState(getFieldValue(name));
84+
85+
const value = getFieldValue(name);
86+
const [_, forceUpdate] = useState({});
87+
88+
const fieldIsHidden = isHidden(name);
89+
90+
const fieldIsDisabled = isDisabled(name);
8491

8592
// Get internal hooks for field registration and rule setting
8693
const { registerComputed, registerField, setFieldRules } = getInternalHooks();
@@ -92,8 +99,8 @@ function ComputedField<Values = any>({
9299
// Register field entity to receive value change notifications
93100
// This ensures the component re-renders when the computed value changes
94101
const unsub = registerField({
95-
changeValue: newValue => {
96-
updateValue(newValue);
102+
changeValue: () => {
103+
forceUpdate({});
97104
},
98105
initialValue: getFieldValue(name),
99106
name,
@@ -112,10 +119,14 @@ function ComputedField<Values = any>({
112119

113120
// Prepare props to pass to child component
114121
const slotProps = {
115-
readOnly: true, // Computed fields are always read-only
122+
// Computed fields are always read-only
123+
disabled: fieldIsDisabled,
124+
readOnly: true,
116125
[valuePropName]: value ?? '' // Pass computed value using specified prop name
117126
};
118127

128+
if (fieldIsHidden) return null;
129+
119130
// Render child component with computed value and read-only state
120131
return <Slot {...slotProps}>{children}</Slot>;
121132
}

primitives/filed-form /src/react/components/Field.tsx

Lines changed: 13 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Slot } from '@radix-ui/react-slot';
1111
import type { ReactElement } from 'react';
1212
import { useEffect, useId, useRef, useState } from 'react';
1313
import type { AllPathsKeys } from 'skyroc-type-utils';
14-
import { capitalize, getEventValue, isEqual, isNil, omitUndefined, toArray } from 'skyroc-utils';
14+
import { getEventValue, isEqual, omitUndefined, toArray } from 'skyroc-utils';
1515

1616
import type { EventArgs, StoreValue } from '../../form-core/types';
1717
import type { Rule } from '../../form-core/validation';
@@ -21,8 +21,6 @@ import { useFieldContext } from '../hooks/FieldContext';
2121
export type FieldProps<Values> = {
2222
/** Child element to render as the form field */
2323
children?: ReactElement;
24-
/** Control mode: 'controlled' for React controlled components, 'uncontrolled' for DOM-based */
25-
controlMode?: 'controlled' | 'uncontrolled';
2624
/** Custom function to extract value from event arguments */
2725
getValueFromEvent?: (...args: EventArgs) => StoreValue;
2826
/** Function to transform value before passing to child component */
@@ -39,8 +37,6 @@ export type FieldProps<Values> = {
3937
rules?: Rule[];
4038
/** Event name that triggers value change (default: 'onChange') */
4139
trigger?: string;
42-
/** Custom function to update uncontrolled component value */
43-
unControlledValueChange?: (ref: any, newValue: StoreValue) => void;
4440
/** Event name(s) that trigger validation */
4541
validateTrigger?: string | string[] | false;
4642
/** Name of the prop to pass the field value (default: 'value') */
@@ -53,38 +49,6 @@ export type FieldProps<Values> = {
5349
*
5450
* @example
5551
* ```tsx
56-
* // Basic usage with validation
57-
* <Form>
58-
* <Field
59-
* name="email"
60-
* rules={[
61-
* { required: true, message: 'Email is required' },
62-
* { type: 'email', message: 'Invalid email format' }
63-
* ]}
64-
* >
65-
* <Input placeholder="Enter your email" />
66-
* </Field>
67-
* </Form>
68-
* ```
69-
*
70-
* @example
71-
* ```tsx
72-
* // Controlled mode with custom validation trigger
73-
* <Field
74-
* name="password"
75-
* controlMode="controlled"
76-
* validateTrigger={['onChange', 'onBlur']}
77-
* rules={[
78-
* { required: true, message: 'Password is required' },
79-
* { min: 8, message: 'Password must be at least 8 characters' }
80-
* ]}
81-
* >
82-
* <Input type="password" placeholder="Enter password" />
83-
* </Field>
84-
* ```
85-
*
86-
* @example
87-
* ```tsx
8852
* // Custom value extraction and normalization
8953
* <Field
9054
* name="phone"
@@ -105,15 +69,13 @@ function Field<Values = any>(props: FieldProps<Values>) {
10569
// Destructure props with default values
10670
const {
10771
children,
108-
controlMode = 'uncontrolled',
10972
getValueFromEvent,
11073
initialValue,
11174
name,
11275
normalize,
11376
preserve = true,
11477
rules,
11578
trigger = 'onChange',
116-
unControlledValueChange,
11779
validateTrigger,
11880
valuePropName = 'value',
11981
...rest
@@ -139,17 +101,19 @@ function Field<Values = any>(props: FieldProps<Values>) {
139101
getFieldsValue,
140102
getFieldValue,
141103
getInternalHooks,
104+
isDisabled,
105+
isHidden,
142106
setFieldValue,
143107
validateField,
144108
validateTrigger: fieldValidateTrigger
145109
} = fieldContext as unknown as InternalFormInstance<Values>;
146110

111+
const fieldDIsabled = isDisabled(name);
112+
const fieldIsHidden = isHidden(name);
113+
147114
// Get internal hooks for field registration and rule setting
148115
const { registerField, setFieldRules } = getInternalHooks();
149116

150-
// Determine if field should be controlled
151-
const isControlled = controlMode === 'controlled';
152-
153117
// Merge field-level and form-level validation triggers
154118
const mergedValidateTrigger = validateTrigger || fieldValidateTrigger;
155119

@@ -179,10 +143,11 @@ function Field<Values = any>(props: FieldProps<Values>) {
179143

180144
// Prepare value props based on control mode
181145
// Controlled: use 'value' prop, Uncontrolled: use 'defaultValue' prop
182-
const valueProps = isControlled ? { [valuePropName]: value } : { [`default${capitalize(valuePropName)}`]: value };
146+
const valueProps = { [valuePropName]: value };
183147

184148
// Create controlled props with change handler
185149
const controlledProps = omitUndefined({
150+
disabled: fieldDIsabled,
186151
[trigger]: name
187152
? (...args: any[]) => {
188153
let newValue: StoreValue;
@@ -223,24 +188,9 @@ function Field<Values = any>(props: FieldProps<Values>) {
223188
useEffect(() => {
224189
// Register field with form store
225190
const unregister = registerField({
226-
changeValue: newValue => {
227-
if (isControlled) {
228-
// Force re-render for controlled components
229-
forceUpdate({});
230-
return;
231-
}
232-
// Handle uncontrolled component value updates
233-
const el = cref.current;
234-
235-
if (!el) return;
236-
237-
if (unControlledValueChange) {
238-
// Use custom value update function
239-
unControlledValueChange(el, newValue);
240-
} else {
241-
// Default: update DOM element value directly
242-
el.value = isNil(newValue) ? '' : (newValue as any);
243-
}
191+
changeValue: () => {
192+
// Force re-render for controlled components
193+
forceUpdate({});
244194
},
245195
initialValue,
246196
name,
@@ -256,6 +206,8 @@ function Field<Values = any>(props: FieldProps<Values>) {
256206
};
257207
}, []);
258208

209+
if (fieldIsHidden) return null;
210+
259211
// Render child component with all necessary props
260212
return (
261213
<Slot

primitives/filed-form /src/react/components/List.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*/
1010

1111
import React, { useEffect, useRef, useState } from 'react';
12-
import type { ArrayElementValue, ArrayKeys } from 'skyroc-type-utils';
12+
import type { AllPathsKeys, ArrayElementValue, ArrayKeys } from 'skyroc-type-utils';
1313

1414
import type { InternalFormInstance, ListRenderItem } from '../hooks/FieldContext';
1515
import { useFieldContext } from '../hooks/FieldContext';
@@ -180,7 +180,10 @@ function List<Values = any>(props: ListProps<Values>) {
180180
const fieldContext = useFieldContext<Values>();
181181

182182
// Extract array operations and internal hooks
183-
const { arrayOp, getInternalHooks } = fieldContext as unknown as InternalFormInstance<Values>;
183+
const { arrayOp, getInternalHooks, isDisabled, isHidden } = fieldContext as unknown as InternalFormInstance<Values>;
184+
185+
const fieldIsHidden = isHidden(name as AllPathsKeys<Values>);
186+
const fieldIsDisabled = isDisabled(name as AllPathsKeys<Values>);
184187

185188
// Get methods for array field management
186189
const { getArrayFields, registerField } = getInternalHooks();
@@ -189,7 +192,7 @@ function List<Values = any>(props: ListProps<Values>) {
189192
const [_, forceUpdate] = useState({});
190193

191194
// Get current array fields with stable keys
192-
const fields = getArrayFields(name, initialValue);
195+
const fields = getArrayFields(name, initialValue, fieldIsDisabled);
193196

194197
// Reference to cleanup function for field registration
195198
const unregisterRef = useRef<() => void>(null);
@@ -214,6 +217,8 @@ function List<Values = any>(props: ListProps<Values>) {
214217
};
215218
}, []);
216219

220+
if (fieldIsHidden) return null;
221+
217222
// Render children with fields and array operations
218223
return <>{children(fields, arrayOp(name))}</>;
219224
}

0 commit comments

Comments
 (0)