构建多步骤表单

官方多步骤插件

1.0.0-beta.15开始,FormKit提供了一个官方的第一方插件,该插件创建了一个多步骤输入类型。

虽然理解如何自己构建多步骤输入仍然有价值,但如果你在寻找在项目中使用多步骤输入的最简单方法,请查看官方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。让我们将每个组放入一个"步骤"部分,我们可以有条件地显示和隐藏:

<!-- 为了简洁,这里只显示一个组 -->
<section v-show="step === 'contactInfo'">
  <FormKit type="group" name: "contactInfo">
    <FormKit type="email" label="*电子邮件地址" validation="required|email" />
  </FormKit>
</section>
...

接下来,让我们引入一些导航UI,这样我们就可以在每个步骤之间切换:

// 现在,手动设置步骤名称
const stepNames = ['contactInfo', 'organizationInfo', 'application']
<!-- 设置标签导航UI。点击时,改变步骤 -->
<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 的有效性。我们只需要捕获这些数据,以便在我们的 UI 中使用它。

关于 FormKit 的一个重要概念是,每个 <FormKit> 组件都有一个匹配的核心节点,它本身有一个反应性的 node.context 对象。这个 context 对象在 context.state.valid 中跟踪节点的有效性。如上所述,当一个 group 的所有后代都有效时,它就变得有效。有了这个概念,让我们构建一个对象,存储每个组的反应性有效性。

我们将利用 FormKit 的插件功能来完成这项工作。虽然 "插件" 这个词可能听起来很吓人,但在 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 composable 中,以便在其他地方重用。
  • 为我们的实用函数创建一个 utils.js 文件。
  • 将我们找到的第一个步骤设置为 activeStep
加载实时示例

显示错误

显示错误更为微妙。虽然用户可能并不知道,但实际上我们需要处理和向用户传达两种类型的错误:

  • 未通过_前端_验证规则的错误(messages的类型为validation
  • 后端错误(messages的类型为error

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
  })
  ...
}
阻塞验证消息 vs 错误

FormKit区分前端验证消息(messages的类型为validation)和错误(messages的类型为error)。

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

  • 如果存在后端错误,我们将始终显示其数量。
  • 只有当用户访问然后退出(模糊)一个组时,我们才会显示前端验证错误的数量——因为我们不希望在他们还在进行中时,用错误的用户界面对他们进行对抗。

添加一个组模糊事件

由于"模糊一个组"在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"> ... rest of form</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以及如何使用它来简化多步骤表单有了更多的了解!

想在你的项目中使用多步输入吗?试试官方插件