form表单验证

自定义表单验证

我们知道原生的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';

};

来看看效果
如果该字段是单选按钮或复选框,我们需要更改我们将错误消息添加到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);

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容