表单验证第三部分:Validity State API Polyfill

Avatar of Chris Ferdinandi
Chris Ferdinandi 发布

DigitalOcean 为您旅途中的每个阶段提供云产品。立即开始使用 价值 200 美元的免费积分!

在本系列文章的上一篇文章中,我们使用 Validity State API 创建了一个轻量级脚本(6kb,压缩后 2.7kb),以增强原生表单验证体验。它适用于所有现代浏览器,并为 IE10 及更高版本提供支持。但是,存在一些浏览器问题。

并非所有浏览器都支持所有 Validity State 属性。Internet Explorer 是主要的违规者,尽管 Edge 确实缺少对 tooLong 的支持,即使 IE10+ 支持它。Chrome、Firefox 和 Safari 只是最近才全面支持。

今天,我们将编写一个轻量级 polyfill,将我们的浏览器支持扩展到 IE9,并将缺少的属性添加到部分支持的浏览器,而不会修改我们脚本中的任何核心代码。

文章系列

  1. HTML 中的约束验证
  2. JavaScript 中的约束验证 API
  3. Validity State API 的 Polyfill(您现在所处的位置!)
  4. 验证 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,如果字段具有大于 0maxLength 属性,并且字段值的 length 大于属性值,我们将返回 true

 0 && length > parseInt(field.getAttribute('maxLength'), 10))
tooShort

相反,使用tooShort,如果字段具有大于0minLength属性,并且字段值的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 对象。

// 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。

文章系列

  1. HTML 中的约束验证
  2. JavaScript 中的约束验证 API
  3. Validity State API 的 Polyfill(您现在所处的位置!)
  4. 验证 MailChimp 订阅表单