HTML 表单约束验证 API:自定义错误信息与 :valid / :invalid 伪类交互
大家好,今天我们来深入探讨 HTML 表单的约束验证 API,重点关注如何自定义错误信息以及如何利用 :valid 和 :invalid 伪类来实现更丰富的用户体验。
HTML5 引入的约束验证 API 极大地简化了客户端表单验证。不再需要依赖大量的 JavaScript 代码来检查用户输入是否符合预期,浏览器自身就能执行很多常见的验证规则。同时,我们可以通过 JavaScript 进行更精细的控制,包括自定义错误信息和动态调整验证逻辑。
约束验证 API 的基础
在深入自定义之前,我们先回顾一下约束验证 API 的核心概念。
- 
约束属性 (Constraint Attributes): 这些 HTML 属性定义了表单控件的验证规则。例如:
required、minlength、maxlength、type="email"、pattern等。 - 
ValidityState 对象: 每个表单控件都有一个
validity属性,该属性返回一个ValidityState对象。这个对象包含一系列布尔属性,指示控件的有效性状态。例如:validity.valueMissing(是否缺少必需的值)、validity.typeMismatch(类型是否不匹配) 等。 - 
checkValidity() 方法: 调用表单控件的
checkValidity()方法会触发验证过程。如果控件有效,则返回true;否则返回false。如果控件无效,浏览器会显示默认的错误提示(除非被阻止)。 - 
reportValidity() 方法: 与
checkValidity()类似,但reportValidity()始终会显示错误提示(如果控件无效)。 - 
setCustomValidity() 方法: 这是自定义错误信息的关键。可以使用
setCustomValidity()方法设置自定义的错误消息。如果设置了非空字符串,控件将被视为无效;如果设置为空字符串,控件将被视为有效(只要其他约束条件满足)。 
自定义错误信息
浏览器默认的错误提示通常比较通用,不够友好。为了提供更好的用户体验,我们需要自定义错误信息。
使用 setCustomValidity()
setCustomValidity() 方法允许我们覆盖浏览器的默认错误提示。
<input type="text" id="username" required minlength="5">
<span id="usernameError" style="color: red;"></span>
<script>
  const usernameInput = document.getElementById('username');
  const usernameError = document.getElementById('usernameError');
  usernameInput.addEventListener('input', () => {
    if (usernameInput.validity.valueMissing) {
      usernameInput.setCustomValidity('用户名不能为空');
      usernameError.textContent = '用户名不能为空';
    } else if (usernameInput.validity.tooShort) {
      usernameInput.setCustomValidity(`用户名至少需要 ${usernameInput.minLength} 个字符`);
      usernameError.textContent = `用户名至少需要 ${usernameInput.minLength} 个字符`;
    } else {
      usernameInput.setCustomValidity(''); // 清除自定义错误
      usernameError.textContent = '';
    }
  });
  // 为了在提交时显示错误,需要监听 form 的 submit 事件
  document.querySelector('form').addEventListener('submit', (event) => {
    if (!usernameInput.checkValidity()) {
      event.preventDefault(); // 阻止表单提交
      usernameInput.reportValidity(); // 显示错误提示
    }
  });
</script>
在这个例子中,我们监听 input 事件,当用户输入时,检查 validity 对象的属性。如果用户名为空或者长度太短,我们使用 setCustomValidity() 设置自定义错误信息,并将其显示在 span 元素中。  关键的一点是,当输入有效时,需要调用 setCustomValidity('') 清除自定义错误,否则控件将一直处于无效状态。  最后,监听 form 的 submit 事件,确保在提交时如果控件无效阻止提交,并显示错误。
一个更复杂的例子:密码强度验证
我们可以结合正则表达式和 setCustomValidity() 来实现更复杂的验证逻辑,例如密码强度验证。
<input type="password" id="password" required pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[@$!%*?&])[A-Za-zd@$!%*?&]{8,}$">
<span id="passwordError" style="color: red;"></span>
<script>
  const passwordInput = document.getElementById('password');
  const passwordError = document.getElementById('passwordError');
  passwordInput.addEventListener('input', () => {
    if (passwordInput.validity.valueMissing) {
      passwordInput.setCustomValidity('密码不能为空');
      passwordError.textContent = '密码不能为空';
    } else if (passwordInput.validity.patternMismatch) {
      passwordInput.setCustomValidity('密码必须包含至少 8 个字符,包括一个大写字母、一个小写字母、一个数字和一个特殊字符');
      passwordError.textContent = '密码必须包含至少 8 个字符,包括一个大写字母、一个小写字母、一个数字和一个特殊字符';
    } else {
      passwordInput.setCustomValidity('');
      passwordError.textContent = '';
    }
  });
  document.querySelector('form').addEventListener('submit', (event) => {
    if (!passwordInput.checkValidity()) {
      event.preventDefault();
      passwordInput.reportValidity();
    }
  });
</script>
在这个例子中,我们使用 pattern 属性定义了一个复杂的正则表达式,要求密码包含至少 8 个字符,包括一个大写字母、一个小写字母、一个数字和一个特殊字符。  如果密码不符合这个规则,validity.patternMismatch 将为 true,我们会设置相应的自定义错误信息。
动态错误信息
错误信息可以根据当前的状态动态变化。例如,可以根据剩余可输入字符的数量来更新错误信息。
<textarea id="comment" maxlength="200"></textarea>
<span id="commentError" style="color: red;"></span>
<span id="commentRemaining">200</span> 字符剩余
<script>
  const commentInput = document.getElementById('comment');
  const commentError = document.getElementById('commentError');
  const commentRemaining = document.getElementById('commentRemaining');
  const maxLength = parseInt(commentInput.getAttribute('maxlength'));
  commentInput.addEventListener('input', () => {
    const remaining = maxLength - commentInput.value.length;
    commentRemaining.textContent = remaining;
    if (remaining < 0) {
      commentInput.setCustomValidity(`超出 ${-remaining} 字符`);
      commentError.textContent = `超出 ${-remaining} 字符`;
    } else {
      commentInput.setCustomValidity('');
      commentError.textContent = '';
    }
  });
  document.querySelector('form').addEventListener('submit', (event) => {
    if (!commentInput.checkValidity()) {
      event.preventDefault();
      commentInput.reportValidity();
    }
  });
</script>
在这个例子中,我们监听 input 事件,计算剩余可输入字符的数量,并将其显示在 span 元素中。如果用户输入的字符超过了 maxlength 属性的限制,我们会设置相应的自定义错误信息。
使用 :valid 和 :invalid 伪类
:valid 和 :invalid 是 CSS 伪类,它们分别用于选择有效的和无效的表单控件。我们可以利用这两个伪类来提供视觉反馈,指示控件的有效性状态。
简单的样式应用
<style>
  input:invalid {
    border: 2px solid red;
  }
  input:valid {
    border: 2px solid green;
  }
</style>
<input type="email" required>
在这个例子中,当 email 输入框无效时,它的边框会变成红色;当有效时,它的边框会变成绿色。
与 JavaScript 结合
我们可以结合 JavaScript 和 :valid/:invalid 伪类来实现更复杂的交互。 例如,当输入框获得焦点时才显示错误提示。
<style>
  input:invalid:focus + span {
    display: inline; /* 或者其他适合的显示方式 */
  }
  span {
    display: none; /* 默认隐藏错误提示 */
    color: red;
  }
</style>
<input type="text" id="username2" required minlength="5">
<span>用户名不符合要求</span>
<script>
  const usernameInput2 = document.getElementById('username2');
  usernameInput2.addEventListener('input', () => {
    if (usernameInput2.validity.valueMissing) {
      usernameInput2.setCustomValidity('用户名不能为空');
    } else if (usernameInput2.validity.tooShort) {
      usernameInput2.setCustomValidity(`用户名至少需要 ${usernameInput2.minLength} 个字符`);
    } else {
      usernameInput2.setCustomValidity('');
    }
  });
  document.querySelector('form').addEventListener('submit', (event) => {
    if (!usernameInput2.checkValidity()) {
      event.preventDefault();
      usernameInput2.reportValidity();
    }
  });
</script>
在这个例子中,我们使用 CSS 选择器 input:invalid:focus + span 来选择当输入框无效且获得焦点时,紧邻其后的 span 元素。  默认情况下,span 元素是隐藏的,只有当输入框无效且获得焦点时才会显示。  input 事件监听器负责更新 setCustomValidity,使得 :invalid 伪类能够正确工作。
动画效果
可以结合 CSS 过渡和动画,为 :valid 和 :invalid 伪类添加动画效果,使用户体验更加流畅。
<style>
  input {
    transition: border-color 0.3s ease;
  }
  input:invalid {
    border-color: red;
  }
  input:valid {
    border-color: green;
  }
  .error-message {
    opacity: 0;
    transition: opacity 0.3s ease;
    color: red;
  }
  input:invalid:focus + .error-message {
    opacity: 1;
  }
</style>
<input type="email" required>
<span class="error-message">请输入有效的邮箱地址</span>
在这个例子中,我们为 input 元素的 border-color 属性添加了过渡效果。 当输入框的有效性状态发生变化时,边框颜色会平滑地过渡到相应的颜色。  同样,错误信息在获得焦点且输入无效时,会以动画的形式显示。
使用 aria 属性增强可访问性
为了提高表单的可访问性,可以使用 ARIA (Accessible Rich Internet Applications) 属性。
aria-invalid 属性
可以使用 aria-invalid 属性来指示表单控件是否包含无效的值。
<input type="email" id="email3" aria-invalid="false" required>
<span id="email3Error" style="color: red;"></span>
<script>
  const emailInput3 = document.getElementById('email3');
  const emailError3 = document.getElementById('email3Error');
  emailInput3.addEventListener('input', () => {
    if (!emailInput3.checkValidity()) {
      emailInput3.setAttribute('aria-invalid', 'true');
      emailError3.textContent = emailInput3.validationMessage; // 使用浏览器默认错误信息
    } else {
      emailInput3.setAttribute('aria-invalid', 'false');
      emailError3.textContent = '';
    }
  });
  document.querySelector('form').addEventListener('submit', (event) => {
    if (!emailInput3.checkValidity()) {
      event.preventDefault();
      emailInput3.reportValidity();
    }
  });
</script>
在这个例子中,我们根据 checkValidity() 的结果,动态地设置 aria-invalid 属性。  屏幕阅读器可以使用这个属性来告知用户输入框是否包含无效的值。  同时,我们使用 validationMessage 属性来获取浏览器默认的错误信息,并将其显示在 span 元素中。  使用 validationMessage 可以避免重复编写错误提示,并确保错误提示与浏览器的行为一致。
aria-describedby 属性
可以使用 aria-describedby 属性将错误提示与表单控件关联起来。
<label for="phone">电话号码:</label>
<input type="tel" id="phone" aria-describedby="phoneError">
<span id="phoneError" role="alert" style="color: red; display: none;">请输入有效的电话号码</span>
<script>
  const phoneInput = document.getElementById('phone');
  const phoneError = document.getElementById('phoneError');
  phoneInput.addEventListener('input', () => {
      const isValid = /^d{3}-d{3}-d{4}$/.test(phoneInput.value);
      if (!isValid) {
          phoneInput.setAttribute('aria-invalid', 'true');
          phoneError.style.display = 'inline';
          phoneError.textContent = '请输入有效的电话号码(格式:XXX-XXX-XXXX)';
      } else {
          phoneInput.setAttribute('aria-invalid', 'false');
          phoneError.style.display = 'none';
          phoneError.textContent = '';
      }
  });
  document.querySelector('form').addEventListener('submit', (event) => {
      const isValid = /^d{3}-d{3}-d{4}$/.test(phoneInput.value);
      if (!isValid) {
          event.preventDefault();
          phoneInput.reportValidity(); // 触发浏览器默认错误,但实际错误信息由我们自定义
      }
  });
</script>
在这个例子中,我们使用 aria-describedby 属性将 phone 输入框与 phoneError 元素关联起来。  role="alert" 属性告诉屏幕阅读器,phoneError 元素包含重要的信息,应该立即告知用户。  当电话号码无效时,我们显示错误提示,并设置 aria-invalid 属性。  屏幕阅读器会读取 phoneError 元素的内容,告知用户错误信息。
总结
HTML 表单的约束验证 API 提供了强大的客户端验证能力。通过 setCustomValidity() 方法,我们可以自定义错误信息,提供更友好的用户体验。结合 :valid 和 :invalid 伪类,我们可以为表单控件添加视觉反馈,指示其有效性状态。使用 ARIA 属性可以增强表单的可访问性。掌握这些技术,可以构建更健壮、更易用、更易于访问的 Web 表单。
几个关键点的回顾
setCustomValidity是自定义错误信息的关键,要记得在输入有效时清空,否则会一直处于无效状态。:valid和:invalid伪类可以和 CSS 动画结合,提供更平滑的用户体验。- ARIA 属性的合理使用,能够提升表单的可用性。