- 作者: Dmitry Sheiko | 译:甄玉磊
- 原文地址:Handling forms with React and HTML5 Form Validation API
- 译文地址:【译】使用React和HTML5表单验证API处理表单元素
(最近在折腾 React,表单这块想用原生校验,找资料时发现了这篇文章。原译文代码的部分有些乱,所以处理了一下并转到这里。)
【译者注:链接序号对应下面扩展阅读,另外可以点击阅读原文查看详细的链接文章】
简介: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 设置校验规则:
<FormItem>
{getFieldDecorator('select', {
rules: [
{ required: true, message: 'Please select your country!' },
],
})(
<Select placeholder="Please select a country">
<Option value="china">China</Option>
<Option value="use">U.S.A</Option>
</Select>
)}
</FormItem>
举例来说,在处理表单提交时,你可以执行 this.props.form.validateFields()
来验证。这样看起来似乎所有事情都考虑到了。然而该解决方案是针对框架的。如果你没有使用这些组件库,则无法使用该方法。
基于schema构建表单
或者我们可以基于 JSON 细则构建单独的表单组件,例如我们可以引入 Winterfell 组件[5],构建如下所示的表单组件
<Winterfell schema={loginSchema} ></Winterfell>
然而这个方案是相当复杂的[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 ) => {
this.props.setValue( event.currentTarget.value );
}
render() {
return (
<div>
<input
onChange={ this.changeValue }
type="text"
value={ this.props.getValue() || "" }
/>
<span>{ this.props.getErrorMessage() }</span>
</div>
);
}
}
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 = () => {
this.setState({ valid: true });
}
onInvalid = () => {
this.setState({ valid: false });
}
submit( model ) {
//...
}
render() {
return (
<Formsy onValidSubmit={this.submit} onValid={this.onValid} onInvalid={this.onInvalid}>
<MyInput
name="email"
validations="isEmail"
validationError="This is not a valid email"
required
></MyInput>
<button type="submit" disabled={ !this.state.valid }>Submit</button>
</Formsy>
);
}
}
我们指定所有需要验证的区域都会添加 validations 属性(详见 validations 列表属性[10])。使用 validationError 属性来设置所期望的校验信息,通过 onValid 和 onInvalid 接收表单校验状态。
这样看上去很简单、干净、灵活。但是我想知道的是为什么我们不依赖 HTML5 的表单验证[11],而是使用繁多的自定义实现呢?
HTML5表单验证
这项技术在很早之前就出现了,首次实现是在 2008 年的 Opera 9.5 中。现在所有的现代浏览器都支持它。表单(数据)验证[12]引入额外的 HTML 属性和 input 类型,这些可以用来设置表单的校验规则。这些校验还可以使用特有的 API [13]来控制和自定义表单JavaScript。
让我们看下面的代码:
<form>
<label for="answer">What do you know, Jon Snow?</label>
<input id="answer" name="answer" required>
<button>Ask</button>
</form>
这是一个简单的表单,期望的功能 —— <input>
元素有required
的属性。因此,如果我们快速按下提交按钮,表单内容不会提交到服务器。相反我们会看到在输入框旁出现提示信息,提示 value
值没有满足给出的限制条件(不能为空)。
现在我们给输入框增加约束条件:
<form>
<label for="answer">What do you know, Jon Snow?</label>
<input id="answer" name="answer" required pattern="nothing|nix">
<button>Ask</button>
</form>
错误提示信息并没有给出我们期望的信息,不是吗?我们可以自定义它(例如为了解释所期望的用户输入值)或者仅仅对输入值进行转换。
<form>
<label for="answer">What do you know, Jon Snow?</label>
<input id="answer" name="answer" required pattern="nothing|nix">
<button>Ask</button>
</form>
const answer = document.querySelector( "[name=answer]" );
answer.addEventListener( "input", ( event ) => {
if ( answer.validity.patternMismatch ) {
answer.setCustomValidity("Oh, it's not a right answer!");
} else {
answer.setCustomValidity( "" );
}
});
以上代码只是检查 input 框的输入事件,其校验状态 patternMismatch
的变化。当输入的值与定义的 pattern
不匹配时,就会出现错误提示信息。如果我们设置了其他的限制条件[14],也会被我们所定义的事件处理函数所覆盖。
你对这个工具提示不太满意?是的,它在不同的浏览器中表现形式是不一样的。让我们给表单元素增加novalidate属性,并且自定义错误提示:
<form novalidate>
<label for="answer">What do you know, Jon Snow?</label>
<input id="answer" name="answer" required pattern="nothing|nix">
<div data-bind="message"></div>
<button>Ask</button>
</form>
const answer = document.querySelector( "[name=answer]" ),
answerError = document.querySelector( "[name=answer] + [data-bind=message]" );
answer.addEventListener( "input", ( event ) => {
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 => (
<Form>
{({ error, valid, pristine, submitting, form }) => (
<>
Form content
<button disabled={ ( pristine || submitting ) } type="submit">Submit</button>
</>
)}
</Form>
);
render( <MyForm ></MyForm>, document.getElementById( "app" ) );
作用域接收到的状态对象具有以下属性:
- error - 表单错误信息(通常指的是服务端的校验信息),可以通过
form.setError()
进行设置; - valid - 布尔值,表示所有的输入是否全部满足规定的约束;
- pristine - 布尔值,表示用户是否和表单进行过交互;
- submitting - 布尔值,表示是否正在提交表单(当用户按下提交按钮时该状态转化为 true,一旦用户定义的异步提交逻辑处理结束,该值变为 false)
- form - 用于访问表单组件 API 的表单实例;
在这里我们使用 pristine
和 submitting
属性将提交按钮切换为禁用状态。
为了在提交表单的同时校验表单输入信息,我们使用 InputGroup 包裹这些input表单。
<InputGroup validate={[ "email" ]}>
{({ error, valid }) => (
<div>
<label htmlFor="emailInput">Email address</label>
<input
type="email"
required
name="email"
id="emailInput" />
{ error && (<div className="invalid-feedback">{error}</div>) }
</div>
)}
</InputGroup>
使用 validate
属性我们可以指定该组内应该使用什么样的 input。[“email”] 意味着我们只有一个名称为“email”的input。
在这个作用区域内我们接收到的状态对象具有下面的属性:
- errors – 所有注册 input 的错误信息数组;
- error – 最后显示的错误信息;
- valid – 布尔值,表示是否所有的输入数据全部满足规定的约束条件;
- inputGroup – 访问表单组件 API 的实例;
渲染之后我们可以得到一个 email 类型的输入框。如果该值为空,或在提交时包含无效的电子邮件地址,则在输入框旁边显示相应的验证消息。
还记得我们在使用原生表单校验 API 自定义错误提示信息时的焦头烂额吗?使用 InputGroup 这一情况将变得好转:
<InputGroup
validate={[ "email" ]}
translate={{
email: {
valueMissing: "C'mon! We need some value",
typeMismatch: "Hey! We expect an email address here"
}
}}>
...
我们可以将每个 input 定义为 key-value 的哈希结构,其中 keys 表示有效性属性[15],values 表示自定义消息。
自定义消息很简单。那么自定义验证该如何实现呢?我们可以使用校验的 prop 来做到:
<InputGroup validate={{
"email": ( input ) => {
if ( !EMAIL_WHITELIST.includes( input.current.value ) ) {
input.setCustomValidity( "Only whitelisted email allowed" );
return false;
}
return true;
}
}}>
...
在这个例子中,我们提供的是包含输入框名字的哈希而不是数组,其中 key 是输入框的名字, value 是验证处理函数。处理函数校验输入值(可以异步完成)并返回一个布尔类型的有效状态。使用 input.setCustomValidity
,我们可以自定义验证消息。
输入框并不总是提交时才进行校验。为了达到实时验证,首先我们需要给输入事件定义事件处理函数:
const onInput = ( e, inputGroup ) => {
inputGroup.checkValidityAndUpdate();
};
事实上,我们可以在用户每次输入时重新验证。控制逻辑如下所示:
<input
type="email"
required
name="email"
onInput={( e ) => onInput( e, inputGroup, form ) }
id="emailInput" />
这样,每当输入值改变时都会进行验证。如果输入值是无效的,我们就会马上收到错误的提示信息。
你可以通过这里找到以上示例的源代码[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( <Provider store={store}>
<App ></App>
</Provider>, document.getElementById( "app" ) );
当我们运行该应用程序时,就能在 store 中找到所有与表单相关的状态。
点击这里[17]查看上述 demo 的源代码。
总结
React 没有内置的表单验证逻辑,但是我们可以使用第三方解决方案。这种方法可能是开发包、表单生成器,还可能是 HOC 或者是包含校验逻辑的任意表单容器组件。我个人倾向于使用容器组件,该组件依赖于 HTML 内置表单验证 API ,并在表单和表单域的范围中显示有效性状态。
扩展阅读
- https://tools.ietf.org/html/rfc1866
- https://reactjs.org/docs/forms.html
- https://react-bootstrap.github.io/components/forms/
- https://ant.design/components/form/
- https://github.com/andrewhathaway/Winterfell
- https://github.com/andrewhathaway/Winterfell/blob/master/examples/schema.js
- https://github.com/mozilla-services/react-jsonschema-form
- http://json-schema.org/
- https://github.com/formsy/formsy-react
- https://github.com/formsy/formsy-react/blob/master/API.md#validators
- https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Form_validation
- https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Form_validation
- https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation
- https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation
- https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
- https://github.com/dsheiko/react-html5-form/blob/master/demo/bootstrap/src/index.jsx
- https://github.com/dsheiko/react-html5-form/blob/master/demo/bootstrap-redux/src/index.jsx
- https://github.com/dsheiko/react-html5-form
- https://dsheiko.github.io/react-html5-form