表单验证第三部分: 一个Validity State API Polyfill
本文转载自:众成翻译 译者:cherryjin 链接:http://www.zcfy.cc/article/3674 原文:https://css-tricks.com/form-validation-part-3-validity-state-api-polyfill/
在这个系列的上一篇文章中, 我们创建了一个轻量级的脚本 (6kb, 2.7kb缩小后) 使用这个 Validity State API 来提高原生的表单验证体验。 这个脚本可以在所有的现代浏览器上运行,并且支持 IE10版本以下的浏览器。但是,浏览器本身仍然具有一些缺陷。
并不是所有的浏览器都支持每一个 Validity State 属性。 Internet Explorer 就是主要的违规者, 即使 IE10+支持 tooLong
Edge 却不支持.并且Chrome, Firefox, 和 Safari 也是最近才支持所有的属性。
在这,我们要编写一个轻量级的polyfill可使浏览器的支持性扩展到 IE9以下的版本,并且添加缺失的属性到那些只支持部分属性的浏览器上,,而不需要动我们的核心代码。
文章系列:
让我们开始吧.
测试支持情况
我们需要做的第一件事就是测试浏览器对 Validity State 的支持情况。
为了完成测试,我们使用 document.createElement('input')
来创建一个表单input
元素,然后检测这个元素是否存在 validity
属性 。
// Make sure that ValidityState is supported
var supported = function () {
var input = document.createElement('input');
return ('validity' in input);
};
supported()
函数在支持input
的浏览器上会返回 true
, 在不支持的浏览器上返回 false
。
仅仅测试 validity
属性是不够的, 我们需要对每个属性进行测试。
让我们扩展 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 属性.
检测 input validity
接下来, 我们会编写我们自己的函数来检测一个表单字段的 validity
,并且返回一个与Validity State API有着相同结构的对象。
设置我们的检查
首先, 我们准备好函数, 并且将字段作为参数传入函数。
// Generate the field validity object
var getValidityState = function (field) {
// Run our validity checks...
};
接下来, 让我们把在测试中需要重复用到的东西抽象成对象。
// 生成字段validity对象
var getValidityState = function (field) {
//变量
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
};
测试 Validity
现在,我们将创建一个包含所有要测试的validity
的对象。
// 生成字段validity对象
var getValidityState = function (field) {
//变量
var type = field.getAttribute('type') || input.nodeName.toLowerCase();
var isNum = type === 'number' || type === 'range';
var length = field.value.length;
// 检查合法性
var checkValidity = {
badInput: false, // 输入的值与模式不匹配
rangeOverflow: false, // 数字字段的值大于max属性值
rangeUnderflow: false, //数字字段的值小于min属性的值
stepMismatch: false, // 数字字段的值不符合stepattribute
tooLong: false, // 用户在具有maxLength属性的字段中输入的值的长度大于属性值
tooShort: false, //用户在具有minLength属性的字段中输入的值的长度小于属性值
typeMismatch: false, // email 或者 URL 字段的值不是一个 email 地址或者URL
valueMissing: false // 必填的字段没有值
};
};
你可能会注意到在checkValidity
对象中没有 valid
属性 。我们只有在进行其他的测试后,才会知道valid
属性值是什么。
我们迭代这些属性进行测试。如果它们中的任何一个值是true
,我们就把 valid
的状态设置为 false
。 否则, 就设置为 true
。接下来,我们就返回整个 checkValidity
。
// 生成字段validity对象
var getValidityState = function (field) {
// 变量
var type = field.getAttribute('type') || input.nodeName.toLowerCase();
var isNum = type === 'number' || type === 'range';
var length = field.value.length;
// 检查合法性
var checkValidity = {
badInput: false, // 输入的值与模式不匹配
rangeOverflow: false, // 数字字段的值大于max属性值
rangeUnderflow: false, // 数字字段的值小于min属性的值
stepMismatch: false, // 数字字段的值不符合stepattribute
tooLong: false, // 用户在具有maxLength属性的字段中输入的值的长度大于属性值
tooShort: false, // 用户在具有minLength属性的字段中输入的值的长度小于属性值
typeMismatch: false, // email或者 URL字段的值不是一个 email地址或者URL
valueMissing: false // 必填的字段没有值
};
// 检查是否有错误
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;
}
}
}
// 给validity对象添加 valid 属性
checkValidity.valid = valid;
// 返回对象
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
,如果该字段的 maxLength
属性值大于0
,并且该字段值的length
大于属性值 ,我们就返回 true
。
<tooLong: (field.hasAttribute('maxLength') && field.getAttribute('maxLength') > 0 && length > parseInt(field.getAttribute('maxLength'), 10))
tooShort
相反的, 对于 tooShort
, 如果该字段的minLength
属性大于0
,并且这个字段值的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
对象的测试。
// 检查合法性
var checkValidity = {
badInput: (isNum && length > 0 && !/[-+]?[0-9]/.test(field.value)), // 数字字段的值不是数字
patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false), // 输入的值与模式不匹配
rangeOverflow: (field.hasAttribute('max') && isNum && field.value > 1 && parseInt(field.value, 10) > parseInt(field.getAttribute('max'), 10)), // 数字字段的值大于max属性值
rangeUnderflow: (field.hasAttribute('min') && isNum && field.value > 1 && parseInt(field.value, 10) < parseInt(field.getAttribute('min'), 10)), //数字字段的值小于min属性值
stepMismatch: (field.hasAttribute('step') && field.getAttribute('step') !== 'any' && isNum && Number(field.value) % parseFloat(field.getAttribute('step')) !== 0), // 数字字段的值不符合 stepattribute
tooLong: (field.hasAttribute('maxLength') && field.getAttribute('maxLength') > 0 && length > parseInt(field.getAttribute('maxLength'), 10)), //用户在具有maxLength属性的字段中输入的值的长度大于属性值
tooShort: (field.hasAttribute('minLength') && field.getAttribute('minLength') > 0 && length > 0 && length < parseInt(field.getAttribute('minLength'), 10)), // 用户在具有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)))), // email或者 URL字段的值不是一个 email地址或者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
将会在单选按钮上失败 。 我们现在编写的ployfill会在没有检测过的单选按钮将 valueMissing
返回true
,即使集合中的另外的按钮以及被检测过。
为了修复这个问题, 我们需要获取集合里的每一个按钮。如果它们中有一个被检测过, 我们就验证这个单选按钮而不是失去焦点的那个。
// 生成字段validity 对象
var getValidityState = function (field) {
// 变量
var type = field.getAttribute('type') || input.nodeName.toLowerCase(); // The field type
var isNum = type === 'number' || type === 'range'; // 字段是数字
var length = field.value.length; // 这个字段的长度
// 如果是单选按钮集合, 获取被选中的字段
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()
方法来实现它.
// 如果不支持完整的ValidityState功能,则可以使用polyfill
if (!supported()) {
Object.defineProperty(HTMLInputElement.prototype, 'validity', {
get: function ValidityState() {
return getValidityState(this);
},
configurable: true,
});
}
把所有的代码集合在一起
一个完整的polyfill就在这里了。为了不使我们的函数污染全局作用域, 我把它包含在一个 IIFE (立即执行函数表达式)中。
;(function (window, document, undefined) {
'use strict';
// 确保 ValidityState 全部被支持 (所有的功能)
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) {
// 变量
var type = field.getAttribute('type') || input.nodeName.toLowerCase();
var isNum = type === 'number' || type === 'range';
var length = field.value.length;
var valid = true;
//检测支持性
var checkValidity = {
badInput: (isNum && length > 0 && !/[-+]?[0-9]/.test(field.value)), // 数字字段的值不是数字
patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false), // 输入的值不符合模式
rangeOverflow: (field.hasAttribute('max') && isNum && field.value > 1 && parseInt(field.value, 10) > parseInt(field.getAttribute('max'), 10)), // 数字字段的值大于max属性值
rangeUnderflow: (field.hasAttribute('min') && isNum && field.value > 1 && parseInt(field.value, 10) < parseInt(field.getAttribute('min'), 10)), // 数字字段的值小于min属性值
stepMismatch: (field.hasAttribute('step') && field.getAttribute('step') !== 'any' && isNum && Number(field.value) % parseFloat(field.getAttribute('step')) !== 0), // 数字字段的值不符合 stepattribute
tooLong: (field.hasAttribute('maxLength') && field.getAttribute('maxLength') > 0 && length > parseInt(field.getAttribute('maxLength'), 10)), // 用户在具有maxLength属性的字段中输入的值的长度大于属性值
tooShort: (field.hasAttribute('minLength') && field.getAttribute('minLength') > 0 && length > 0 && length < parseInt(field.getAttribute('minLength'), 10)), // 用户在具有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)))), // email 或者 URL 字段的值不是一个 email地址或者 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))) // 必填字段没有值
};
//检查是否有错误
for (var key in checkValidity) {
if (checkValidity.hasOwnProperty(key)) {
// If there's an error, change valid value
if (checkValidity[key]) {
valid = false;
break;
}
}
}
//给 validity对象添加valid属性
checkValidity.valid = valid;
// 返回对象
return checkValidity;
};
//如果不支持完整的ValidityState功能,则可以使用polyfill
if (!supported()) {
Object.defineProperty(HTMLInputElement.prototype, 'validity', {
get: function ValidityState() {
return getValidityState(this);
},
configurable: true,
});
}
})(window, document);
将其添加到你的站点中,可使Validity State API扩展到IE9以下, 并且将缺少的属性添加到只支持部分属性的浏览器上。 (你可以 在 GitHub上下载这个插件版本, too.)
在上一篇文章中我们编写的表单验证脚本可以使用 classList
API, 它得到了现代浏览器以及IE10 以上版本的支持 。为了真正得到IE9+的支持, 我们也应该包含 Eli Grey 编写的classList.js polyfill.