构建多步骤表单

官方多步骤插件

1.0.0-beta.15 版本开始,FormKit 提供了一个官方的第一方插件,用于创建 multi-step 输入类型。

虽然了解如何自己构建多步骤输入仍然有价值,但如果您想要在项目中使用最简单的多步骤输入方式,请查看 官方 FormKit 多步骤插件 — 它是免费且开源的!

在网页上,很少有什么交互比面对一个庞大而令人生畏的表单更令人不悦。多步骤表单 — 有时被称为 "向导" — 可以通过将一个大型表单分解为更小的可接近的步骤来减轻这种痛苦,但构建它们可能也很复杂。

在本指南中,我们将使用 FormKit 构建一个多步骤表单,并了解如何以最少的代码提供提升的用户体验。让我们开始吧!

组合 API

本指南假设您熟悉 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>

通过这些更新,我们的表单现在能够在用户正确填写给定步骤中的所有字段时通知用户!

我们还将进行一些其他改进:

  • 将“步骤逻辑”提取到一个Vue可组合中,以便在其他地方重用。
  • 为我们的实用函数创建一个utils.js文件。
  • 将我们找到的第一个步骤设置为activeStep
加载实时示例

显示错误

显示错误更加复杂。尽管用户可能不知道,但我们实际上需要处理并向用户传达两种类型的错误:

  • 来自失败的_前端_验证规则的错误(类型为validationmessages
  • 后端错误(类型为errormessages

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区分前端验证消息(类型为validationmessages)和错误(类型为errormessages)。

让我们更新示例以显示这两种类型的错误,并满足以下要求:

  • 如果存在后端错误,我们将始终显示后端错误的计数。
  • 仅当用户访问并退出(失去焦点)一个组时,我们才显示前端验证错误的计数,因为如果用户仍在进行中,我们不希望向他们展示错误UI。

添加组失去焦点事件

由于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

我们将为两种类型的错误使用相同的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状态之前是禁用的。
  • 在表单中添加了一些额外的文本,以更好地模拟真实世界的UI。

这就是它 - 一个完全功能的多步骤表单:

加载实时示例
想要看到使用FormKit Schema构建的示例吗?查看 Playground

改进的方法

当然,任何事物都可以改进,这个表单也不例外。以下是一些想法:

  • 将表单状态保存到window.localStorage中,以便用户的表单状态即使意外离开也能保持。
  • 预填充任何已知的表单值,以便用户不必填写已知数据。
  • 添加一个“尚未提交”状态指示器,提醒用户仍然需要提交。

在本指南中,我们涵盖了很多主题,希望您对FormKit有更多了解,并学会如何使用它来简化多步骤表单!

想要在您的项目中使用多步骤输入吗?尝试官方插件