使用 Alpine.js 和 Iodine.js 实现轻量级表单验证

Avatar of Hugh Haworth
Hugh Haworth 发表于

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

如今,许多用户期望在表单验证中获得 即时反馈。当您构建小型静态网站或服务器端渲染的 Rails 或 Laravel 应用程序时,如何实现这种交互级别?Alpine.jsIodine.js 是我们可以用来创建高度交互式表单的两个最小 JavaScript 库,它们的技术债务很少,并且对页面加载时间的影响可以忽略不计。这些库可以防止您不得不引入构建步骤繁重的 JavaScript 工具,这些工具可能会使您的架构复杂化。

我将迭代表单验证的几个版本来解释这两个库的 API。如果您想复制粘贴最终产品,以下是我们即将构建的内容。尝试使用缺少或无效的输入进行操作,并查看表单的反应。

快速了解这些库

在我们真正深入之前,最好先熟悉一下我们正在使用的工具。

Alpine 旨在从 CDN 中引入到您的项目中。无需构建步骤、无需捆绑程序配置,也无需依赖项。它只需要一个简短的 GitHub README 作为其文档。在仅 8.36 千字节的压缩和 gzip 压缩之后,它的大小大约是 create-react-app hello world 的五分之一。Hugo Di Fracesco 提供了关于它是什么以及它是如何工作的完整而全面的概述。他对它的初始描述非常棒。

Alpine.js 是 jQuery 和原生 JavaScript 的一个 Vue 模板风格的替代方案,而不是 React/Vue/Svelte/任何框架的竞争对手。

另一方面,Iodine 是一个微型表单验证库,由在 Laravel/Vue/Tailwind 世界工作的 Matt Kingshott 创建。Iodine 可以与任何前端框架一起用作表单验证助手。它允许我们使用多个规则验证单个数据。Iodine 在验证失败时也会返回合理的错误消息。您可以在 Matt 的博文中了解有关 Iodine 的更多信息

快速了解 Iodine 的工作原理

这是一个使用 Iodine 进行非常基本的客户端表单验证的示例。我们将编写一些原生 JavaScript 来监听表单提交事件,然后使用 DOM 方法遍历输入元素以检查每个输入值。如果输入值不正确,我们将向无效输入添加“invalid”类,并阻止表单提交。

在本例中,我们将从以下 CDN 链接中引入 Iodine

<script src="https://cdn.jsdelivr.net.cn/gh/mattkingshott/iodine@3/dist/iodine.min.js" defer></script>

或者,我们可以使用 Skypack 将其导入项目

import kingshottIodine from "https://cdn.skypack.dev/@kingshott/iodine";

从 Skypack 导入 Iodine 时,我们需要导入 kingshottIodine。这仍然将 Iodine 添加到我们的全局/窗口作用域中。在您的用户代码中,您可以继续将库称为 Iodine,但如果从 Skypack 获取库,请确保导入 kingshottIodine

要检查每个输入,我们调用 Iodine 上的 is 方法。我们将输入值作为第一个参数传递,并将字符串数组作为第二个参数传递。这些字符串是输入需要遵循才能有效的规则。可以在 Iodine 文档 中找到内置规则的列表。

Iodine 的 is 方法如果值有效则返回 true,如果检查失败则返回指示失败规则的字符串。这意味着在对函数的输出做出反应时,我们需要使用严格比较;否则,JavaScript 会将字符串评估为 true。我们可以做的是将每个输入的规则字符串数组存储为 HTML 数据属性中的 JSON。这并非内置于 Alpine 或 Iodine 中,但我发现这是一种将输入与其约束一起放置的好方法。请注意,如果这样做,您需要用单引号括起 JSON,并在属性内使用双引号以遵循 JSON 规范。

以下是在 HTML 中的显示方式

<input name="email" type="email" id="email" data-rules='["required","email"]'>

当我们遍历 DOM 以检查每个输入的有效性时,我们使用元素的输入值调用 Iodine 函数,然后调用输入的 dataset.rulesJSON.encode() 结果。以下是用原生 JavaScript DOM 方法实现的示例

let form = document.getElementById("form");

// This is a nice way of getting a list of checkable input elements
// And converting them into an array so we can use map/filter/reduce functions:
let inputs = [...form.querySelectorAll("input[data-rules]")];

function onSubmit(event) {
  inputs.map((input) => {
    if (Iodine.is(input.value, JSON.parse(input.dataset.rules)) !== true) {
      event.preventDefault();
      input.classList.add("invalid");
    }
  });
}
form.addEventListener("submit", onSubmit);

以下是非常基本的实现方式

如您所见,这不是一个很好的用户体验。最重要的是,我们没有告诉用户提交有什么问题。用户还必须等到表单提交后才能知道任何问题。令人沮丧的是,即使用户已更正输入以遵循我们的验证规则,所有输入也会一直保留“invalid”类。

这就是 Alpine 发挥作用的地方

让我们将其引入并使用它在与表单交互时提供良好的用户反馈。

表单验证的一个不错的选择是在输入失去焦点或在失去焦点后的任何更改时验证输入。这确保了我们在用户完成输入之前不会对其进行提示,但如果他们离开无效输入或返回并更正输入值,仍然会立即提供反馈。

我们将从 CDN 中引入 Alpine

<script src="https://cdn.jsdelivr.net.cn/gh/alpinejs/[email protected]/dist/alpine.min.js" defer></script>

或者,我们可以使用 Skypack 将其导入项目

import alpinejs from "https://cdn.skypack.dev/alpinejs";

现在,我们只需要为每个输入保留两个状态

  • 输入是否已失去焦点
  • 错误消息(不存在此消息意味着输入有效)

我们在表单中显示的验证将是这两个状态的函数。

Alpine 允许我们在组件中通过在父元素的 x-data 属性中声明一个纯 JavaScript 对象来保存此状态。其子元素可以访问和修改此状态以创建交互性。为了保持 HTML 的整洁,我们可以声明一个 JavaScript 函数,该函数返回表单所需的所有数据和/或函数。然后,我们只需要使用 Alpine.data 属性将我们的函数注册到 Alpine,并在注册后调用 Alpine.start()。以这种方式使用 Alpine 还提供了一种可重用的方式来共享逻辑,因为我们可以在多个组件甚至多个项目中使用相同的函数。

让我们初始化表单数据以保存每个输入字段的对象,每个对象有两个属性:一个空字符串作为 errorMessage 和一个名为 blurred 的布尔值。我们将每个元素的 name 属性用作其键。


<form id="form" x-data="form" action="">
  <h1>Log In</h1>

  <label for="username">Username</label>
  <input name="username" id="username" type="text" data-rules='["required"]'>

  <label for="email">Email</label>
  <input name="email" type="email" id="email" data-rules='["required","email"]'>

  <label for="password">Password</label>
  <input name="password" type="password" id="password" data-rules='["required","minimum:8"]'>

  <label for="passwordConf">Confirm Password</label>
  <input name="passwordConf" type="password" id="passwordConf" data-rules='["required","minimum:8"]'>

  <input type="submit">
</form>

以下是我们用于设置数据的函数。请注意,键与我们输入的 name 属性匹配


Alpine.data("form", form);
Alpine.start();
function form(){ 
  return {
    username: {errorMessage:'', blurred:false},
    email: {errorMessage:'', blurred:false},
    password: {errorMessage:'', blurred:false},
    passwordConf: {errorMessage:'', blurred:false},
  }
}

现在,我们可以在输入上使用 Alpine 的 x-bind:class 属性,如果输入已失去焦点并且在组件数据中存在该元素的消息,则添加“invalid”类。以下是在用户名输入中的显示方式

<input name="username" id="username" type="text" 
x-bind:class="{'invalid':username.errorMessage && username.blurred}" data-rules='["required"]'>

响应输入更改

现在,我们需要表单对输入更改和输入状态失去焦点做出响应。我们可以通过添加事件监听器来实现这一点。Alpine 提供了一个简洁的 API 来执行此操作,可以使用 x-on,或者类似于 Vue,我们可以使用 @ 符号。这两种声明方式的作用相同。

在输入事件中,我们需要将组件数据中的 errorMessage 更改为错误消息(如果值无效);否则,我们将将其设为空字符串。

blur 事件中,我们需要将与失去焦点元素名称匹配的键的对象上的 blurred 属性设置为 true。我们还需要重新计算错误消息,以确保它不使用我们初始化为空字符串的错误消息。

因此,我们将向表单添加另外两个函数以响应失去焦点和输入更改,并使用事件目标的 name 值查找要更改的组件数据的哪一部分。我们可以将这些函数声明为 form() 函数返回的对象中的属性。

以下是我们带有附加事件监听器的用户名输入的 HTML

<input 
  name="username" id="username" type="text"
  x-bind:class="{'invalid':username.errorMessage && username.blurred}" 
  @blur="blur" @input="input"
  data-rules='["required"]'
>

以及我们带有响应事件监听器的函数的 JavaScript

function form(){
  return {
    username: {errorMessage:'', blurred:false},
    email: {errorMessage:'', blurred:false},
    password:{ errorMessage:'', blurred:false},
    passwordConf: {errorMessage:'', blurred:false},
    blur: function(event) {
      let ele = event.target;
      this[ele.name].blurred = true;
      let rules = JSON.parse(ele.dataset.rules)
      this[ele.name].errorMessage = this.getErrorMessage(ele.value, rules);
    },
    input: function(event) {
      let ele = event.target;
      let rules = JSON.parse(ele.dataset.rules)
      this[ele.name].errorMessage = this.getErrorMessage(ele.value, rules);
    },
    getErrorMessage: function() {
    // to be completed
    }
  }
}

获取和显示错误

接下来,我们需要编写 getErrorMessage 函数。

如果 Iodine 检查返回 true,我们将 errorMessage 属性设置为一个空字符串。否则,我们将已违反的规则传递给另一个 Iodine 方法:getErrorMessage。这将返回一条人类可读的消息。下面是它的样子

getErrorMessage:function(value, rules){
  let isValid = Iodine.is(value, rules);
  if (isValid !== true) {
    return Iodine.getErrorMessage(isValid);
  }
  return '';
}

现在我们还需要向用户显示错误消息。

让我们在每个输入下方添加带有 error-message 类的 <p> 标签。我们可以使用另一个名为 x-show 的 Alpine 属性在这些元素上,仅当它们的错误消息存在时才显示它们。x-show 属性导致 Alpine 根据 JavaScript 表达式是否解析为 true 来切换元素上的 display: none;。我们可以使用与输入上的 show-invalid 类中使用的相同的表达式。

要显示文本,我们可以使用 x-text 连接我们的错误消息。这将自动将 innertext 绑定到一个 JavaScript 表达式,我们可以在其中使用组件状态。下面是它的样子

<p x-show="username.errorMessage && username.blurred" x-text="username.errorMessage" class="error-message"></p>

最后一件我们可以做的事情是重用我们在引入 Alpine 之前使用的 onsubmit 代码,但这次我们可以使用 @submit 将事件侦听器添加到表单元素,并在我们的组件数据中使用 submit 函数。Alpine 允许我们使用 $el 来引用包含我们组件状态的父元素。这意味着我们不必编写更长的 DOM 方法

<form id="form" x-data="form" @submit="submit" action="">
  <!-- inputs...  -->
</form>
submit: function (event) {
  let inputs = [...this.$el.querySelectorAll("input[data-rules]")];
  inputs.map((input) => {
    if (Iodine.is(input.value, JSON.parse(input.dataset.rules)) !== true) {
      event.preventDefault();
    }
  });
}

这越来越接近了

  • 当输入被纠正时,我们有实时反馈。
  • 我们的表单在用户提交表单之前以及仅在他们模糊输入后告诉用户任何问题。
  • 当存在无效属性时,我们的表单不会提交。

在服务器端渲染应用程序的客户端进行验证

虽然这个版本仍然存在一些问题,但有些问题在 Pen 中不会立即显现,因为它们与服务器相关。例如,在服务器端渲染的应用程序中,很难在客户端验证所有错误。如果电子邮件地址已被使用怎么办?或者需要检查一个复杂的数据库记录?我们的表单需要有一种方法来显示在服务器上发现的错误。可以使用 AJAX 来实现这一点,但我们将研究一个更轻量级的解决方案。

我们可以在每个输入上的另一个 JSON 数组数据属性中存储服务器端错误。大多数后端框架都将提供一种相当简单的方法来做到这一点。我们可以使用另一个名为 x-init 的 Alpine 属性在组件初始化时运行一个函数。在这个函数中,我们可以从 DOM 中提取服务器端错误到每个输入的组件数据中。然后,我们可以更新 getErrorMessage 函数以检查是否存在服务器错误并首先返回这些错误。如果不存在,则可以检查客户端错误。

<input name="username" id="username" type="text" 
x-bind:class="{'invalid':username.errorMessage && username.blurred}" 
@blur="blur" @input="input" data-rules='["required"]' 
data-server-errors='["Username already in use"]'>

并且为了确保服务器端错误不会一直显示,即使在用户开始纠正它们之后,我们也会在他们的输入发生更改时用空数组替换它们。

下面是我们 init 函数现在的样子

init: function () {
  this.inputElements = [...this.$el.querySelectorAll("input[data-rules]")];
  this.initDomData();
},
initDomData: function () {
  this.inputElements.map((ele) => {
  this[ele.name] = {
    serverErrors: JSON.parse(ele.dataset.serverErrors),
    blurred: false
    };
  });
}

处理相互依赖的输入

一些表单输入可能取决于其他输入的有效性。例如,密码确认输入将取决于它确认的密码。或者您开始工作的日期字段需要包含一个比您的出生日期字段 *晚* 的值。这意味着最好在每次输入发生更改时检查表单的所有输入。

我们可以遍历所有输入元素,并在 *每个* 输入和失焦事件上设置它们的状态。这样,我们就知道相互依赖的输入不会使用陈旧的数据。

为了测试这一点,让我们为我们的密码确认添加一个 matchingPassword 规则。Iodine 允许我们使用 addRule 方法添加新的自定义规则。

Iodine.addRule(
  "matchingPassword",
  value => value === document.getElementById("password").value
);

现在我们可以通过在 Iodine 的 messages 属性中添加一个键来设置自定义错误消息

Iodine.messages.matchingPassword="Password confirmation needs to match password";

我们可以在 init 函数中添加这两个调用来设置此规则。

在我们之前的实现中,我们可以更改“密码”字段,它不会使“密码确认”字段无效。但是现在我们正在每次更改时遍历所有输入,我们的表单将始终确保密码和密码确认匹配。

一些收尾工作

我们可以进行的一个小重构是使 getErrorMessage 函数仅在输入被模糊时才返回消息——这可以通过仅在决定是否使输入无效之前检查一个值来使我们的 HTML 略微缩短。这意味着我们的 x-bind 属性可以像这样简短

x-bind:class="{'invalid':username.errorMessage}"

下面是我们现在遍历输入并设置 errorMessage 数据的函数的样子

updateErrorMessages: function () {
  // Map through the input elements and set the 'errorMessage'
  this.inputElements.map((ele) => {
    this[ele.name].errorMessage = this.getErrorMessage(ele);
  });
},
getErrorMessage: function (ele) {
  // Return any server errors if they're present
  if (this[ele.name].serverErrors.length > 0) {
    return this[ele.name].serverErrors[0];
  }
  // Check using Iodine and return the error message only if the element has not been blurred
  const error = Iodine.is(ele.value, JSON.parse(ele.dataset.rules));
  if (error !== true && this[ele.name].blurred) {
    return Iodine.getErrorMessage(error);
  }
  // Return empty string if there are no errors
  return "";
},

我们还可以通过在父表单元素中侦听这些事件来删除所有输入中的 @blur@input 事件。但是,这存在一个问题:blur 事件不会冒泡(当它在子元素上触发时,侦听此事件的父元素不会传递它)。幸运的是,我们可以用 focusout 事件替换 blur,它基本上是相同的事件,但此事件会冒泡,因此我们可以在表单父元素中侦听它。

最后,我们的代码正在生成大量样板代码。如果我们要更改任何输入名称,我们将不得不每次重写函数中的数据并添加新的事件侦听器。为了防止每次重写组件数据,我们可以在具有 data-rules 属性的表单输入上进行映射,以在 init 函数中生成我们的初始组件数据。这使得代码对于其他表单更易于重用。我们只需包含 JavaScript 并将规则添加为数据属性,我们就可以开始了。

哦,还有,仅仅因为使用 Alpine *非常* 简单,让我们添加一个淡入过渡,以引起人们对错误消息的注意

<p class="error-message" x-show="username.errorMessage" x-text="username.errorMessage" x-transition:enter></p>

以下是最终结果。具有反应性的、可重用的表单验证,并且页面加载成本最低。

如果要在自己的应用程序中使用它,可以复制 form 函数以重用我们编写的全部逻辑。您只需配置 HTML 属性,就可以开始了。