在本系列文章的上一篇文章中,我们使用 Validity State API 创建了一个轻量级脚本(6kb,压缩后 2.7kb),以增强原生表单验证体验。它适用于所有现代浏览器,并为 IE10 及更高版本提供支持。但是,存在一些浏览器问题。
并非所有浏览器都支持所有 Validity State 属性。Internet Explorer 是主要的违规者,尽管 Edge 确实缺少对 tooLong
的支持,即使 IE10+ 支持它。Chrome、Firefox 和 Safari 只是最近才全面支持。
今天,我们将编写一个轻量级 polyfill,将我们的浏览器支持扩展到 IE9,并将缺少的属性添加到部分支持的浏览器,而不会修改我们脚本中的任何核心代码。
文章系列
- HTML 中的约束验证
- JavaScript 中的约束验证 API
- Validity State API 的 Polyfill(您现在所处的位置!)
- 验证 MailChimp 订阅表单
让我们开始吧。
测试支持
我们需要做的第一件事是测试浏览器是否支持 Validity State。
为此,我们将使用 document.createElement('input')
创建一个表单输入,然后检查该元素上是否存在 validity
属性。
// Make sure that ValidityState is supported
var supported = function () {
var input = document.createElement('input');
return ('validity' in input);
};
supported()
函数将在支持的浏览器中返回 true
,在不支持的浏览器中返回 false
。
但是,仅仅测试 validity
属性是不够的。我们还需要确保所有 Validity State 属性都存在。
让我们扩展 supported()
函数来测试所有属性。
// Make sure that ValidityState is supported in full (all features)
var supported = function () {
var input = document.createElement('input');
return ('validity' in input && 'badInput' in input.validity && 'patternMismatch' in input.validity && 'rangeOverflow' in input.validity && 'rangeUnderflow' in input.validity && 'stepMismatch' in input.validity && 'tooLong' in input.validity && 'tooShort' in input.validity && 'typeMismatch' in input.validity && 'valid' in input.validity && 'valueMissing' in input.validity);
};
IE11 和 Edge 等浏览器将无法通过此测试,即使它们支持许多 Validity State 属性。
检查输入有效性
接下来,我们将编写自己的函数来检查表单字段的有效性,并使用与 Validity State API 相同的结构返回一个对象。
设置我们的检查
首先,我们将设置我们的函数并将字段作为参数传入。
// Generate the field validity object
var getValidityState = function (field) {
// Run our validity checks...
};
接下来,让我们设置一些变量,以便在我们的有效性测试中反复使用。
// Generate the field validity object
var getValidityState = function (field) {
// Variables
var type = field.getAttribute('type') || field.nodeName.toLowerCase(); // The field type
var isNum = type === 'number' || type === 'range'; // Is the field numeric
var length = field.value.length; // The field value length
};
测试有效性
现在,我们将创建包含所有有效性测试的对象。
// Generate the field validity object
var getValidityState = function (field) {
// Variables
var type = field.getAttribute('type') || input.nodeName.toLowerCase();
var isNum = type === 'number' || type === 'range';
var length = field.value.length;
// Run validity checks
var checkValidity = {
badInput: false, // value does not conform to the pattern
rangeOverflow: false, // value of a number field is higher than the max attribute
rangeUnderflow: false, // value of a number field is lower than the min attribute
stepMismatch: false, // value of a number field does not conform to the stepattribute
tooLong: false, // the user has edited a too-long value in a field with maxlength
tooShort: false, // the user has edited a too-short value in a field with minlength
typeMismatch: false, // value of a email or URL field is not an email address or URL
valueMissing: false // required field without a value
};
};
您会注意到,checkValidity
对象中缺少 valid
属性。我们只有在运行完其他测试后才能知道它是什么。
我们将循环遍历每个测试。如果任何测试为 true
,我们将把 valid
状态设置为 false
。否则,我们将将其设置为 true
。然后,我们将返回整个 checkValidity
。
// Generate the field validity object
var getValidityState = function (field) {
// Variables
var type = field.getAttribute('type') || input.nodeName.toLowerCase();
var isNum = type === 'number' || type === 'range';
var length = field.value.length;
// Run validity checks
var checkValidity = {
badInput: false, // value does not conform to the pattern
rangeOverflow: false, // value of a number field is higher than the max attribute
rangeUnderflow: false, // value of a number field is lower than the min attribute
stepMismatch: false, // value of a number field does not conform to the stepattribute
tooLong: false, // the user has edited a too-long value in a field with maxlength
tooShort: false, // the user has edited a too-short value in a field with minlength
typeMismatch: false, // value of a email or URL field is not an email address or URL
valueMissing: false // required field without a value
};
// Check if any errors
var valid = true;
for (var key in checkValidity) {
if (checkValidity.hasOwnProperty(key)) {
// If there's an error, change valid value
if (checkValidity[key]) {
valid = false;
break;
}
}
}
// Add valid property to validity object
checkValidity.valid = valid;
// Return object
return checkValidity;
};
编写测试
现在我们需要编写每个测试。大多数测试都将使用 test()
方法对字段值使用正则表达式模式。
badInput
对于 badInput
,如果字段是数字,至少包含一个字符,并且至少有一个字符 *不是* 数字,我们将返回 true
。
badInput: (isNum && length > 0 && !/[-+]?[0-9]/.test(field.value))
patternMismatch
patternMismatch
属性是最容易测试的属性之一。如果字段具有 pattern
属性,至少包含一个字符,并且字段值与包含的 pattern
正则表达式不匹配,则此属性为 true
。
patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false)
rangeOverflow
如果字段具有 max
属性,是数字,并且至少包含一个字符超过 max
值,则 rangeOverflow
属性应返回 true
。我们需要使用 parseInt()
方法将 max
的字符串值转换为整数。
rangeOverflow: (field.hasAttribute('max') && isNum && field.value > 1 && parseInt(field.value, 10) > parseInt(field.getAttribute('max'), 10))
rangeUnderflow
如果字段具有 min
属性,是数字,并且至少包含一个字符小于 min
值,则 rangeUnderflow
属性应返回 true
。与 rangeOverflow
一样,我们需要使用 parseInt()
方法将 min
的字符串值转换为整数。
rangeUnderflow: (field.hasAttribute('min') && isNum && field.value > 1 && parseInt(field.value, 10) < parseInt(field.getAttribute('min'), 10))
stepMismatch
对于 stepMismatch
属性,如果字段是数字,具有 step
属性,并且属性的值 *不是* any
,我们将使用余数运算符 (%
) 来确保字段值除以 step
没有余数。如果有余数,我们将返回 true
。
stepMismatch: (field.hasAttribute('step') && field.getAttribute('step') !== 'any' && isNum && Number(field.value) % parseFloat(field.getAttribute('step')) !== 0)
tooLong
对于 tooLong
,如果字段具有大于 0
的 maxLength
属性,并且字段值的 length
大于属性值,我们将返回 true
。
0 && length > parseInt(field.getAttribute('maxLength'), 10))
tooShort
相反,使用tooShort
,如果字段具有大于0
的minLength
属性,并且字段值的length
小于属性值,我们将返回true
。
tooShort: (field.hasAttribute('minLength') && field.getAttribute('minLength') > 0 && length > 0 && length < parseInt(field.getAttribute('minLength'), 10))
typeMismatch
typeMismatch
属性是最复杂的验证属性。我们首先需要确保字段不为空。然后,如果字段的type
是email
,我们需要运行一个正则表达式文本,如果是url
,则需要运行另一个正则表达式文本。如果它是这些值之一,并且字段值与我们的正则表达式模式不匹配,我们将返回true
。
typeMismatch: (length > 0 && ((type === 'email' && !/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/.test(field.value)) || (type === 'url' && !/^(?:(?:https?|HTTPS?|ftp|FTP):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)(?:\.(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)*)(?::\d{2,5})?(?:[\/?#]\S*)?$/.test(field.value))))
valueMissing
valueMissing
属性也比较复杂。首先,我们要检查字段是否具有required
属性。如果有,我们需要根据字段类型运行三种不同的测试之一。
如果是复选框或单选按钮,我们需要确保它被选中。如果是下拉菜单,我们需要确保选择了值。如果是其他类型的输入,我们需要确保它有值。
valueMissing: (field.hasAttribute('required') && (((type === 'checkbox' || type === 'radio') && !field.checked) || (type === 'select' && field.options[field.selectedIndex].value < 1) || (type !=='checkbox' && type !== 'radio' && type !=='select' && length < 1)))
所有测试的完整集合
以下是包含所有测试的完成的checkValidity
对象。
// Run validity checks
var checkValidity = {
badInput: (isNum && length > 0 && !/[-+]?[0-9]/.test(field.value)), // value of a number field is not a number
patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false), // value does not conform to the pattern
rangeOverflow: (field.hasAttribute('max') && isNum && field.value > 1 && parseInt(field.value, 10) > parseInt(field.getAttribute('max'), 10)), // value of a number field is higher than the max attribute
rangeUnderflow: (field.hasAttribute('min') && isNum && field.value > 1 && parseInt(field.value, 10) < parseInt(field.getAttribute('min'), 10)), // value of a number field is lower than the min attribute
stepMismatch: (field.hasAttribute('step') && field.getAttribute('step') !== 'any' && isNum && Number(field.value) % parseFloat(field.getAttribute('step')) !== 0), // value of a number field does not conform to the stepattribute
tooLong: (field.hasAttribute('maxLength') && field.getAttribute('maxLength') > 0 && length > parseInt(field.getAttribute('maxLength'), 10)), // the user has edited a too-long value in a field with maxlength
tooShort: (field.hasAttribute('minLength') && field.getAttribute('minLength') > 0 && length > 0 && length < parseInt(field.getAttribute('minLength'), 10)), // the user has edited a too-short value in a field with minlength
typeMismatch: (length > 0 && ((type === 'email' && !/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/.test(field.value)) || (type === 'url' && !/^(?:(?:https?|HTTPS?|ftp|FTP):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)(?:\.(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)*)(?::\d{2,5})?(?:[\/?#]\S*)?$/.test(field.value)))), // value of a email or URL field is not an email address or URL
valueMissing: (field.hasAttribute('required') && (((type === 'checkbox' || type === 'radio') && !field.checked) || (type === 'select' && field.options[field.selectedIndex].value < 1) || (type !=='checkbox' && type !== 'radio' && type !=='select' && length < 1))) // required field without a value
};
单选按钮的特殊注意事项
在支持的浏览器中,只有当组中没有元素被选中时,required
才会在单选按钮上失败。我们目前编写的 polyfill 即使组中另一个按钮被选中,也会在未选中的单选按钮上将valueMissing
作为true
返回。
为了解决这个问题,我们需要获取组中的每个按钮。如果其中一个被选中,我们将验证该单选按钮,而不是失去焦点的那个按钮。
// Generate the field validity object
var getValidityState = function (field) {
// Variables
var type = field.getAttribute('type') || input.nodeName.toLowerCase(); // The field type
var isNum = type === 'number' || type === 'range'; // Is the field numeric
var length = field.value.length; // The field value length
// If radio group, get selected field
if (field.type === 'radio' && field.name) {
var group = document.getElementsByName(field.name);
if (group.length > 0) {
for (var i = 0; i < group.length; i++) {
if (group[i].form === field.form && field.checked) {
field = group[i];
break;
}
}
}
}
...
};
validity
属性添加到表单字段
将最后,如果 Validity State API 没有完全支持,我们希望添加或覆盖validity
属性。我们将使用Object.defineProperty()
方法来完成此操作。
// If the full set of ValidityState features aren't supported, polyfill
if (!supported()) {
Object.defineProperty(HTMLInputElement.prototype, 'validity', {
get: function ValidityState() {
return getValidityState(this);
},
configurable: true,
});
}
将所有内容放在一起
以下是 polyfill 的完整内容。为了将我们的函数排除在全局范围之外,我将其包装在 IIFE(立即调用函数表达式)中。
;(function (window, document, undefined) {
'use strict';
// Make sure that ValidityState is supported in full (all features)
var supported = function () {
var input = document.createElement('input');
return ('validity' in input && 'badInput' in input.validity && 'patternMismatch' in input.validity && 'rangeOverflow' in input.validity && 'rangeUnderflow' in input.validity && 'stepMismatch' in input.validity && 'tooLong' in input.validity && 'tooShort' in input.validity && 'typeMismatch' in input.validity && 'valid' in input.validity && 'valueMissing' in input.validity);
};
/**
* Generate the field validity object
* @param {Node]} field The field to validate
* @return {Object} The validity object
*/
var getValidityState = function (field) {
// Variables
var type = field.getAttribute('type') || input.nodeName.toLowerCase();
var isNum = type === 'number' || type === 'range';
var length = field.value.length;
var valid = true;
// Run validity checks
var checkValidity = {
badInput: (isNum && length > 0 && !/[-+]?[0-9]/.test(field.value)), // value of a number field is not a number
patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false), // value does not conform to the pattern
rangeOverflow: (field.hasAttribute('max') && isNum && field.value > 1 && parseInt(field.value, 10) > parseInt(field.getAttribute('max'), 10)), // value of a number field is higher than the max attribute
rangeUnderflow: (field.hasAttribute('min') && isNum && field.value > 1 && parseInt(field.value, 10) < parseInt(field.getAttribute('min'), 10)), // value of a number field is lower than the min attribute
stepMismatch: (field.hasAttribute('step') && field.getAttribute('step') !== 'any' && isNum && Number(field.value) % parseFloat(field.getAttribute('step')) !== 0), // value of a number field does not conform to the stepattribute
tooLong: (field.hasAttribute('maxLength') && field.getAttribute('maxLength') > 0 && length > parseInt(field.getAttribute('maxLength'), 10)), // the user has edited a too-long value in a field with maxlength
tooShort: (field.hasAttribute('minLength') && field.getAttribute('minLength') > 0 && length > 0 && length < parseInt(field.getAttribute('minLength'), 10)), // the user has edited a too-short value in a field with minlength
typeMismatch: (length > 0 && ((type === 'email' && !/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/.test(field.value)) || (type === 'url' && !/^(?:(?:https?|HTTPS?|ftp|FTP):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)(?:\.(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)*)(?::\d{2,5})?(?:[\/?#]\S*)?$/.test(field.value)))), // value of a email or URL field is not an email address or URL
valueMissing: (field.hasAttribute('required') && (((type === 'checkbox' || type === 'radio') && !field.checked) || (type === 'select' && field.options[field.selectedIndex].value < 1) || (type !=='checkbox' && type !== 'radio' && type !=='select' && length < 1))) // required field without a value
};
// Check if any errors
for (var key in checkValidity) {
if (checkValidity.hasOwnProperty(key)) {
// If there's an error, change valid value
if (checkValidity[key]) {
valid = false;
break;
}
}
}
// Add valid property to validity object
checkValidity.valid = valid;
// Return object
return checkValidity;
};
// If the full set of ValidityState features aren't supported, polyfill
if (!supported()) {
Object.defineProperty(HTMLInputElement.prototype, 'validity', {
get: function ValidityState() {
return getValidityState(this);
},
configurable: true,
});
}
})(window, document);
将此添加到您的网站将把 Validity State API 扩展到 IE9,并将丢失的属性添加到部分支持的浏览器。(您也可以在 GitHub 上下载 polyfill)。
我们在上一篇文章中编写的表单验证脚本也使用了classList
API,该 API 在所有现代浏览器以及 IE10 及更高版本中受支持。要真正获得 IE9+ 支持,我们还应该包含Eli Grey 的classList.js
polyfill。
文章系列
- HTML 中的约束验证
- JavaScript 中的约束验证 API
- Validity State API 的 Polyfill(您现在所处的位置!)
- 验证 MailChimp 订阅表单
感谢 Chris:很棒的东西!
不知道你是否看过 PPK 关于原生表单验证的发现(第一部分,第二部分,以及 第三部分)。
他的结论是:“它不起作用”,不要只使用原生验证。
你的想法是什么?
我实际上在写这篇文章之前就读过它们。我大体上同意 PPK 的观点:CSS 钩子与 HTML 属性之间不能很好地配合使用。JS API 很奇怪,笨拙,在某些情况下是多余的,并且缺乏一致的浏览器支持。
尽管如此……我认为 Validity State 几乎完美,而且有了 polyfill 来将支持添加到旧浏览器,并在现代浏览器之间驱动一致性,它完全适合许多基本和中级验证需求,并且占用空间小巧。
悲剧的是,
badInput
的 RegExp:!/[-+]?[0-9]/.test(field.value)
无法按预期工作。
它接受“1p”或“p1p”作为有效值。
嗯……抓住了!让我弄清楚这里发生了什么,并相应地更新。
我真的很喜欢你关于表单验证的系列文章。
受你的
supported
函数的启发,我创建了自己的函数,但我对其进行了改进,希望你不介意。我期待你本系列文章的下一篇文章(也是最后一篇文章)。
我喜欢数组和循环方法!你介意我把这个整合到 polyfill 中吗?这太棒了!
我很乐意。;)
干杯!刚在 GitHub 上创建了一个关于此的问题。非常感谢!