Rx Form State
Edit page
开始
教程
值处理表单初始值与重置表单校验自定义表单域提交表单嵌套表单简单的嵌套表单列表类型的嵌套表单深层嵌套嵌套表单的性能优化建议嵌套表单的全局校验表单域值关联全局值关联
API

嵌套表单

@sinoui/rx-form-state的表单域name属性支持 JsonPath形式,以支持嵌套表单的场景。如:

表单值数据:

{
"address": {
"city": "北京",
"street": "海淀"
}
}

地址相关的两个表单项:

<Field name="address.city" />
<Field name="address.street" />

列表类型嵌套表单的数据结构:

{
"contacts": [
{
"userName": "张三",
"telephone": "13443565678"
},
{
"userName": "李四",
"telephone": "13312341234"
}
]
}

我们可以这样定义表单域:

第一个联系人:
<Field name="contacts[0].userName" />
<Field name="contacts[0].telephone" />
第二个联系人:
<Field name="contacts[1].userName" />
<Field name="contacts[1].telephone" />

@sinoui/rx-form-state 还提供了useFieldArrayFieldArray辅助嵌套表单的开发。

本篇教程我们主要以以下三种嵌套表单为例,阐述不同场景嵌套表单的实现方式:

  • 简单的嵌套表单 -- 填写地址
  • 列表类型的嵌套 -- 添加联系人
  • 深层嵌套 -- 添加常用联系人

简单的嵌套表单

简单的嵌套表单不需要过多复杂的处理,只需要指定正确的name值即可。

基本用法:

import React from 'react';
import {
useFormState,
FormStateContext,
Field,
FormValueMonitor,
} from '@sinoui/rx-form-state';
function FormDemo() {
const formState = useFormState();
return (
<FormStateContext.Provider value={formState}>
<form formState={formState}>
<div>
<label>用户名</label>
<Field as="input" name="userName" required />
</div>
<label>地址:</label>
<div>
<label>城市</label>
<Field as="input" name="address.city" required />
</div>
<div>
<label>/</label>
<Field as="input" name="address.region" required />
</div>
<div>
<label>街道</label>
<Field as="input" name="address.street" required />
</div>
<FormValueMonitor>
{(values) => (
<div>表单值:{JSON.stringify(values, undefined, 2)}</div>
)}
</FormValueMonitor>
</form>
</FormStateContext.Provider>
);
}

运行效果:

表单值:{}

列表类型的嵌套表单

可以借助useFieldArray实现列表类型的嵌套表单,我们以添加联系方式为例来说明列表类型的嵌套列表应该如何实现:

TelephoneForm.tsx

import React from 'react';
import { Field, useFieldArray } from '@sinoui/rx-form-state';
const types = ['家庭', '工作', 'iPhone', '手机', '主要', '工作传真', '其他'];
function TelephoneForm() {
const {
getFieldName: name, // 获取表单域名称
items, // 列表数据项
insert, // 插入一条新的数据
remove, // 移除一条数据
push, // 在数据项最后新增一条数据
swap, // 移动数据项
} = useFieldArray('telephones');
const handlePush = () => {
if (items.length < types.length) {
const idx = types.findIndex(
(type) => items.findIndex((item: any) => item.type === type) === -1,
);
if (idx !== -1) {
push({ type: types[idx] });
}
} else {
push({ type: '其他' });
}
};
const handleInsert = (index: number) => {
if (items.length < types.length) {
const idx = types.findIndex(
(type) => items.findIndex((item: any) => item.type === type) === -1,
);
if (idx !== -1) {
insert(index + 1, { type: types[idx] });
}
} else {
insert(index + 1, { type: '其他' });
}
};
return (
<div style={{ paddingTop: 16, paddingBottom: 16 }}>
<label> 添加电话</label>
{items.map((_telephone, index) => (
<div
// eslint-disable-next-line react/no-array-index-key
key={index}
style={{
display: 'flex',
padding: 8,
border: '1px solid green',
marginTop: 8,
marginBottom: 8,
}}
>
<div>
<Field
name={name(index, 'type')}
as="input"
required
placeholder="类型"
/>
</div>
<div>
<Field
name={name(index, 'telephone')}
as="input"
required
maxLength={11}
minLength={4}
placeholder="电话"
/>
</div>
<button type="button" onClick={() => handleInsert(index)}>
+
</button>
<button type="button" onClick={() => remove(index)}>
-
</button>
{index > 0 && (
<button type="button" onClick={() => swap(index, index - 1)}>
⬆️
</button>
)}
{index < items.length - 1 && (
<button type="button" onClick={() => swap(index, index + 1)}>
⬇️
</button>
)}
</div>
))}
<button type="button" onClick={handlePush}>
+
</button>
</div>
);
}
export default TelephoneForm;

FormDemo.tsx

import React from 'react';
import { Field, useFormState, FormStateContext } from '@sinoui/rx-form-state';
import TelephoneForm from './TelephoneForm';
function FormDemo() {
const formState = useFormState();
return (
<FormStateContext.Provider value={formState}>
<form>
<div>
<label>姓氏</label>
<Field as="input" type="text" name="firstName" required />
</div>
<div>
<label>名字</label>
<Field as="input" type="text" name="lastName" required />
</div>
<div>
<label>公司</label>
<Field as="input" type="text" name="company" />
</div>
<TelephoneForm />
<div>
<label>备注</label>
<Field as="input" name="note" />
</div>
</form>
</FormStateContext.Provider>
);
}

上述示例的数据结构为:

{
"firstName": "",
"lastName": "",
"company": "",
"telephones": [{ "type": "", "telephone": "" }],
"note": ""
}

运行效果:

表单值:{}

深层嵌套

深层嵌套表单的实现过程中,我们可能需要多次使用useFieldArray,此时需要特别注意参数的指定。

添加常用联系人的数据结构如下:

{
"userName": "",
"topContacts": [
{
"userName": "",
"telephones": [{ "type": "", "telephone": "138xxxx0015" }]
}
]
}

分析上述数据结构,我们要维护常用联系人以及联系人的联系方式,需要创建两层嵌套表单,分别是:联系人表单(TopContactsForm)和联系方式表单(TelephoneForm)。 我们可以使用两次useFieldArray来实现。具体如下:

TopContactsForm.tsx

/* eslint-disable react/no-array-index-key */
/* eslint-disable jsx-a11y/label-has-for */
/* eslint-disable jsx-a11y/label-has-associated-control */
/* eslint-disable jsx-a11y/accessible-emoji */
import React from 'react';
import { useFieldArray, Field } from '../../src';
import TelephoneForm from './TelephoneForm';
function FormInner(props: any) {
const { name, index, insert, remove, swap, itemsLength } = props;
return (
<div
style={{
display: 'flex',
padding: 8,
border: '1px solid green',
margin: 8,
}}
>
<div>
<Field
name={name(index, 'userName')}
as="input"
required
placeholder="姓名"
/>
</div>
<TelephoneForm parentName={`topContacts[${index}]`} />
<button type="button" onClick={() => insert(index + 1, {})}>
+
</button>
<button type="button" onClick={() => remove(index)}>
-
</button>
{index > 0 && (
<button type="button" onClick={() => swap(index, index - 1)}>
⬆️
</button>
)}
{index < itemsLength - 1 && (
<button type="button" onClick={() => swap(index, index + 1)}>
⬇️
</button>
)}
</div>
);
}
const Item = React.memo(FormInner);
function TopContactsForm() {
const {
getFieldName: name,
items,
insert,
remove,
push,
swap,
} = useFieldArray('topContacts');
return (
<div style={{ paddingTop: 16, paddingBottom: 16 }}>
<label>添加常用联系人</label>
{items.map((_telephone, index) => (
<Item
key={index}
index={index}
name={name}
itemsLength={items.length}
insert={insert}
remove={remove}
swap={swap}
/>
))}
<button type="button" onClick={() => push({})}>
+
</button>
</div>
);
}
export default TopContactsForm;

TelephoneForm.tsx

/* eslint-disable jsx-a11y/label-has-for */
/* eslint-disable jsx-a11y/label-has-associated-control */
/* eslint-disable jsx-a11y/accessible-emoji */
import React, { useCallback, useRef, useEffect } from 'react';
import { useFieldArray, Field } from '../../src';
const types = ['家庭', '工作', 'iPhone', '手机', '主要', '工作传真', '其他'];
function FormInner(props: any) {
const { name, index, handleInsert, remove, swap, itemsLength } = props;
return (
<div
style={{
display: 'flex',
padding: 8,
border: '1px solid green',
marginTop: 8,
marginBottom: 8,
}}
>
<div>
<Field
name={name(index, 'type')}
as="input"
required
placeholder="类型"
/>
</div>
<div>
<Field
name={name(index, 'telephone')}
as="input"
required
maxLength={11}
minLength={4}
placeholder="电话"
/>
</div>
<button type="button" onClick={() => handleInsert(index)}>
+
</button>
<button type="button" onClick={() => remove(index)}>
-
</button>
{index > 0 && (
<button type="button" onClick={() => swap(index, index - 1)}>
⬆️
</button>
)}
{index < itemsLength - 1 && (
<button type="button" onClick={() => swap(index, index + 1)}>
⬇️
</button>
)}
</div>
);
}
const Item = React.memo(FormInner);
function TelephoneForm(props: { parentName?: string }) {
const { parentName } = props;
const {
getFieldName: name,
items,
insert,
remove,
push,
swap,
} = useFieldArray(parentName ? `${parentName}.telephones` : 'telephones');
const itemsRef = useRef(items);
useEffect(() => {
itemsRef.current = items;
}, [items]);
const handlePush = () => {
if (items.length < types.length) {
const idx = types.findIndex(
(type) => items.findIndex((item: any) => item.type === type) === -1,
);
if (idx !== -1) {
push({ type: types[idx] });
}
} else {
push({ type: '其他' });
}
};
const handleInsert = useCallback(
(index: number) => {
if (itemsRef.current.length < types.length) {
const idx = types.findIndex(
(type) =>
itemsRef.current.findIndex((item: any) => item.type === type) ===
-1,
);
if (idx !== -1) {
insert(index + 1, { type: types[idx] });
}
} else {
insert(index + 1, { type: '其他' });
}
},
[insert],
);
return (
<div style={{ paddingTop: 4, paddingBottom: 4 }}>
<label> 添加电话</label>
{items.map((_telephone, index) => (
<Item
// eslint-disable-next-line react/no-array-index-key
key={index}
index={index}
itemsLength={items.length}
name={name}
remove={remove}
swap={swap}
handleInsert={handleInsert}
/>
))}
<button type="button" onClick={() => handlePush()}>
+
</button>
</div>
);
}
export default TelephoneForm;

FormDemo.tsx

import React from 'react';
import { Field, useFormState, FormStateContext } from '@sinoui/rx-form-state';
import TopContactsForm from './TopContactsForm';
function FormDemo() {
const formState = useFormState();
return (
<FormStateContext.Provider value={formState}>
<form>
<div>
<label>用户名</label>
<Field as="input" type="text" name="userName" required />
</div>
<TopContactsForm />
</form>
</FormStateContext.Provider>
);
}

运行效果:

嵌套表单的性能优化建议

对于复杂的嵌套表单,我们要特别注意其性能优化。比如列表类型的嵌套表单,我们在改变其中一条记录的数据时,理想状态是只有当前的这条数据记录相关组件重新渲染。 此时,我们只需要把相关组件使用React.memo渲染即可。具体使用可参照深层嵌套。 更多关于组件性能优化方式可参考React 组件性能优化

嵌套表单的全局校验

简单嵌套表单的全局校验只需要注意其返回值的数据结构,具体事例可参考嵌套表单校验; 列表类型的嵌套表单在做全局校验时,建议拆分校验函数,而非在一个校验函数中写大段的逻辑处理,例如上述深层嵌套表单的全局校验函数我们使用下面的方式定义:

FormDemo.tsx

/**
* 联系方式校验
*/
function validateTelephone(value) {
const errors = {};
if (value.telephone && value.telephone.length < 4) {
errors.telephone = '不能少于4位';
}
return errors;
}
/**
* 常用联系人校验
*/
function validateTopContact(topContact) {
const errors = {};
if (!topContact.userName) {
errors.userName = '必填';
}
if (topContact.telephones) {
errors.telephones = topContact.telephones.map(validateTelephone);
}
return errors;
}
function validate(values) {
let errors = {};
if (!values.userName) {
errors.userName = '必填';
}
if (values.topContacts) {
errors.topContacts = values.topContacts.map(validateTopContact);
}
return errors;
}
function FormDemo() {
const formState = useFormState({}, { validate });
return (
<FormStateContext.Provider value={formState}>
<form>
<div>
<label>用户名</label>
<Field as="input" type="text" name="userName" />
</div>
<TopContactsForm />
<FormValueMonitor>
{(values) => (
<div>表单值:{JSON.stringify(values, undefined, 2)}</div>
)}
</FormValueMonitor>
</form>
</FormStateContext.Provider>
);
}

运行效果:

表单值:{}

表单域值关联

嵌套表单的表单域值关联,跟普通表单的表单域值关联处理方式基本一致。 只是依赖函数在处理值的时候需要特别注意一下,例如调整上述的联系人示例, 有三个表单域,分别是:姓氏、名字和姓名,它们之间存在一种关系:姓名 = 姓氏 + 名字

数据结构:

{
"topContacts": [
{
"firstName": "张",
"lastName": "三",
"userName": "张三"
}
]
}

表单域的定义如下:

<Field
name={name(index, 'firstName')}
as="input"
required
placeholder="姓氏"
/>
<Field
name={name(index, 'lastName')}
as="input"
required
placeholder="名字"
/>
<Field
name={name(index, 'userName')}
as="input"
required
placeholder="姓名"
readOnly
relyFields={[name(index, 'firstName'), name(index, 'lastName')]}
relyFn={(values) => relyFn(values, index)}
/>

relyFn函数的定义如下:

/**
* 计算值的依赖函数 这里需要也别注意值处理,如果层次特别深的话,建议使用lodash的get方法
*/
const relyFn = (values, index) => {
if (values.topContacts) {
const { firstName = '', lastName = '' } = values.topContacts[index];
return `${firstName}${lastName}`;
}
return '';
};

完整示例如下:

TopContactsForm.tsx

import React from 'react';
import {
Field,
useFieldArray,
useFormState,
FormValueMonitor,
FormStateContext,
} from '@sinoui/rx-form-state';
import TelephoneForm from './TelephoneForm';
/**
* 计算值的依赖函数 这里需要也别注意值处理,如果层次特别深的话,建议使用lodash的get方法
*/
const relyFn = (values, index) => {
if (values.topContacts) {
const { firstName = '', lastName = '' } = values.topContacts[index];
return `${firstName}${lastName}`;
}
return '';
};
function FormInner(props: any) {
const { name, index, insert, remove, swap, itemsLength } = props;
return (
<div
style={{
display: 'flex',
padding: 8,
border: '1px solid green',
margin: 8,
}}
>
<div>
<Field
name={name(index, 'firstName')}
as="input"
required
placeholder="姓氏"
/>
</div>
<div>
<Field
name={name(index, 'lastName')}
as="input"
required
placeholder="名字"
/>
</div>
<div>
<Field
name={name(index, 'userName')}
as="input"
required
placeholder="姓名"
readOnly
relyFields={[name(index, 'firstName'), name(index, 'lastName')]}
relyFn={(values) => relyFn(values, index)}
/>
</div>
<TelephoneForm parentName={`topContacts[${index}]`} />
<button type="button" onClick={() => insert(index + 1, {})}>
+
</button>
<button type="button" onClick={() => remove(index)}>
-
</button>
{index > 0 && (
<button type="button" onClick={() => swap(index, index - 1)}>
⬆️
</button>
)}
{index < itemsLength - 1 && (
<button type="button" onClick={() => swap(index, index + 1)}>
⬇️
</button>
)}
</div>
);
}
const Item = React.memo(FormInner);
function TopContactsForm() {
const {
getFieldName: name,
items,
insert,
remove,
push,
swap,
} = useFieldArray('topContacts');
return (
<div style={{ paddingTop: 16, paddingBottom: 16 }}>
<label>添加常用联系人</label>
{items.map((_telephone, index) => (
<Item
key={index}
index={index}
name={name}
itemsLength={items.length}
insert={insert}
remove={remove}
swap={swap}
/>
))}
<button type="button" onClick={() => push({})}>
+
</button>
</div>
);
}

FormDemo.tsx

function FormDemo() {
const formState = useFormState();
return (
<FormStateContext.Provider value={formState}>
<form>
<div>
<label>用户名</label>
<Field as="input" name="userName" required />
</div>
<TopContactsForm />
<FormValueMonitor>
{(values) => (
<div>表单值:{JSON.stringify(values, undefined, 2)}</div>
)}
</FormValueMonitor>
</form>
</FormStateContext.Provider>
);
}

运行效果:

表单值:{}

全局值关联

这里我们依旧采用深层嵌套表单的示例来说明全局值关联的使用。

假设现在我们需要存储一个字段来表示常用联系人总数,此时我们需要定义一个全局依赖规则,并将这个依赖规则作为useFormState的第二个参数对象的relys属性。具体实现如下:

const countRely = [
'topContacts',
(draft) => {
if (draft.topContacts) {
draft.count = draft.topContacts.length;
}
},
];
function FormDemo() {
const formState = useFormState({}, { relys: [countRely] });
return (
<FormStateContext.Provider value={formState}>
<form>
<div>
<label>用户名</label>
<Field as="input" type="text" name="userName" />
</div>
<TopContactsForm />
<div>
<label>常用联系人总数</label>
<Field
as="input"
type="number"
name="count"
placeholder="常用联系人总数"
/>
</div>
<FormValueMonitor>
{(values) => (
<div>表单值:{JSON.stringify(values, undefined, 2)}</div>
)}
</FormValueMonitor>
</form>
</FormStateContext.Provider>
);
}

运行效果:

表单值:{}

如果上述示例中的联系人总数不需要存储,只是用来展示统计结果的话,我们不需要使用全局值关联,只需要借助FormValueMonitor即可实现,具体如下:

function FormDemo() {
const formState = useFormState();
return (
<FormStateContext.Provider value={formState}>
<form>
<div>
<label>用户名</label>
<Field as="input" type="text" name="userName" />
</div>
<TopContactsForm />
<FormValueMonitor>
{(values) => (
<div>
常用联系人总数:{values.topContacts && values.topContacts.length}
</div>
)}
</FormValueMonitor>
</form>
</FormStateContext.Provider>
);
}

运行效果:

常用联系人总数: