Skip to content

useForm

useFielduseFields适合于简单的表单场景,但是对于复杂的表单场景,我们需要更多的功能,比如表单校验,表单提交等。

则可以选用useFrom,其提供了更加完整的创建可绑定表单的完整解决方案,可以让更方便将AutoStore的状态和表单控件进行双向绑定,使得收集数据变得更简单。

基本原理

useForm的基本原理如下:

1. 创建Form组件

useForm返回一个Form组件,该组件是对标准form的封装。

2. 初始化表单

useForm内部的useEffect会自动初始化表单.

然后在初始化时,调用querySelectorAll获取到所有表单内部的input,textarea,select元素 ,依次遍历这些元素,根据name属性,从state中获取对应的值,并设置绑定到表单元素上,完成表单字段的初始化。

3. 订阅变更事件

要实现双向绑定,我们需要:

  • 监听表单元素的change事件

由于表单事件onchange会冒泡,所以我们只需要在form元素上监听change事件即可。

所以通过form.addEventListener('input',onChange)就可以在表单元素变化时触发捕获到onChange事件。

然后在onChange事件中,我们可以通过event.target获取到表单元素.

最后将表单元素的值更新到state[event.target.name]

  • 监听state的变化

使用store.watch监听state的变化,当state变化时,将数据更新到name=<path>的表单元素上即可。

快速入门

第1步: 创建表单

首先,我们需要使用useForm创建一个表单。

tsx
import { useForm } from "@autostorejs/react"

const { Form } = useForm({
  user:{
    firstName:"Zhang",
    lastName:"Fisher",
    age:18,
    vip:false 
  }  
})
  • useForm内部调用createStore来创建一个AutoStore,所以其本质上useForm是一个useStore超集。所以useForm返回的对象中包含了useStore返回的对象。

    tsx
    const { useReactive,watch,$,.... } = useForm({...})
  • useForm返回值中最主要的是Form组件,该组件是对标准form的封装。

第2步:声明字段

声明字段有3种方式:

简单字段

只需要为inputtextareaselect元素设置name属性,且name为一个字符串路径指向状态即可。

tsx
<input name="user.firstName" />

封装字段

当然,实际中的输入字段我们一般会进行封装,以便可以进行更多的控制。

我们也可以在封装元素上通过data-field-name='<状态路径>'标识这是一个表单字段。

tsx
<div data-field-name="user.name" >
  <label>First Name</label>
  <input/>  
  <span className="invalid"></span>
</div>
  • 使用data-field-name标识表单字段可以让表单能进行更多的控制。

Field字段组件

使用Field字段组件可以实现更复杂的控制,如校验、字段联动等等

tsx
const { Form,Field } = useForm({...})

<Field 
  name="user.name"
  validate={(value)=>value.length>=3}
  render={({value,validate,onChange,name,error})=>{
    return <>
      <label>First Name</label>
      <input name={name} value={value} onChange={onChange}/>  
      <span className="invalid"></span>
      </>
  }}
>  
</Field>

第3步:提交表单

基本用法

useForm返回一个Form组件,该组件是对标准form元素的封装。

tsx
import { useForm } from "@autostorejs/react"
const { Form } = useForm({
  user:{
    firstName:"Zhang",
    lastName:"Fisher",
    age:18,
    vip:false 
  }  
}) 
<Form>
  <input name="user.firstName" />
  <input name="user.lastName" />
  <input name="user.age" />
  <input name="user.vip" />
</Form>

就这么简单,轻松实现表单store.state之间的双向绑定了,输入的数据会自动同步到state中,反之亦然。

下面是一个简单的示例:

提示

配置input元素的name=<状态数据路径>即可。

表单校验

标准校验

当指定customReport=false时使用标准的HTML表单校验功能,只需要在input元素上设置maxLengthminLengthrequiredpattern等属性即可。

  • 当表单校验失败时,表单会提供伪类:invalid,可以通过input:invalidform:invalid来设置样式。
  • 关于更多的HTML5表单校验功能请参考:HTML5 表单验证
  • 关于input元素的属性请参考:input

自定义校验显示

多数情况下,我们并不满足于默认的校验显示方式,我们希望自定义校验显示,比如在输入控件下方显示红色错误提示。

  • customReport=true是默认情况
  • 使用data-field-name="xxxx"指定字段名。
  • 重点:由data-validate-field="<字段状态路径>"指定该元素要显示哪个字段的校验错误信息。

提示

  • : 为什么user.name在初始化时不会显示校验错误?
  • 🍨 : 上例中user.name='x'不满足minLength=3校验规则,但是在初始化时并没有显示错误。这是浏览器的默认行为,必须是有用户输入时才会校验。并且这种行为在不同的字段校验规则却是不一样的,pattern校验就会在初始化时生效。上例中的phone就会显示错误。这种不一致性主要是由浏览器决定的,不同的浏览器还不一样,要避免这种不致性,需要使用自定义校验规则。

自定义校验规则

上述两个例子中,我们使用的是标准的HTML5表单校验规则功能(patternminLength等等),这存在几个问题

  • 校验规则比较单一,无法满足复杂的校验需求。
  • 不同浏览器的校验行为存在细微差异。

为此,我们需要在使用useForm时,传入一个validate函数,该函数用于自定义校验规则,实现更复杂的校验逻辑。

validate函数的签名如下:

ts
{
  validate?:(path:string,value:any,part:string | null,fieldEle:HTMLElement)=>boolean | string
}
参数 说明
path 字段元素的name属性,即状态路径
value 字段值
part 将字段值分解为几部分时的标识,详见下文分解字段
fieldEle 字段元素
  • validate函数返回true代表校验成功,返回false代表校验失败。返回string代表校验失败时的提示信息。
  • 当校验失败时,会在field元素,input元素以信data-field-name元素自动添加invalid类。

提示

对比上例浏览器的默认行为,user.name在初始化时会显示校验错误

校验样式控制

拆分字段

input元素上 data-field-part属性可以指定字段的分割方式,将一个状态值绑定到多个input上,实现双向绑定。

正则表达式拆分

当状态值是一个字符串时,可以指定data-field-part="<正则表达式>"进行拆分。

下例中net.ip是一个ip地址,我们希望将其拆分为4input元素进行绑定。

tsx
<div data-field-name="net.ip">
    <div>                                
        <Input data-field-part="(\d{1,3})\.\d{1,3}\.\d{1,3}\.\d{1,3}" inline width={60} /> 
        <span>.</span>
        <Input data-field-part="\d{1,3}\.(\d{1,3})\.\d{1,3}\.\d{1,3}" inline width={60}/>
        <span>.</span>
        <Input data-field-part="\d{1,3}\.\d{1,3}\.(\d{1,3})\.\d{1,3}" inline width={60}/>
        <span>.</span>
        <Input data-field-part="\d{1,3}\.\d{1,3}\.\d{1,3}\.(\d{1,3})" inline width={60}/>
    </div>
</div>
  • data-field-part="<正则表达式>"只适用于指向的状态值是字符串的情况。
  • 由于状态与input是双向绑定的,而<正则表达式>不仅用于从状态中提取,也用于将输入input的值更新到状态的指定位置中。
  • 我们约定,<正则表达式>中必须具有一个捕获组,如:\d{1,3}\.(\d{1,3})\.\d{1,3}\.\d{1,3},其中(\d{1,3})就是非命名捕获组。在绑定时会读取该组的值,更新到input状态中,反之同理。
  • 正常情况下,每一个part均具有不同位置的正则捕获组,所以part的值是不同的。如果没有正确的指定捕获组,则会导致字段拆分不能正常工作。

提示

基于正则表达式的拆分是一种非常灵活的方式,适用于字符串内容,要求开发者设计可双向兼容的正则表达式。

数组拆分

当状态值是一个数组时,可以指定data-field-part="<索引>"进行快速拆分。

提示

数组拆分是一种快速的拆分方式,适用于数组内容,拆分时会识别数据类型,自动转换为对应的数据类型。

对象拆分

当状态值是一个{...}时,可以指定data-field-part="<Key>"进行快速拆分。

自定义拆分

如果上述的拆分方式不能满足需求,也可以通过自定义toStatefromState这两个配置参数来自定义拆分方式。

  • toState: 用于将input的输入值转换为状态值。
  • fromState: 用于将状态值转换为input的输入值。

注意

如果指定了toStatefromState,则需要开发者自行处理数组和对象的拆分逻辑。

表单字段

Field组件

Field组件是useForm提供的一个用于封装表单字段的高级组件,可以用于更加灵活的表单字段封装。

Field组件的基本用法如下:

tsx

const { Field, Form } = useForm({...})

<Form onSubmit={}>
    <Field 
        name="user.firstName"     // 使用绑定的状态路径
        validate={validate}       // 自定义校验规则
        render={({value,timeout,....,bind})=>{
          // 在此渲染表单字段
          return <div>
              <label>First Name</label>
              <input {...bind} />
            </div>
        }} 
    />
</Form>

提交表单

useForm提供了submit方法,用于提交表单。

  • submit方法会触发submit事件,可以通过onSubmit监听该事件。
  • 然后在onSubmit事件中,使用AJAX/fetch将表单数据提交到服务器即可。
  • submiting属性用于标识表单是否正在提交中,可以用于控制提交按钮的状态。

示例如下: