处理冗长、复杂的表单的一种方法是将其分解成多个步骤。例如,回答一组问题,然后继续下一组问题,然后再继续下一组,依此类推。我们通常将这些称为**多步骤表单**(原因显而易见),但也有人称之为“向导”表单。
多步骤表单可能是一个好主意!通过一次只显示几个输入,表单可能会显得更容易理解,并防止用户因大量的表单字段而感到不知所措。虽然我没有查过,但我敢说没有人喜欢填写一个巨大的表单——这就是多步骤表单派上用场的地方。
问题在于,多步骤表单——虽然减少了前端的感知复杂性——但在开发方面可能会让人感到复杂和难以驾驭。但是,我要告诉你,使用 React 作为基础,不仅可以实现它,而且相对简单。所以,这就是我们今天要一起构建的!
这是最终产品
让我们开始构建吧!
创建多步骤表单的最简单方法是创建一个容器表单元素,该元素包含所有步骤作为其内部组件。以下是一个可视化图表,显示了该容器()、其内部的组件(
、
、
)以及它们之间传递状态和属性的方式。

充当容器,而其内部的三个子组件充当表单的每个步骤。虽然它看起来比常规表单更复杂,但多步骤表单仍然使用与 React 表单相同的原理
- 状态用于存储数据和用户输入。
- 组件用于编写方法和界面。
- 属性用于将数据和函数传递到元素中。
我们不会只有一个表单组件,而是会拥有一个父组件和三个子组件。在上图中,将通过属性将数据和函数发送到子组件,而子组件将触发
handleChange()
函数来设置状态中的值。它们形成一个和谐的大家庭!我们还需要一个函数来将表单从一个步骤移动到另一个步骤,稍后我们会讲到这一点。
步骤子(get it?)组件将从父组件接收
value
和onChange
属性。
组件将渲染一个电子邮件地址输入框
将渲染一个用户名输入框
将渲染一个密码输入框和一个提交按钮
将同时向子组件提供数据和函数,子组件将使用其
props
将用户输入传递回父组件。
创建步骤(子)组件
首先,我们将创建表单的子组件。在本例中,我们保持组件非常精简,每个步骤只使用一个输入,但每个步骤实际上可以像我们想要的那样复杂。由于子组件彼此之间看起来几乎相同,因此我在这里只展示其中一个。但是请务必查看演示以获取完整代码。
class Step1 extends React.Component {render() {
if (this.props.currentStep !== 1) { // Prop: The current step
return null
}
// The markup for the Step 1 UI
return(
<div className="form-group">
<label htmlFor="email">Email address</label>
<input
className="form-control"
id="email"
name="email"
type="text"
placeholder="Enter email"
value={this.props.email} // Prop: The email input data
onChange={this.props.handleChange} // Prop: Puts data into state
/>
</div>
)}
}
现在,我们可以将此子组件放入表单的render()
函数中并传入必要的属性。就像在React 的表单文档中一样,我们仍然可以使用handleChange()
将用户提交的数据与setState()
一起放入状态中。handleSubmit()
函数将在表单提交时运行。
接下来是父组件
让我们创建父组件——我们现在都知道,我们称之为——并初始化其状态和方法。
我们使用一个currentStep
状态,它将初始化为默认值 1,表示表单的第一步()。随着表单的进行,我们将更新状态以指示当前步骤。
class MasterForm extends Component {
constructor(props) {
super(props)
// Set the initial input values
this.state = {
currentStep: 1, // Default is Step 1
email: '',
username: '',
password: '',
}
// Bind the submission to handleChange()
this.handleChange = this.handleChange.bind(this)
}
// Use the submitted data to set the state
handleChange(event) {
const {name, value} = event.target
this.setState({
[name]: value
})
}
// Trigger an alert on form submission
handleSubmit = (event) => {
event.preventDefault()
const { email, username, password } = this.state
alert(`Your registration detail: \n
Email: ${email} \n
Username: ${username} \n
Password: ${password}`)
}
// Render UI will go here...
}
好的,这是我们正在寻找的基本功能。接下来,我们想要为实际的表单创建外壳 UI 并调用其中的子组件,包括将通过handleChange()
从传递的必需状态属性。
render() {
return (
<React.Fragment>
<h1>A Wizard Form!</h1>
Step {this.state.currentStep}
<form onSubmit={this.handleSubmit}>
// Render the form steps and pass in the required props
<Step1
currentStep={this.state.currentStep}
handleChange={this.handleChange}
email={this.state.email}
/>
<Step2
currentStep={this.state.currentStep}
handleChange={this.handleChange}
username={this.state.username}
/>
<Step3
currentStep={this.state.currentStep}
handleChange={this.handleChange}
password={this.state.password}
/>
</form>
</React.Fragment>
)
}
一步一步来
到目前为止,我们允许用户填写表单字段,但我们没有提供任何实际方法来继续下一步或返回上一步。这就需要 next 和 previous 函数来检查当前步骤是否有前一步或下一步;如果有,则相应地向上或向下推动currentStep
属性。
class MasterForm extends Component {
constructor(props) {
super(props)
// Bind new functions for next and previous
this._next = this._next.bind(this)
this._prev = this._prev.bind(this)
}
// Test current step with ternary
// _next and _previous functions will be called on button click
_next() {
let currentStep = this.state.currentStep
// If the current step is 1 or 2, then add one on "next" button click
currentStep = currentStep >= 2? 3: currentStep + 1
this.setState({
currentStep: currentStep
})
}
_prev() {
let currentStep = this.state.currentStep
// If the current step is 2 or 3, then subtract one on "previous" button click
currentStep = currentStep <= 1? 1: currentStep - 1
this.setState({
currentStep: currentStep
})
}
}
我们将使用一个get
函数来检查当前步骤是 1 还是 3。这是因为我们有一个三步表单。当然,随着向表单中添加更多步骤,我们可以更改这些检查。我们还希望仅在实际上有下一步和上一步可以导航到时显示下一步和上一步按钮。
// The "next" and "previous" button functions
get previousButton(){
let currentStep = this.state.currentStep;
// If the current step is not 1, then render the "previous" button
if(currentStep !==1){
return (
<button
className="btn btn-secondary"
type="button"
onClick={this._prev}>
Previous
</button>
)
}
// ...else return nothing
return null;
}
get nextButton(){
let currentStep = this.state.currentStep;
// If the current step is not 3, then render the "next" button
if(currentStep <3){
return (
<button
className="btn btn-primary float-right"
type="button"
onClick={this._next}>
Next
</button>
)
}
// ...else render nothing
return null;
}
剩下的就是渲染这些按钮了
// Render "next" and "previous" buttons
render(){
return(
<form onSubmit={this.handleSubmit}>
{/*
... other codes
*/}
{this.previousButton}
{this.nextButton}
</form>
)
}
恭喜,您现在是表单向导了!🧙
这是关于多步骤表单的多步骤教程中的最后一步。哇,多么元!虽然我们没有深入探讨样式,但希望这能为您提供关于如何使复杂表单不那么……复杂的有力概述!
这是最终演示的再次展示,以便您可以在其完整且辉煌的上下文中查看所有代码
考虑到 React 利用了状态、属性更改、可重用组件等,因此它非常适合这种事情。我知道 React 对某些人来说似乎门槛很高,但我写了一本书,让它变得更容易上手。我希望您能看看!
最近,我的团队一直在使用 React 创建几个类似的工作流程。
我们发现将多步骤表单与 React Context 配合使用非常有用,这样我们就不必担心将公共状态数据和方法作为属性传递到每个子组件中。
https://reactjs.ac.cn/docs/context.html
哇,感谢您的信息,Russell!我一直在听说 Context API,但还没有时间去探索它。现在一定会去看看。
很棒的文章,但向导表单不仅仅是向前迈进……我想知道您将如何解决错误处理,因为这是真正的挑战。您是在每个步骤还是在最后一步提交表单?如果在最后一步提交表单,并且服务器端验证响应错误的电子邮件地址(在第一步),如何将用户引导到该特定步骤进行更正?
谢谢!:D
实际上,这是下一篇文章的一个好主意!但是对于您的问题的鸟瞰图答案是,客户端的验证应该在之前和下一个按钮按下时运行。更大的问题可能是验证电子邮件。电子邮件是否已注册?我们如何检查服务器以查看它是否是重复的电子邮件?我将尝试进行 React 向导表单的深入探讨,在那里我将使用示例应用程序展示所有内容。谢谢!
虽然使用 React 来做这种类型的表单很有趣。我认为它不必要地过于复杂了。
这是一个仅使用 HTML/CSS 的简单解决方案。您需要 JS 才能在输入无效时禁用下一个按钮
以下是一些我一直在尝试的代码,以表明您的输入无效。如果存在无效输入,则提交按钮将不会提交。但是,当您逐步浏览每个项目并隐藏旧项目时,这会失败。因此,您绝对希望每个步骤都有有效数据。我相信用户会提前知道他们输入的内容是不好的。
这里有一些概念没有提到。我知道你不可能面面俱到,但我很好奇你会如何处理这些情况。
每个步骤都在它自己的路由上
整体表单的验证,而不仅仅是一个步骤。返回并更改先前步骤中的值可能会影响后续步骤中字段的验证
启用/禁用步骤
还有其他一些内容我差点提到,但后来意识到这有点超出了本文的范围。
感谢您的阅读。这与我当前的任务奇怪地相关。
需要更少的类,更多的上下文和钩子!是的,React 又一次改变了!
感谢你的文章,Nathan!
只有一个避免逻辑泄漏的小技巧:显示/隐藏步骤的责任由父组件(在本例中为 )管理,那么它应该负责决定显示哪个子组件。一般来说,在父组件中决定是否渲染一个组件比在子组件中决定更可预测。
将此逻辑放在父组件中将允许您在需要时无缝地重新排序步骤。它们不会知道自己是哪个“步骤”。
我对这个“get”函数非常好奇。我以前从未见过这个关键字,现在我想知道它是如何工作的。谷歌搜索也找不到任何信息。我也同意Julio关于渲染逻辑的观点。
虽然此表单对于简单的三步表单非常有用,但在每个子组件中都有很多类似的代码,如果要创建六步或十步表单,逻辑会更多,是否有一种方法可以从这些组件中提取并使它们更通用以便可以重复使用,想知道你的想法???