1.更多用户交互。这意味着可能需要大量的自定义组件,如数据选择器、上传、自动完成等。
3.频繁的状态变化。每次用户输入一个值,它可能会改变应用程序状态,因此有必要更新表单元素或显示错误消息。
3.表单验证,即验证用户输入数据的有效性。表单验证的形式有很多,比如验证时的输入,失去焦点后的验证,或者提交表单前的验证等等。
4.异步网络通信。当用户输入和异步网络通信并存时,需要考虑的事情就更多了。例如,自动完成需要根据用户的输入异步获取相应的数据。如果用户每次输入都发起一个请求,会造成很大的资源浪费。因为每个输入都是异步获取数据的,所以两个连续的用户输入所获得的数据也可能存在“迟到早退”的问题。
正是由于这些特点,形式的发展变得困难。在接下来的章节中,我们将结合RxJS和Form来帮助我们更好地解决这些问题。
保存表单状态
对于表单组件,需要保存所有表单元素的信息(如值、有效性等)。),HTML表单也不例外。
那么,HTML表单在哪里保存表单状态呢?如何获取表单元素信息?
主要有以下方法:
Document.forms返回所有表单节点。返回所有表单元素。Event.target.elements还可以获取所有的表单元素。document.forms[0]。元素[0]。价值;//获取第一个form const form=document . queryselector(' form ')中第一个form元素的值;form.elements[0]。价值;form.addEventListener('submit ',function(event){ console . log(event . target . elements[0])。值);});确认
表单验证有两种类型:
内置表单检查。默认情况下,提交表单时会自动触发。您可以通过设置novalidate属性来关闭浏览器的自动验证。
JavaScript验证。
表单新验证输入名称='username '必填/输入名称='password '类型='password '必填minlength='6' maxlength='6'/输入名称=' email '类型=' email '/输入类型=' submit '值=' submit'/表单有问题
定制很难。例如,不支持内联验证,表单只能在提交时验证,不能自定义错误消息的样式。
很难处理复杂的场景。比如表单元素的嵌套。
输入组件的行为不统一,这使得获取表单元素的值变得困难。例如,复选框和多选不能直接取值,需要额外转换。
var $ form=document . queryselector(' form ');函数getFormValues(form){ var values={ };var元素=form.elements//elemtns是(var I=0;一.要素.长度;I){ var input=elements[I];if(input . name){ switch(input . type . tolowercase()){ case ' checkbox ' : if(input . checked){ values[input . name]=input . checked;} breakcase ' select-multiple ' : values[input . name]=values[input . name]| |[];for(var j=0;j输入长度;j ) { if(输入[j]。选定){ values[input.name]。推送(输入[j]。值);} } breakdefault : values[input . name]=input . value;打破;} } }返回值;} $ form . addeventlistener(' submit ',function(event){ event . preventdefault();getFormValues(event . target);console . log(event . target . elements);console . log(getFormValues(event . target));});反应接收形式
感兴趣的同学可以先看看https://github.com/reeli/react-rx-form的源代码
反应和RxJS
RxJS是一个非常强大的数据管理工具,但是它不具备渲染用户界面的功能,而React特别擅长处理界面。那为什么不结合他们的优势呢?使用React和RxJS来解决我们的表单问题。既然知道了各自的长处,分工就相对明确了。
RxJS负责管理状态,React负责渲染界面。
设计理念
与reducxform不同,我们不将表单的状态存储在store中,而是直接存储在Form/component中。然后用RxJS通知每个Field/数据,然后Field/组件会根据数据决定是否需要更新UI。如果需要更新,会调用setState,否则什么都不做。
例如,假设表单中有三个字段(如下所示)。当只有字段a的值发生变化时,为了防止表单/及其子组件重新呈现,Redux表单的内部需要受到shouldComponentUpdate()的限制。
//伪代码Form FieldA/FieldB/FieldC//Form,而RxJS可以把组件更新的粒度控制到最小,换句话说就是真正需要重渲染Field/重渲染,但不需要重渲染的组件,不会被重渲染。
核心是主体
从以上设计思路,可以总结出以下两个问题。
表单和字段的关系是一对多,表单的状态需要通知多个字段。字段需要根据数据修改组件的状态。第一个问题,需要的是一个可以支持组播的可观测函数。第二个问题需要一个观察者的功能。在RxJS中,它不仅是可观测的,还是观测者,还可以实现组播,这就是Subject!因此,在实现表单时,将广泛使用主题。
FormState数据结构
表单组件还需要一个状态来保存所有字段的状态,这个状态就是表单状态。
那么formState的结构应该如何定义呢?
在最早的版本中,formState的结构是:长度如下
接口表单状态{[字段名称:字符串]: {脏?布尔型;感动?布尔型;参观过吗?布尔型;错误? TErrorvalue:字符串;};}formState是一个对象,其键是FieldName,其值是保存字段状态的对象。
看起来不错,不是吗?
酪。
最后,formState的结构变成了下面的:
接口表单状态{ field s : {[field name : string]: { dirty?布尔型;感动?布尔型;参观过吗?布尔型;错误?字符串|未定义;};};values : {[field name : string]: any;};}注意:字段不包含字段值,只包含字段的一些状态信息。值中只有字段值。
为什么呢?
事实上,在实现最基本的表单和字段组件时,以上两种数据结构都是可行的。
到底是什么问题?
在这里买把钥匙。目前你只需要知道formState的数据结构是什么样子就可以了。
数据流
为了更好地理解数据流,让我们看一个简单的例子。我们有一个表单组件,它包含一个字段组件和字段组件内的文本输入。数据流可以是:如下所示
用户在输入框中输入字符。输入的更改事件将被触发。字段的变更操作将被分派。根据字段的更改操作,修改表单状态。当表单状态更新时,将通知字段的观察者。该字段的观察者挑选出当前字段的状态,设置有任何更新时的状态选择,如果没有更新,则不做任何事情。设置状态将使字段重新显示,新的字段值可以通知输入。核心组件
首先,我们需要创建两个基本组件,一个字段组件和一个表单组件。
场分量
字段组件是连接表单组件和表单元素的中间层。它的功能是使输入组件的职责更加单一。有了它,Input只需要显示,不需要关心其他复杂的逻辑(validate/normalize等)。).此外,对于Input组件,它不仅可以用在Form组件中,还可以用在Form组件之外(有些地方可能不需要validate之类的逻辑),所以Field层的抽象还是很重要的。
拦截和转换。格式化/解析/规范化.表单验证。参考HTML表单的表单验证,我们可以将验证放在Field组件上,并结合验证规则来满足不同的需求。触发字段状态的更改(如已触摸、已访问),以提供子组件所需的信息。字段状态(错误、已触摸、已访问.)和回调函数(onChange、onBlur.)来绑定表单元素的事件。RxJS的功能是用来控制Field组件的更新,减少不必要的重编。与表单交流。当字段状态发生变化时,需要通知表单。在表单中,字段的状态发生了变化,需要通知字段。
表单组件
管理表单状态。表单组件向字段提供表单状态,并在字段更改时通知表单。提供formValues。当表单验证失败时,阻止表单提交。通知字段表单状态的每次更改。表单主题将在表单中创建;表单状态的每一次更改都将受到formSubjectdollar的影响;在上发送数据,每个字段将被注册为表单主题美元;观察者。也就是说,Field知道表单状态的每一个变化,所以它可以决定在适当的时候更新它。当格式更改时通知字段。例如,当您开始提交时。组件之间的通信
1.表单和字段之间的通信。
上下文主要用于跨级别组件通信。在实际开发中,表单和字段可能会跨级别,因此我们需要使用Context来确保表单和字段之间的通信。表单通过上下文向字段提供其实例方法和表单状态。
字段和表单之间的通信。
表单组件为字段组件提供了一个d__ispatch__方法,用于字段和表单之间的通信。该表单管理所有字段的状态和值。如果要更新字段的状态或值,必须调度相应的操作。
3.表单元素和字段之间的通信
表单元素和Field之间的通信主要是通过回调函数。字段提供回调函数,如onChange和onBlur来形成元素。
界面设计
对于界面的设计,简洁明了非常重要。因此,字段只保留必要的属性,不通过字段传递表单元素所需的其他属性,而是让它们自己定义。
通过子渲染,给子组件提供了相应的状态和方法,结构和层次更加清晰。
字段:
类型TValidator=(value : string | boolean)=string | undefined;接口IFieldProps { children :(prop : IFieldInnerProps)=React。ReactNodename:字符串;默认值?任何;验证?验证器| t验证器[];}Form:
接口IRxFormProps { children :(prop 3360 irxforminerprops)=React。ReactNode初始值? {[field name : string]: any;}}在这里,一个最基本的表单就完成了。接下来,我们将扩展它以满足更复杂的业务场景。
字段数组主要用于渲染多组字段。
回到我们之前的问题,为什么要把formState的结构分为fileds和values呢?
实际上,问题在于FieldArray。
初始长度由initLength或formValues决定。formState的整体更新。FormValues
使用RxJS,我们将字段更新的粒度控制到最小,也就是说,如果一个字段的值发生变化,它不会导致Form组件和其他Feild组件重新呈现。
由于菲尔德只能感知自身价值的变化,问题就出现了。如何实现字段之间的联动?
因此,FormValues组件应运而生。
每当表单值改变时,表单值组件将通知子组件新的表单值。也就是说,如果使用FormValues组件,formValues组件的每一次更改都会导致FormValues组件及其子组件的重新显示,因此不建议广泛使用,否则可能会带来性能问题。
总之,使用FormValues时,最好放在影响最小的地方。也就是说,当formValues发生变化时,让尽可能少的组件重新显示。
在下面的代码中,是否显示字段b需要根据字段a的值来判断,所以只需要对字段a和字段b应用FormValues。
FormValues {({ formValues,updateFormValues })=(field A name=' A '/{!formValues。字段名称=' B '/}/)}/FormValuesFormSection
FormSection主要用于对多个表单中的一组字段组复用进行分组。主要是给名字加个前缀。
那么如何给字段和字段数组的名称加前缀呢?
我的第一个想法是通过React获取子组件的名称。子级,然后用FormSection的名称进行拼接。
但是,窗体节和字段可能不是父子关系!因为Field组件也可以被提取成一个独立的组件。因此,存在跨级别组件通信的问题。
没错!我们仍将使用上下文进行跨级别组件通信。但是这里我们需要先从FormConsumer中获取对应的上下文值,然后通过Provider向Consumer提供前缀。此时,字段/字段数组通过使用者获得的是表单部分中提供者提供的值,而不是表单组件的提供者提供的值。因为消费者将消费最近的提供商提供的价值。
FormConsumer {(formcontext value)={ return(FormProvider value={ }.formContextValue,field prefix : ` $ { formcontext value . field prefix | | ' ' } $ { name } `,} } { children }/FormProvider);}}/FormConsumer
主要用于工具类方法。
集成测试
主要用于Field、FieldArray等组件。因为它们不能独立于表单而存在,所以单元测试不能用于它们。
注:在测试中,不可能直接修改实例上的属性,因此props上面的所有节点都被设置为React的只读(通过Object.defineProperty方法)。但是可以通过整体设置道具来绕过。
instance.props={.instance.props,subscribeformaction : mock subscribeformaction,dispatch: mockDispatch,};自动填写表单实用程序
如果项目中表单太多,无疑会成为QA测试的负担。这个时候,我们希望有一个自动填表的工具,帮助我们提高考试的效率。
当编写这个工具时,我们需要模拟输入事件。
input.value=' vconst event=new Event('input ',{ bubble : true });input.dispatchEvent(事件);我们的期望是通过上面的代码模拟DOM的输入事件,然后触发React的onChange事件。但是,未触发反应的onChange事件。因此,不能为输入元素设置值。
因为ReactDOM在模拟onChange事件时有一个逻辑:只有当输入的值发生变化时,ReactDOM才会生成onChange事件。
React 16将覆盖输入值设置器。有关详细信息,请参考ReactDOM的输入值跟踪。因此,我们只需要获取原始值setter并调用它。
const nativeInputValueSetter=object . getowntpropertysdescriptor(窗口。HTMLInputElement.prototype,' value ')。设置;nativeInputValueSetter.call(输入,‘v’);const event=new Event('input ',{ bubble : true });input.dispatchEvent(事件);
在开发环境中,调试可以通过日志来完成。目前,日志在开发环境中自动打印,但在其他环境中不会。日志信息主要包括:前一状态、动作和下一状态。
注:由于prevState、action、nextState都是Object,打印时不要忘记调用cloneDeep,否则最终打印值的正确性无法保证,也就是说最终结果可能不是打印瞬间的值。
最后的
本文只讲React Rx Form的思路和一些核心技术,你也可以按照这个思路自己实现一个版本。当然也可以参考源代码。欢迎提出建议和问题。Github地址: https://github.com/reeli/react-rx-form
以上就是本文的全部内容。希望对大家的学习有帮助,支持我们。