表单验证第三部分: 一个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属性是最复杂的。 我们首先需要确认字段不为空 。 接着,如果字段的typeemail或者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.

文章系列:

  • 在HTML中的约束验证
  • 用JavaScript编写约束验证API
  • 一个Validity State API Polyfill
  • 验证MailChimp订阅表单Vans Varsity Pack Sk8-Hi Era