【译】使用React和HTML5表单验证API处理表单元素

作者: Dmitry Sheiko | 译:甄玉磊
原文地址:[http://dsheiko.com/weblog/handling-forms-with-react-and-html5-form-validation-api]
【译者注:链接序号对应下面扩展阅读,另外可以点击阅读原文查看详细的链接文章】

简介:React 没有内置的表单验证逻辑,但是我们可以使用第三方解决方案。这种方法可能是开发包、表单生成器,还可能是 HOC 或者是包含校验逻辑的任意表单容器组件。那么选择哪种方法呢?我们将在本文中一 一介绍。

每当我们提及应用程序中的用户输入框时,首先映入脑海的就是 HTML 的表单元素。最早的 HTML 版本就已经支持 Web 表单。众所周知,这一特性于 1991 年提出,且在 1995 年作为 RFC(征求评议文件) 1866[1] 号协议标准化。与此同时表单元素也得到了广泛应用,几乎每一个代码库和框架中都在使用。那么在 React 中如何使用呢? Facebook 在如何处理表单上提供了受控的输入框[2]。该输入框指的是受控表单,主要是通过交互事件和通过 value 属性传递 state 值实现对输入框的控制。因此,你可以决定表单的校验和提交逻辑。拥有好的用户体验的 UI ,意味着你应该考虑到诸如“提交”、“校验”的逻辑以及内联的错误提示信息,根据有效性、原始状态和提交状态来切换元素。难道我们不能提取这种逻辑,简单的插入到表单元素中吗?当然可以,唯一要考虑的问题是我们要采用何种方法和解决方案。

组件库中的表单

如果你习惯使用诸如 ReactBootstrap[3] 或者 AntDesign[4] 等 React 组件库,很可能已经对其中的表单组件感到满意。这两个组件库所提供的表单组件满足了多种需求。例如在 AntDesign 组件库中我们定义了一个表单元素 Form 以及带有表单域的 FormItem ,即包裹了任意输入控制的容器。你可以在 FormItem 设置校验规则:

{getFieldDecorator('select', {
rules: [
{ required: true, message: 'Please select your country!' },
],
})(
<select>
<option value="china">China</option>
<option value="use">U.S.A</option>
</select>
)}

举例来说,在处理表单提交时,你可以执行 this.props.form.validateFields() 来验证。这样看起来似乎所有事情都考虑到了。然而该解决方案是针对框架的。如果你没有使用这些组件库,则无法使用该方法。

基于schema构建表单

或者我们可以基于 JSON 细则构建单独的表单组件,例如我们可以引入 Winterfell 组件[5],构建如下所示的表单组件

<br />

然而这个方案是相当复杂的[6]。除此之外,我们还需要满足特定的语法。类似的另一种解决方案 react-jsonschema-form[7],依赖于 JSON schema[8]。JSON schema 是一种与项目无关的数据文档,用来注释和校验 JSON 文档。但是,它可以将我们定义的特有属性应用在项目中,且在文档中定义。

Formsy(译者注:一种 React 表单验证组件)

我更倾向于使用一种具有逻辑校验,适用于任意 HTML 表单的组件。其中最常用的方法是—— Formsy[9]。是什么样的结构呢?我构建了自己的表单组件,并用HOC(Higher-Order Components,高阶组件)
Formsy 包裹起来:

import { withFormsy } from "formsy-react";
import React from "react";

class MyInput extends React.Component {

changeValue = ( event ) =&gt; {
this.props.setValue( event.currentTarget.value );
}

render() {
return (
&lt;div&gt;&lt;input type="text" value="{" /&gt;
{ this.props.getErrorMessage() }&lt;/div&gt;
);
}
}

export default withFormsy( MyInput );

如上所示,组件的 props 属性中接收了 getErrorMessage() 函数,该函数可以用来生成行内错误提示信息。
这样我们开发了一个输入域组件,将其放入表单中如下所示:

import Formsy from "formsy-react";
import React from "react";
import MyInput from "./MyInput";

export default class App extends React.Component {

onValid = () =&gt; {
this.setState({ valid: true });
}

onInvalid = () =&gt; {
this.setState({ valid: false });
}

submit( model ) {
//...
}

render() {
return (

&lt;button disabled="disabled" type="submit"&gt;Submit&lt;/button&gt;
);
}
}

我们指定所有需要验证的区域都会添加 validations 属性(详见 validations 列表属性[10])。使用 validationError 属性来设置所期望的校验信息,通过 onValidonInvalid 接收表单校验状态。
这样看上去很简单、干净、灵活。但是我想知道的是为什么我们不依赖 HTML5 的表单验证[11],而是使用繁多的自定义实现呢?

HTML5表单验证

这项技术在很早之前就出现了,首次实现是在 2008 年的 Opera 9.5 中。现在所有的现代浏览器都支持它。表单(数据)验证[12]引入额外的 HTML 属性和 input 类型,这些可以用来设置表单的校验规则。这些校验还可以使用特有的 API [13]来控制和自定义表单JavaScript
让我们看下面的代码:

<br />&lt;form&gt;&lt;label for="answer"&gt;What do you know, Jon Snow?&lt;/label&gt;
&lt;input id="answer" name="answer" required="" type="text" /&gt;
&lt;button&gt;Ask&lt;/button&gt;&lt;/form&gt;```
这是一个简单的表单,期望的功能—— `&amp;lt;input&amp;gt;` 元素有必填的属性。因此,如果我们快速按下提交按钮,表单内容不会提交到服务器。相反我们会看到在输入框旁出现提示信息,提示 `value` 值没有满足给出的限制条件(不能为空)。

&lt;a href="http://img12.360buyimg.com/uba/jfs/t20722/99/1200275996/4305/bec1c2a/5b21df0aN9a9e5424.png"&gt;&lt;img src="http://img12.360buyimg.com/uba/jfs/t20722/99/1200275996/4305/bec1c2a/5b21df0aN9a9e5424.png" /&gt;&lt;/a&gt;

现在我们给输入框增加约束条件

What do you know, Jon Snow?

Ask

这样 value 值不仅仅是必填值,还必须完全符合由 `pattern` 属性设置的规范。

&lt;img src="http://img14.360buyimg.com/uba/jfs/t21367/238/1163573212/5237/b8bf8393/5b21df59N5b2c16b1.png" /&gt;

错误提示信息并没有给出我们期望的信息,不是吗?我们可以自定义它(例如为了解释所期望的用户输入值)或者仅仅对输入值进行转换。

What do you know, Jon Snow?

Ask

`

const answer = document.querySelector( "[name=answer]" );
answer.addEventListener( "input", ( event ) =&gt; {
if ( answer.validity.patternMismatch ) {
answer.setCustomValidity("Oh, it's not a right answer!");
} else {
answer.setCustomValidity( "" );
}
});

以上代码只是检查 input 框的输入事件,其校验状态 patternMismatch 的变化。当输入的值与定义的 pattern 不匹配时,就会出现错误提示信息。如果我们设置了其他的限制条件[14],也会被我们所定义的事件处理函数所覆盖。

你对这个工具提示不太满意?是的,它在不同的浏览器中表现形式是不一样的。让我们给表单元素增加novalidate属性,并且自定义错误提示:

<br /><br /><br /><form><label for="answer">What do you know, Jon Snow?</label>

<div></div>
<button>Ask</button>

</form>

const answer = document.querySelector( "[name=answer]" ),
answerError = document.querySelector( "[name=answer] + [data-bind=message]" );

answer.addEventListener( "input", ( event ) =&gt; {
answerError.innerHTML = answer.validationMessage;
});

尽管这个介绍比较简短,你也能体会到技术背后的力量和灵活性。最重要的是,它是原生的表单验证。所以我们为什么要依赖繁多的自定义库。而不去使用原生的验证呢?

满足表单校验的 React API

react-html5-form 将 React(还可以选择Redux) 和 HTML5 表单校验 API 联系起来了。它提供 ‘From’ 组件和 ‘InputGroup’ 组件(类似于 Formsy 组件库中的 input,或者 AntDesign 组件库中的 FormItem )。这样,Form 组件定义了表单及其作用区域。InputGroup 组件可以包含一个或多个输入框。我们简单地用这些组件包裹一个任意的表单内容(只是普通的 HTML 或 React 组件)。在用户事件上,我们可以请求表单验证,并根据有效的输入值,获得 Form 和 InputGroup 组件的更新状态。
好了,让我们实践一下,首先我们定义表单区域:

import React from "react";
import { render } from "react-dom";
import { Form, InputGroup } from "Form";

const MyForm = props =&gt; (



<form>{({ error, valid, pristine, submitting, form }) =&gt; (

Form content
<button disabled="disabled" type="submit">Submit</button>

)}</form>

);

render( , document.getElementById( "app" ) );

作用域接收到的状态对象具有以下属性:
– error- 表单错误信息(通常指的是服务端的校验信息),可以通过 form.setError()进行设置;
– valid- 布尔值,表示所有的输入是否全部满足规定的约束;
– pristine- 布尔值,表示用户是否和表单进行过交互;
– submitting- 布尔值,表示是否正在提交表单(当用户按下提交按钮时该状态转化为 true,一旦用户定义的异步提交逻辑处理结束,该值变为 false)
– form- 用于访问表单组件 API 的表单实例;

在这里我们使用 pristine 和 submitting 属性将提交按钮切换为禁用状态。
为了在提交表单的同时校验表单输入信息,我们使用 InputGroup 包裹这些input表单。

<br />{({ error, valid }) =&gt; (


<div><label>Email address</label>

{ error &amp;&amp; (
<div>{error}</div>
) }

</div>


)}

使用 validate 属性我们可以指定该组内应该使用什么样的 input。[“email”] 意味着我们只有一个名称为“email”的input。
在这个作用区域内我们接收到的状态对象具有下面的属性:
– errors – 所有注册 input 的错误信息数组;
– error – 最后显示的错误信息;
– valid – 布尔值,表示是否所有的输入数据全部满足规定的约束条件;
– inputGroup – 访问表单组件 API 的实例;

渲染之后我们可以得到一个 email 类型的输入框。如果该值为空,或在提交时包含无效的电子邮件地址,则在输入框旁边显示相应的验证消息。

还记得我们在使用原生表单校验 API 自定义错误提示信息时的焦头烂额吗?使用 InputGroup 这一情况将变得好转:

<br />...

我们可以将每个 input 定义为 key-value 的哈希结构,其中 keys 表示有效性属性[15],values 表示自定义消息。
自定义消息很简单。那么自定义验证该如何实现呢?我们可以使用校验的 prop 来做到:

{
if ( !EMAIL_WHITELIST.includes( input.current.value ) ) {
input.setCustomValidity( "Only whitelisted email allowed" );
return false;
}
return true;
}
}}&gt;
...

在这个例子中,我们提供的是包含输入框名字的哈希而不是数组,其中 key 是输入框的名字, value 是验证处理函数。处理函数校验输入值(可以异步完成)并返回一个布尔类型的有效状态。使用 input.setCustomValidity,我们可以自定义验证消息。
输入框并不总是提交时才进行校验。为了达到实时验证,首先我们需要给输入事件定义事件处理函数:

const onInput = ( e, inputGroup ) =&gt; {
inputGroup.checkValidityAndUpdate();
};

事实上,我们可以在用户每次输入时重新验证。控制逻辑如下所示:

&lt;input name="email" required="" type="email" /&gt; onInput( e, inputGroup, form ) }
id="emailInput" /&gt;

这样,每当输入值改变时都会进行验证。如果输入值是无效的,我们就会马上收到错误的提示信息。
你可以通过这里找到以上示例的源代码[16]。
顺便说一句,你是否考虑过将组件派生的表单状态树和 Redux 存储值联系起来?我们也可以这样做。
Redux 提供了包含所有注册表单状态树的 reducer:html5form。我们可以按照如下所示将 html5form 和 store 结合起来:

import React from "react";
import { render } from "react-dom";
import { createStore, combineReducers } from "redux";
import { Provider } from "react-redux";

import { App } from "./Containers/App.jsx";
import { html5form } from "react-html5-form";

const appReducer = combineReducers({
html5form
});

// Store creation
const store = createStore( appReducer );

render(
, document.getElementById( "app" ) );

当我们运行该应用程序时,就能在 store 中找到所有与表单相关的状态。

点击这里查看上述 demo 的源代码[17]。

总结

React 没有内置的表单验证逻辑,但是我们可以使用第三方解决方案。这种方法可能是开发包、表单生成器,还可能是 HOC 或者是包含校验逻辑的任意表单容器组件。我个人倾向于使用容器组件,该组件依赖于 HTML 内置表单验证 API ,并在表单和表单域的范围中显示有效性状态。
– react-html5-form 源代码[18]
– 演示例子链接[19]

扩展阅读:

[1] https://tools.ietf.org/html/rfc1866
[2] https://reactjs.org/docs/forms.html
[3] https://react-bootstrap.github.io/components/forms/
[4] https://ant.design/components/form/
[5] https://github.com/andrewhathaway/Winterfell
[6] https://github.com/andrewhathaway/Winterfell/blob/master/examples/schema.js
[7] https://github.com/mozilla-services/react-jsonschema-form
[8] http://json-schema.org/
[9] https://github.com/formsy/formsy-react
[10] https://github.com/formsy/formsy-react/blob/master/API.md#validators
[11] https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Form_validation
[12] https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Form_validation
[13] https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation
[14] https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation
[15] https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
[16] https://github.com/dsheiko/react-html5-form/blob/master/demo/bootstrap/src/index.jsx
[17] https://github.com/dsheiko/react-html5-form/blob/master/demo/bootstrap-redux/src/index.jsx
[18] https://github.com/dsheiko/react-html5-form
[19] https://dsheiko.github.io/react-html5-form

更多内容请关注我们团队的公众账号“全栈探索”

文章来源:

Author:开元
link:https://jdc.jd.com/archives/212629