表单字段间的联动
在前端开发中,表单是最常见的交互元素之一。尤其是当多个表单项之间存在联动关系时,如何高效地管理这些联动,成为了开发中的一大挑战。
本文利用 antd 的 form 组件,将带你一起探讨如何通过封装联动表单,简化复杂的逻辑,不仅提升开发效率,更能优化用户体验。无论你是前端新手还是经验丰富的开发者,都能从中找到提升工作流的灵感。
常见联动方式
Form.Item 的 dependencies 属性
dependencies允许你根据一个或多个表单项的值来控制其他表单项的显示、验证或其他行为。常用于表单项之间的值联动。例如,用户选择某个选项时,另一个字段才会显示或者启用。
<Form.Item name="field1">
<Input />
</Form.Item>
<Form.Item name="field2"
dependencies={['field1']}
shouldUpdate={(prevValues, currentValues) => prevValues.field1 !== currentValues.field1}
>
{({ getFieldValue }) => { return getFieldValue('field1') === 'specificValue' ? <Input /> : null; }}
</Form.Item>
Form.List 组件
Form.List用于处理动态表单项,可以在一个表单中处理多个字段的动态增删,且可以实现字段间的联动。你可以在 Form.List 中使用 dependencies 或 shouldUpdate 来实现联动。
<Form.List name="dynamicFields">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, fieldKey, ...restField }) => (
<Form.Item
{...restField}
label={`Field ${name}`}
name={[name, 'field']}
fieldKey={[fieldKey, 'field']}
>
<Input />
</Form.Item>
))}
<Form.Item>
<Button onClick={() => add()} type="dashed">
Add Field
</Button>
</Form.Item>
</>
)}
</Form.List>
onValuesChange 回调函数
onValuesChange 是 Form 的一个回调函数,当表单字段值发生变化时,会触发该函数。你可以通过它来监听表单字段的变化,并在变化时进行相应的处理(如联动其他字段)。
<Form onValuesChange={(changedValues, allValues) => {
if (changedValues.field1) {
// 根据 field1 的变化动态改变其他字段
}
}}>
<Form.Item name="field1">
<Input />
</Form.Item>
<Form.Item name="field2">
<Input />
</Form.Item>
</Form>
setFieldsValue 和 getFieldValue
通过 Form 实例的 setFieldsValue 和 getFieldValue 方法,你可以在某个字段值变化时,动态地更新其他字段的值,达到联动效果。
const [form] = Form.useForm();
const handleField1Change = value => {
if (value === 'specificValue') {
form.setFieldsValue({ field2: 'defaultValue' });
}
};
return (
<Form form={form}>
<Form.Item name="field1">
<Input onChange={e => handleField1Change(e.target.value)} />
</Form.Item>
<Form.Item name="field2">
<Input />
</Form.Item>
</Form>
);
useEffect 与 useState 控制联动
在 React 组件中,结合 useState 和 useEffect 钩子可以控制表单字段的联动。通过 useEffect 监听某个字段的变化,然后更新其他字段。
const [field1, setField1] = useState('');
useEffect(() => {
if (field1 === 'specificValue') {
// 执行相关操作,更新其他字段
}
}, [field1]);
return (
<Form>
<Form.Item name="field1">
<Input value={field1} onChange={e => setField1(e.target.value)} />
</Form.Item>
<Form.Item name="field2">
<Input />
</Form.Item>
</Form>
);
自定义表单
lifecycleCallbacks
lifecycleCallbacks 一般指的是表单组件或字段在不同生命周期阶段触发的回调函数集合。
可以把它理解成表单字段/表单自己在“出生—更新—销毁”过程中挂钩的钩子函数。
1️⃣ 表单/字段生命周期阶段
lifecycleCallbacks 通常包含几个阶段:
| 生命周期阶段 | 触发时机 | 常见用途 |
|---|---|---|
onInit | 表单或字段初始化时 | 设置默认值、注册事件、动态修改配置 |
onMount | 字段被渲染到页面上 | 获取 DOM 引用、触发动画、启动异步请求 |
onChange | 字段值变化时 | 联动其他字段、验证或格式化值 |
onValidate | 表单或字段校验时 | 自定义验证逻辑 |
onDestroy | 字段被移除/表单卸载时 | 清理定时器、取消订阅、释放资源 |
注意:不同库对名称和阶段可能略有差异,但核心思想一致:表单状态变化时触发的回调集合。
const form = createForm({
effects() {
onInit(() => {
console.log('表单初始化');
});
onFieldValueChange('username', (fieldState) => {
console.log('username 字段值变化:', fieldState.value);
});
onDestroy(() => {
console.log('表单销毁,清理资源');
});
}
});
创建两个类
formModel
作用:把表单拆成字段对象(FieldModel) ,并通过 lifecycleCallbacks 管理字段生命周期钩子
formModel 初始化时接收以下数据:
- fieldsConfig:所有的字段配置项
- formInstance:表单实例
- lifecycleCallbacks:表单特定生命周期阶段触发的回调函数集合
- initialValues:表单默认值
lifecycleCallbacks
在表单中,lifecycleCallbacks 是指与表单字段生命周期相关的一些回调函数。每个字段在其生命周期的不同阶段都可能会触发某些操作或逻辑,lifecycleCallbacks 提供了一个统一的机制,用于在这些特定阶段执行预定义的回调函数。
具体来说,生命周期回调通常用于处理表单字段的各种状态变化,比如值的变动、字段的显示与隐藏、校验、提交等等。通过 lifecycleCallbacks,开发者可以为每个字段提供自定义的行为,或者在字段的特定生命周期阶段注入特定逻辑。
- init:初始化,可以做一些默认配置或数据加载
- valueChange:值变化,可以实时验证、联动操作或触发其他字段的更新
- hidden:显示/隐藏,例如,当某个字段的值变化时,可以动态地显示或隐藏相关字段
export class FormModel {
private fields: Record<string, FieldModel> = {}; // 存储表单字段的集合
constructor(params: {
fieldsConfig: IConfigFormItem[];
formInstance: FormInstance;
lifecycleCallbacks?: Record<string, IFieldLifecycleCallbacks>;
initialValues?: Record<string, any>;
}) {
const { fieldsConfig, lifecycleCallbacks, formInstance, initialValues } = params;
// 根据字段配置项初始化每个字段的 FieldModel
fieldsConfig.forEach((fieldConfig) => {
const field = new FieldModel({
key: fieldConfig.key,
fieldConfig,
changeSelfValue: (value: any) => {
formInstance.setFieldsValue({
[fieldConfig.key]: value,
});
},
// 获取当前字段的生命周期回调集合
lifecycleCallbacks: lifecycleCallbacks?.[fieldConfig.key],
initialValue: initialValues,
});
// 将每个 FieldModel 存入 fields 中,字段的 key 为对象的键
this.fields[fieldConfig.key] = field;
});
}
// 根据字段的 key 获取对应的 FieldModel
getFieldModelByKey(key: string): FieldModel {
return this.fields[key];
}
// 表单值变更时的回调函数,遍历所有字段并调用它们的 afterValueChange 方法
onFormValueChange(key: string, value: any): void {
Object.values(this.fields).forEach((field) => {
field?.afterValueChange(key, value);
});
}
}
fieldModel
作用:管理字段,并创建字段的 store 状态存储,在 store 内数据更新后重新绘制字段
fieldModel 初始化时接收以下数据:
- key:字段唯一标识符
- fieldConfig:字段配置项
- changeSelfValue:字段更新的方法
- lifecycleCallbacks:字段在不同生命周期的回调集合
- initialValue:表单初始值
实现图解
import { UseBoundStore, StoreApi, create } from 'zustand';
import { FieldTypeEnum } from "../constant";
import { IConfigFormItem, IField, IFieldLifecycleCallbacks, IFieldStore, IParams } from "../type";
export class FieldModel<T = any> {
key: string; // 字段的唯一标识符
public store: UseBoundStore<StoreApi<IFieldStore>>; // 字段的状态存储,使用zustand进行状态管理
private lifecycleCallbacks: IFieldLifecycleCallbacks = {}; // 存储字段生命周期回调函数
private readonly DEFAULT_STORE: IFieldStore = { // 字段store的默认值
hidden: false, // 默认字段不隐藏
disabled: false, // 默认字段不禁用
};
private fieldConfig: IConfigFormItem; // 字段的配置项
private changeSelfValue: (value?: T) => void; // 用于修改字段本身的值
// 获取当前字段的所有相关方法和状态
private get field(): IField {
return {
fieldKey: this.key,
setHiddenState: this.setHiddenState,
changeSelfValue: this.changeSelfValue,
clearSelfValue: this.clearSelfValue,
setDisabledState: this.setDisabledState,
};
}
constructor({
key,
fieldConfig,
changeSelfValue,
lifecycleCallbacks,
initialValue,
}: IParams<T>) {
this.key = key;
this.fieldConfig = fieldConfig;
this.store = create<IFieldStore>(() => this.DEFAULT_STORE); // 初始化store
this.changeSelfValue = changeSelfValue; // 保存字段值修改函数
this.registerLifeCycle(lifecycleCallbacks); // 注册生命周期回调
// 执行初始化阶段的生命周期回调(如果存在)
lifecycleCallbacks?.onInit?.({
initialValue,
field: this.field,
});
}
// 设置字段的隐藏状态
public setHiddenState = (hidden: boolean) => {
this.store.setState({ hidden });
};
// 设置字段的禁用状态
public setDisabledState = (disabled: boolean) => {
this.store.setState({ disabled });
};
// 获取当前字段的类型
public getFieldType = (): FieldTypeEnum => {
return this.fieldConfig.type;
};
// 注册字段生命周期回调函数
public registerLifeCycle = (callbacks?: IFieldLifecycleCallbacks): void => {
this.lifecycleCallbacks = callbacks || {};
};
// 清空字段值
private clearSelfValue = (): void => {
this.changeSelfValue(undefined);
};
// 在字段值变化时调用的回调
afterValueChange = (key: string, value: any) => {
// 如果有定义 afterFieldValueChange 回调,执行它
if (this.lifecycleCallbacks.afterFieldValueChange) {
this.lifecycleCallbacks.afterFieldValueChange({
changedKey: key,
changedValue: value,
field: this.field,
});
}
};
}
lifecycleCallbacks
不同生命周期的回调函数集合,通过 lifecycleCallbacks,开发者可以很方便地扩展字段的行为和状态,例如在字段值变化后进行联动操作或其他处理等。
以下是实例中lifecycleCallbacks方法展示,仅供参考~💐
export const detailFormFieldsLifecycleCallbacks = {
// 字段key值,与itemConfig的key是一致的
age: {
onInit: onAgeInit, // 初始化的回调
afterFieldValueChange: onAgeChange, // 值变更的回调
}
};
const onAgeInit: OnFieldInit = ({ initialValue, field }) => {
const { setHiddenState } = field;
if (!initialValue?.name) {
setHiddenState(true);
}
};
const onAgeChange: AfterFieldValueChange = ({ changedKey, changedValue: value, field: { setHiddenState, clearSelfValue, changeSelfValue } }) => {
if (changedKey === 'name') {
if (!value) {
clearSelfValue();
setHiddenState(true);
} else {
setHiddenState(false);
if(value === '酸酸'){
changeSelfValue(6);
}else if(value === '臭臭'){
changeSelfValue(5);
}
}
}
};
封装form
form表单需要接收以下外部参数:
children: 接收外部传入的多个表单字段并展示onValuesChange:自定义的表单值修改事件formInstance:表单实例lifecycleCallbacks:表单特定生命周期阶段触发的回调函数集合initialValues:表单初始值
form表单实现的能力:
- 初始化一个 formModel,利用 context 并向内部的子组件传递 formModel
- 扩展 onValuesChange 方法的基础上增加 formModel 的 onFormValueChange逻辑
export default function Form(props: IProps) {
const {
children,
onValuesChange,
formInstance,
lifecycleCallbacks,
initialValues,
} = props;
const formModelRef = useRef<FormModel | null>(null);
useMemo(() => {
formModelRef.current = new FormModel({
fieldsConfig: props.itemsConfig,
formInstance,
lifecycleCallbacks: lifecycleCallbacks.fields,
initialValues,
});
}, [props.itemsConfig, formInstance, lifecycleCallbacks, initialValues]);
const handleValuesChange: FormProps['onValuesChange'] = useCallback(
(changedValues: Record<string, any>, values: Record<string, any>) => {
Object.entries(changedValues).forEach(([key, value]) => {
formModelRef.current?.onFormValueChange(key, value);
});
onValuesChange?.(changedValues, values);
},
[onValuesChange],
);
return (
<formModelContext.Provider value={formModelRef.current}>
<Form
onValuesChange={handleValuesChange}
form={formInstance}
initialValues={initialValues}
>
{children}
</Form>
</formModelContext.Provider>
);
}
封装formItem
formItem 表单字段会从外部接收以下数据:
item:表单字段的配置项formContext:表单实例
formItem 表单字段实现的能力:
- 根据字段的唯一 key 值初始化一个 fieldModel,并且从 fieldModel 中获取表单字段的 hidden 属性和 disabled 属性
- 将上述属性传递给 Form.Item 组件
export default function FormItem(props:IProps) {
const { item } = props;
const formModel = useFormModel();
const fieldModel = formModel?.getFieldModelByKey(item.key);
const store = useMemo(() => fieldModel!.store, [fieldModel]);
const { hidden, disabled } = useStore(store);
const { key, label, required, requiredMessage, placeholder } = item;
const rules = useMemo(
() => [
{
required: required === 1,
message: requiredMessage ?? "请填写必填项",
},
],
[required, requiredMessage]
);
return (
<Form.Item label={label} rules={rules} hidden={hidden} name={key}>
<Input disabled={disabled} placeholder={placeholder ?? "请输入"} />
</Form.Item>
);
};
两者组合
- form:表单实例
- itemsConfig:自字段的配置信息
- initialValues:初始值
- lifecycleCallbacks: 表单特定生命周期阶段触发的回调函数集合
原理图解
const [form] = useForm();
const itemsConfig = [...];
<WorkForm
formInstance={form}
itemsConfig={itemsConfig}
initialValues={{name: "史努比",age: 12,}}
lifecycleCallbacks={lifecycleCallbacks}
>
{itemsConfig.map((item) => (
<WorkFormItem
key={item.key}
item={item}
formContext={form}
/>
))}
</WorkForm>
另外,表单字段控件类型可以改为 map 表,但是需要字段类型及 UI 效果大体一致时,不会出现特立独行的控件~😄