自定义表单验证

我们知道原生的html元素支持做一些简单的验证,但是不是很美观,我们可以自己来定义这些错误消息,仍然使用元素的那套验证。

validity属性提供了关于表单字段的一组信息,值是布尔值(true/false)。

var myField = document.querySelector('input[type="text"]');
var validityState = myField.validity;

返回的对象(validityState)包含以下属性:

  • valid: 当字段通过验证的时候,是true

  • valueMissing: 当必填的字段是空的时候,这个是true

  • typeMismatch: 当字段type限制是emailurl的时候,但输入的value(值)不是正确的类型,这个时候返回的值是true

  • tooShort: 这个就是当元素有minLength属性的时候,但是输入的value没有达到这个限制(比这个小),这时候是true

  • tooLong: 这个和tooShort差不多,不过这个是和maxLength进行对比。

  • patternMismatch: 这个与typeMismatch差不多,不过是与pattern类型相比较。

  • badInput: 类型是number,但是输入的value不是number,就会返回true

  • stepMismatch: 当字段具有step属性且输入的value不符合步骤值时,返回true

  • rangeOverflow: 当字段具有max属性且输入的数字value大于max时,就返回true

  • rangeUnderflow: 当字段具有min属性且输入的数字value低于min时,就会返回true

下面就用这些验证和js配合,来构建一个健壮的表单验证。

禁用原生验证

首先使用novalidate属性来禁止原生的form验证提示。 作为最佳实践,我们应该使用js脚本来添加,这样当js无法加载时,保证原生的验证仍然可用。

var forms = document.querySelectorAll('form');
for (var i = 0; i < forms.length; i++) {
    forms[i].setAttribute('novalidate', true);
}

可能有些表单,你不想验证,比如搜索表单。你只想验证一些需要验证的表单,那么我们来加一个类名,叫做. validate

// js加载时添加novalidate属性
var forms = document.querySelectorAll('.validate');
for (var i = 0; i < forms.length; i++) {
    forms[i].setAttribute('novalidate', true);
}

可以在线查看效果。

用户离开时检查有效性(失去焦点)

我们将使用事件冒泡(或事件传播)来监听所有blur事件,而不是为每个表单字段添加一个监听器。

// Listen to all blur events
document.addEventListener('blur', function (event) {
    // Do something on blur...
}, true);

对于上面代码第三个参数(true)不了解的同学可以看看这里

第三个参数叫做useCapture,他通常默认是设置成falseblur事件不会像click事件那样冒泡。将此参数设置为true允许我们捕获所有blur事件,而不仅仅是那些直接发生在我们正在侦听的元素上的事件。

接下来,我们要确保blur元素是具有.validate类的表单中的字段。我们可以使用event.target获取blur的元素,并通过调用event.target.form获取它的父级form。然后使用classList来检查是否含有验证这个类。

// Listen to all blur events
document.addEventListener('blur', function (event) {

    // Only run if the field is in a form to be validated
    if (!event.target.form.classList.contains('validate')) return;

    // Validate the field
    var error = event.target.validity;
    console.log(error);

}, true);

如果errortrue,则该字段有效。否则,出现错误。 可以在线查看

获取错误

由于我们需要检查每个属性,因此代码可能会有点长。让我们为此设置一个单独的函数,并将我们的字段传递给它。

// Validate the field
var hasError = function (field) {
    // Get the error
};

// Listen to all blur events
document.addEventListner('blur', function (event) {

    // Only run if the field is in a form to be validated
    if (!event.target.form.classList.contains('validate')) return;

    // Validate the field
    var error = hasError(event.target);

}, true);

我们要忽略一些字段类型:禁用的字段,filereset输入以及submit input和按钮。如果一个字段不是其中之一,那就让它有效。

// Validate the field
var hasError = function (field) {

    // Don't validate submits, buttons, file and reset inputs, and disabled fields
    if (field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') return;

    // Get validity
    var validity = field.validity;

};

如果没有错误,我们将返回null。否则,我们将检查每个有效状态属性,直到找到错误。

// Validate the field
var hasError = function (field) {

    // Don't validate submits, buttons, file and reset inputs, and disabled fields
    if (field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') return;

    // Get validity
    var validity = field.validity;

    // If valid, return null
    if (validity.valid) return;

    // If field is required and empty
    if (validity.valueMissing) return 'Please fill out this field.';

    // If not the right type
    if (validity.typeMismatch) return 'Please use the correct input type.';

    // If too short
    if (validity.tooShort) return 'Please lengthen this text.';

    // If too long
    if (validity.tooLong) return 'Please shorten this text.';

    // If number input isn't a number
    if (validity.badInput) return 'Please enter a number.';

    // If a number value doesn't match the step interval
    if (validity.stepMismatch) return 'Please select a valid value.';

    // If a number field is over the max
    if (validity.rangeOverflow) return 'Please select a smaller value.';

    // If a number field is below the min
    if (validity.rangeUnderflow) return 'Please select a larger value.';

    // If pattern doesn't match
    if (validity.patternMismatch) return 'Please match the requested format.';

    // If all else fails, return a generic catchall error
    return 'The value you entered for this field is invalid.';

};

这是一个好的开始,但我们可以做一些额外的解析,以使我们的一些错误更有用。对于typeMismatch,我们可以检查它是否应该是emailurl,并相应地自定义错误。

// If not the right type
if (validity.typeMismatch) {

    // Email
    if (field.type === 'email') return 'Please enter an email address.';

    // URL
    if (field.type === 'url') return 'Please enter a URL.';

}

如果字段值太长或太短,我们可以找出它应该有多长或多短以及实际有多长或多短。然后我们可以在错误中包含该信息。

// If too short
if (validity.tooShort) return 'Please lengthen this text to ' + field.getAttribute('minLength') + ' characters or more. You are currently using ' + field.value.length + ' characters.';

// If too long
if (validity.tooLong) return 'Please short this text to no more than ' + field.getAttribute('maxLength') + ' characters. You are currently using ' + field.value.length + ' characters.';

如果数字字段超出或低于允许范围,我们可以在错误中包含最小或最大允许值。

// If a number field is over the max
if (validity.rangeOverflow) return 'Please select a value that is no more than ' + field.getAttribute('max') + '.';

// If a number field is below the min
if (validity.rangeUnderflow) return 'Please select a value that is no less than ' + field.getAttribute('min') + '.';

如果pattern模式不匹配且字段具有title,我们可以将其用作我们的错误显示,就像浏览器原生行为一样。

// If pattern doesn't match
if (validity.patternMismatch) {

    // If pattern info is included, return custom error
    if (field.hasAttribute('title')) return field.getAttribute('title');

    // Otherwise, generic error
    return 'Please match the requested format.';

}

这是我们的hasError()函数的完整代码:

// Validate the field
var hasError = function (field) {

    // Don't validate submits, buttons, file and reset inputs, and disabled fields
    if (field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') return;

    // Get validity
    var validity = field.validity;

    // If valid, return null
    if (validity.valid) return;

    // If field is required and empty
    if (validity.valueMissing) return 'Please fill out this field.';

    // If not the right type
    if (validity.typeMismatch) {

        // Email
        if (field.type === 'email') return 'Please enter an email address.';

        // URL
        if (field.type === 'url') return 'Please enter a URL.';

    }

    // If too short
    if (validity.tooShort) return 'Please lengthen this text to ' + field.getAttribute('minLength') + ' characters or more. You are currently using ' + field.value.length + ' characters.';

    // If too long
    if (validity.tooLong) return 'Please shorten this text to no more than ' + field.getAttribute('maxLength') + ' characters. You are currently using ' + field.value.length + ' characters.';

    // If number input isn't a number
    if (validity.badInput) return 'Please enter a number.';

    // If a number value doesn't match the step interval
    if (validity.stepMismatch) return 'Please select a valid value.';

    // If a number field is over the max
    if (validity.rangeOverflow) return 'Please select a value that is no more than ' + field.getAttribute('max') + '.';

    // If a number field is below the min
    if (validity.rangeUnderflow) return 'Please select a value that is no less than ' + field.getAttribute('min') + '.';

    // If pattern doesn't match
    if (validity.patternMismatch) {

        // If pattern info is included, return custom error
        if (field.hasAttribute('title')) return field.getAttribute('title');

        // Otherwise, generic error
        return 'Please match the requested format.';

    }

    // If all else fails, return a generic catchall error
    return 'The value you entered for this field is invalid.';

};

在线的codepen中自己尝试一下。

显示错误消息

我们将创建一个showError()函数来处理它,并传入我们的字段和错误。然后,我们将在事件监听器中调用它。

// Show the error message
var showError = function (field, error) {
    // Show the error message...
};

// Listen to all blur events
document.addEventListener('blur', function (event) {

    // Only run if the field is in a form to be validated
    if (!event.target.form.classList.contains('validate')) return;

    // Validate the field
    var error = hasError(event.target);

    // If there's an error, show it
    if (error) {
        showError(event.target, error);
    }

}, true);

showError中将会做几件事: 1. 我们将在字段中添加一个带有错误的类,以便我们可以设置它的样式。 2. 如果已存在错误消息,我们将使用新文本对其进行更新。 3. 否则,我们将创建一条消息并在字段后立即将其注入DOM。

我们还将使用字段ID为消息创建唯一ID,以便稍后再次找到它(如果没有ID,则使用name)。

var showError = function (field, error) {

    // Add error class to field
    field.classList.add('error');

    // Get field id or name
    var id = field.id || field.name;
    if (!id) return;

    // Check if error message field already exists
    // If not, create one
    var message = field.form.querySelector('.error-message#error-for-' + id );
    if (!message) {
        message = document.createElement('div');
        message.className = 'error-message';
        message.id = 'error-for-' + id;
        field.parentNode.insertBefore( message, field.nextSibling );
    }

    // Update error message
    message.innerHTML = error;

    // Show error message
    message.style.display = 'block';
    message.style.visibility = 'visible';

};

为了确保屏幕阅读器和其他辅助技术知道我们的错误消息与我们的字段相关联,我们还需要添加aria-describedby角色。

var showError = function (field, error) {

    // Add error class to field
    field.classList.add('error');

    // Get field id or name
    var id = field.id || field.name;
    if (!id) return;

    // Check if error message field already exists
    // If not, create one
    var message = field.form.querySelector('.error-message#error-for-' + id );
    if (!message) {
        message = document.createElement('div');
        message.className = 'error-message';
        message.id = 'error-for-' + id;
        field.parentNode.insertBefore( message, field.nextSibling );
    }

    // Add ARIA role to the field
    field.setAttribute('aria-describedby', 'error-for-' + id);

    // Update error message
    message.innerHTML = error;

    // Show error message
    message.style.display = 'block';
    message.style.visibility = 'visible';

};

设置错误消息的样式

我们可以使用.error.error-message类来设置表单字段和错误消息的样式。 举个简单的例子,你可能希望在带有错误的字段周围显示红色边框,并将错误消息设置为红色和斜体。

.error {
  border-color: red;
}

.error-message {
  color: red;
  font-style: italic;
}

在线效果

隐藏错误消息

一旦我们显示错误,字段验证后,我们需要删除错误消息。让我们创建另一个函数removeError(),并传入该字段。我们也将从事件监听器调用此函数。

// Remove the error message
var removeError = function (field) {
    // Remove the error message...
};

// Listen to all blur events
document.addEventListener('blur', function (event) {

    // Only run if the field is in a form to be validated
    if (!event.target.form.classList.contains('validate')) return;

    // Validate the field
    var error = event.target.validity;

    // If there's an error, show it
    if (error) {
        showError(event.target, error);
        return;
    }

    // Otherwise, remove any existing error message
    removeError(event.target);

}, true);

removeError()中,我们想要: 1. 从我们的字段中删除错误类。 2. 从字段中删除aria-describedby角色。 3. 隐藏DOM中的任何可见错误消息。

因为我们可以在页面上有多个表单,并且这些表单可能具有相同名称或ID的字段(即使这些字段无效,也会发生),我们将使用querySelector限制我们对错误消息的搜索,而不是整个文档。

// Remove the error message
var removeError = function (field) {

    // Remove error class to field
    field.classList.remove('error');

    // Remove ARIA role from the field
    field.removeAttribute('aria-describedby');

    // Get field id or name
    var id = field.id || field.name;
    if (!id) return;

    // Check if an error message is in the DOM
    var message = field.form.querySelector('.error-message#error-for-' + id + '');
    if (!message) return;

    // If so, hide it
    message.innerHTML = '';
    message.style.display = 'none';
    message.style.visibility = 'hidden';

};

来看看[效果](https://codepen.io/cferdinandi/embed/JJYYpY?height=559&theme-id=1&slug-hash=JJYYpY&default-tab=js%2Cresult&user=cferdinandi&embed-version=2&pen-title=Form Validation%3A Remove the Error After It's Fixed&name=cp_embed_5)。 如果该字段是单选按钮或复选框,我们需要更改我们将错误消息添加到DOM的方式。 对于这些类型的输入,字段标签通常位于字段之后,或者完全包装它。此外,如果单选按钮是组的一部分,我们希望错误出现在组之后,而不仅仅是单选按钮。 看看这里

首先,我们需要修改showError()方法。如果字段typeradio并且它具有name,我们希望获得具有相同name的所有单选按钮(即组中的所有其他单选按钮)并将我们的field变量重置为组中的最后一个。

// Show the error message
var showError = function (field, error) {

    // Add error class to field
    field.classList.add('error');

    // If the field is a radio button and part of a group, error all and get the last item in the group
    if (field.type === 'radio' && field.name) {
        var group = document.getElementsByName(field.name);
        if (group.length > 0) {
            for (var i = 0; i < group.length; i++) {
                // Only check fields in current form
                if (group[i].form !== field.form) continue;
                group[i].classList.add('error');
            }
            field = group[group.length - 1];
        }
    }

    ...

};

当我们将消息注入DOM时,我们首先要检查字段类型是否为radiocheckbox。如果是这样,我们希望得到字段标签并在它之后而不是在字段本身之后注入我们的消息。

// Show the error message
var showError = function (field, error) {

    ...

    // Check if error message field already exists
    // If not, create one
    var message = field.form.querySelector('.error-message#error-for-' + id );
    if (!message) {
        message = document.createElement('div');
        message.className = 'error-message';
        message.id = 'error-for-' + id;

        // If the field is a radio button or checkbox, insert error after the label
        var label;
        if (field.type === 'radio' || field.type ==='checkbox') {
            label = field.form.querySelector('label[for="' + id + '"]') || field.parentNode;
            if (label) {
                label.parentNode.insertBefore( message, label.nextSibling );
            }
        }

        // Otherwise, insert it after the field
        if (!label) {
            field.parentNode.insertBefore( message, field.nextSibling );
        }
    }

    ...

};

当我们去除错误时,我们同样需要检查该字段是否是组中一部分的单选按钮,如果是,请使用该组中的最后一个单选按钮来获取错误消息的ID。

// Remove the error message
var removeError = function (field) {

    // Remove error class to field
    field.classList.remove('error');

    // If the field is a radio button and part of a group, remove error from all and get the last item in the group
    if (field.type === 'radio' && field.name) {
        var group = document.getElementsByName(field.name);
        if (group.length > 0) {
            for (var i = 0; i < group.length; i++) {
                // Only check fields in current form
                if (group[i].form !== field.form) continue;
                group[i].classList.remove('error');
            }
            field = group[group.length - 1];
        }
    }

    ...

};

那再来看看在线的例子啊

检查提交中的所有字段

submit事件添加一个监听器。

// Check all fields on submit
document.addEventListener('submit', function (event) {
    // Validate all fields...
}, false);

如果表单具有.validate类,我们将获取每个字段,遍历每个字段,并检查错误。

// Check all fields on submit
document.addEventListener('submit', function (event) {

    // Only run on forms flagged for validation
    if (!event.target.classList.contains('validate')) return;

    // Get all of the form elements
    var fields = event.target.elements;

    // Validate each field
    // Store the first field with an error to a variable so we can bring it into focus later
    var error, hasErrors;
    for (var i = 0; i < fields.length; i++) {
        error = hasError(fields[i]);
        if (error) {
            showError(fields[i], error);
            if (!hasErrors) {
                hasErrors = fields[i];
            }
        }
    }

    // If there are errrors, don't submit form and focus on first element with error
    if (hasErrors) {
        event.preventDefault();
        hasErrors.focus();
    }

    // Otherwise, let the form submit normally
    // You could also bolt in an Ajax form submit process here

}, false);

来啊,再来看看在线的现成版本

原文:Form Validation Part 2: The Constraint Validation API (JavaScript)

Last updated