从 1.0.0-beta.15
版本开始,FormKit 提供了一个官方的第一方插件,用于创建 multi-step
输入类型。
虽然了解如何自己构建多步骤输入仍然有价值,但如果您想要在项目中使用最简单的多步骤输入方式,请查看 官方 FormKit 多步骤插件 — 它是免费且开源的!
在网页上,很少有什么交互比面对一个庞大而令人生畏的表单更令人不悦。多步骤表单 — 有时被称为 "向导" — 可以通过将一个大型表单分解为更小的可接近的步骤来减轻这种痛苦,但构建它们可能也很复杂。
在本指南中,我们将使用 FormKit 构建一个多步骤表单,并了解如何以最少的代码提供提升的用户体验。让我们开始吧!
本指南假设您熟悉 Vue 组合 API。
让我们首先明确我们多部分表单的需求:
首先,让我们创建一个基本的 无步骤 表单,以便我们有内容可以使用。我们的示例将是一个假设的申请接收补助金的应用程序,因此我们将把表单组织成 3 个部分 — "联系信息"、"组织信息" 和 "申请"。稍后,这些将成为完整的表单步骤。
我们将为每个输入包含一系列验证规则,并暂时将每个部分限制为 1 个问题,直到我们完成完整的结构。最后,为了本指南的目的,我们将在每个示例的底部输出收集到的表单数据:
现在我们有了一个定义好的结构,让我们将表单分成不同的部分。
首先,让我们使用 group (<FormKit type="group" />
) 将每个输入部分包装起来,以便我们可以独立验证每个组。FormKit 组非常强大,因为它们知道其所有后代的验证状态,而不会影响表单的标记。
当所有子元素(及其子元素)都有效时,组本身变为有效:
<!-- 为了简洁起见,这里只显示一个组 -->
<FormKit type="group" name: "contactInfo">
<FormKit type="email" label="*电子邮件地址" validation="required|email" />
</FormKit>
...
在我们的情况下,我们还需要一些包装 HTML。让我们将每个组放入一个 "step" 部分中,我们可以根据需要条件性地显示和隐藏它们:
<!-- 为了简洁起见,这里只显示一个组 -->
<section v-show="step === 'contactInfo'">
<FormKit type="group" name: "contactInfo">
<FormKit type="email" label="*电子邮件地址" validation="required|email" />
</FormKit>
</section>
...
接下来,让我们引入一些导航界面,以便我们可以在每个步骤之间切换:
// 目前,手动设置步骤名称
const stepNames = ['contactInfo', 'organizationInfo', 'application']
<!-- 设置选项卡导航界面。点击时,更改步骤 -->
<ul class="steps">
<li
v-for="stepName in stepNames"
class="step"
@click="step = stepName"
:data-step-active="step === stepName"
>
{{ camel2title(panel) }}
</li>
</ul>
下面是将它们放在一起的样子:
多步骤表单的 CSS 样式(例如此示例中的选项卡)不包含在默认的 Genesis 主题中。此示例的样式是自定义编写的,您需要提供自己的样式。
它开始看起来像一个真正的多步骤表单!但还有更多工作要做,因为我们有几个问题:
让我们解决第一个问题。
FormKit 已经默认跟踪 group
的有效性。我们只需要捕获这些数据,以便在我们的界面中使用。
关于 FormKit 的一个重要概念是,每个 <FormKit>
组件都有一个匹配的 core node,它本身有一个响应式的 node.context
对象。这个 context
对象通过 context.state.valid
跟踪节点的有效性。如上所述,当所有后代都有效时,group
就变为有效。在这种情况下,让我们构建一个存储每个组的响应式有效性的对象。
我们将利用 FormKit 的 plugin 功能来完成这个任务。虽然 "plugin" 这个术语听起来可能有些吓人,但在 FormKit 中,插件只是在创建节点时调用的设置函数。插件会被所有后代继承(例如组中的子元素)。
这是我们的自定义插件,称为 stepPlugin
:
// 我们的插件和模板代码将使用 'steps'
const steps = reactive({})
const stepPlugin = (node) => {
// 仅对 <FormKit type="group" /> 运行
if (node.props.type == 'group') {
// 构建我们的 steps 对象
steps[node.name] = steps[node.name] || {}
// 添加当前组的响应式有效性
node.on('created', () => {
steps[node.name].valid = toRef(node.context.state, 'valid')
})
// 停止插件继承到后代节点。
// 我们只关心代表步骤的顶级组。
return false
}
}
上面我们的插件生成的 steps
响应式对象如下所示:
{
contactInfo: { valid: false },
organizationInfo: { valid: false }
application: { valid: false }
}
为了使用我们的插件,我们将它添加到根表单 <FormKit type="form" />
中。这意味着我们表单中的每个顶级组都将继承该插件:
<FormKit type="form" :plugins="[stepPlugin]"> ... 表单的其余部分 </FormKit>
现在,我们的模板通过插件实时访问每个组的有效状态,让我们编写UI来在步骤导航栏中显示这些数据。
由于我们的插件动态存储了所有组的名称在steps
对象中,我们不再需要手动定义步骤。让我们为每个步骤添加一个data-step-valid="true"
属性,以便我们可以使用CSS来定位:
<ul class="steps">
<li
v-for="(step, stepName) in steps"
class="step"
@click="activeStep = stepName"
:data-step-valid="step.valid"
:data-step-active="activeStep === stepName"
>
{{ camel2title(stepName) }}
</li>
</ul>
通过这些更新,我们的表单现在能够在用户正确填写给定步骤中的所有字段时通知用户!
我们还将进行一些其他改进:
activeStep
。显示错误更加复杂。尽管用户可能不知道,但我们实际上需要处理并向用户传达两种类型的错误:
validation
的messages
)error
的messages
)FormKit使用其消息存储来跟踪这两种类型的错误/消息。
有了我们已经就位的插件,添加这两种类型的跟踪相对简单:
const stepPlugin = (node) => {
...
// 存储或更新阻止验证消息的计数。
// 每次计数更改时,FormKit都会发出“count:blocking”事件(带有计数)。
node.on('count:blocking', ({ payload: count }) => {
steps[node.name].blockingCount = count
})
// 存储或更新后端错误消息的计数。
node.on('count:errors', ({ payload: count }) => {
steps[node.name].errorCount = count
})
...
}
FormKit区分前端验证消息(类型为validation
的messages
)和错误(类型为error
的messages
)。
让我们更新示例以显示这两种类型的错误,并满足以下要求:
由于HTML中不存在“失去焦点组”的概念,我们将在插件中引入一个名为visitedSteps
的数组来实现它。以下是相关代码:
import { watch } from 'vue'
import { getNode, createMessage } from '@formkit/core'
const stepPlugin = (node) => {
...
const activeStep = ref('')
const visitedSteps = ref([]) // 跟踪已访问的步骤
// 监听activeStep并存储已访问的步骤
watch(activeStep, (newStep, oldStep) => {
if (oldStep && !visitedSteps.value.includes(oldStep)) {
visitedSteps.value.push(oldStep)
}
// 如果组已访问,则触发字段上的显示验证
visitedSteps.value.forEach((step) => {
const node = getNode(step)
// node.walk()方法遍历当前节点的所有后代并执行提供的函数。
node.walk((n) => {
n.store.set(
createMessage({
key: 'submitted',
value: true,
visible: false
})
)
})
})
})
...
}
您可能想知道为什么我们要遍历给定步骤的所有后代(node.walk()
)并创建键为submitted
,值为true
的消息?当用户尝试提交表单时,这是FormKit告知自身所有输入处于submitted
状态的方式。在此状态下,FormKit会强制显示任何阻止验证消息。我们在“组失去焦点”事件中手动触发相同的操作。
我们将为两种类型的错误使用相同的UI,因为最终用户实际上并不关心区别。以下是我们更新后的步骤HTML,它输出一个带有错误总数errorCount
+ blockingCount
的红色气泡:
<li v-for="(step, stepName) in steps" class="step" ...>
<span
v-if="checkStepValidity(stepName)"
class="step--errors"
v-text="step.errorCount + step.blockingCount"
/>
{{ camel2title(stepName) }}
</li>
我们离终点几乎就要到了!这是我们当前的表单,现在可以告诉用户他们正确或错误地填写了每个步骤:
最后一步是提交表单并处理从后端服务器接收到的任何错误。为了本指南的目的,我们将模拟后端。
我们通过将<FormKit type="form">
添加一个@submit
处理程序来提交表单:
<FormKit type="form" @submit="submitApp"> ... 其余的表单内容</FormKit>
这是我们的提交处理程序:
const submitApp = async (formData, node) => {
try {
const res = await axios.post(formData)
node.clearErrors()
alert('您的申请已成功提交!')
} catch (err) {
node.setErrors(err.formErrors, err.fieldErrors)
}
}
请注意,FormKit将我们的提交处理程序传递了两个有用的参数:表单数据以单个请求准备的对象形式(我们称之为formData
),以及表单的底层核心node
,我们可以使用node.clearErrors()
和node.setErrors()
助手函数来清除错误或设置任何返回的错误。
setErrors()
接受两个参数:表单级别的错误和字段级别的错误。我们的模拟后端返回了err
响应,我们使用它来设置任何错误。
那么,如果用户在提交时处于第3步(申请)并且隐藏步骤上存在字段级别的错误会发生什么?幸运的是,只要节点存在于DOM中,FormKit就能够适当地放置这些错误。这就是为什么我们在步骤中使用了v-show
而不是v-if
的原因 - DOM节点需要存在才能在相应的FormKit节点上设置错误。
大功告成!🎉我们完成了!除了我们的提交处理程序之外,我们还为这个最终表单添加了一些UI和UX装饰,使其更加真实:
utils.js
中添加了一个模拟后端,返回错误。valid
状态之前是禁用的。这就是它 - 一个完全功能的多步骤表单:
当然,任何事物都可以改进,这个表单也不例外。以下是一些想法:
window.localStorage
中,以便用户的表单状态即使意外离开也能保持。在本指南中,我们涵盖了很多主题,希望您对FormKit有更多了解,并学会如何使用它来简化多步骤表单!