约束验证:html5网页表单的本地客户端验证
译者言:此文翻译自Constraint Validation: Native Client Side Validation for Web Forms
众所周知,验证表单一直有很痛苦的开发体验。实现一种用户体验好、开发体验好且可访问性好的客户端验证是很困难的事情。在HTML5出现之前没有一种本地实现的验证,所以开发者使用了各种各样基于javascript的验证方案。
为了解放开发者,HTML5引入了约束验证的概念,一种本地实现的网页表单验证。
尽管所有主流浏览器的最新版本都支持了这个特性,约束验证目前还仅限于展示和demo的阶段。写这篇文章的目的就是帮助开发了解这个新的api,做出更好的网页表单。
在这个教程中作者会:
- 全面地展示约束验证是什么;
- 发现现今标准和浏览器实现上的限制;
- 讨论现在如何在你的表单中使用约束验证;
约束验证是什么?
约束验证的核心是浏览器在提交表单时判断其是否合法的算法。为了做这个判断,算法利用了几个新的HTML5属性:min、max、step, pattern, 和 required 以及已经存在的 maxlength 和type。
下面的这个例子中Text input使用了一个值为空的 required 属性:
<form> <input type="text" value="" required /></form> |
如果你在支持约束验证的浏览器中提交表单,那么浏览器会阻止表单提交并显示成如下效果:
Chrome 21
Firefox 15
Internet Explorer 10
标准中没有规定浏览器应该如何显示错误信息,这是由浏览器自己约定的。不过标准提供了一套DOM API、新HTML属性和CSS钩子帮助开发者自定义样式。
DOM API
约束验证的API添加了如下属性和方法到DOM节点中。
WILLVALIDATE
willValidate 属性标识了这个DOM节点是否使用约束验证。对于可以提交的元素来说这个属性会被设置成 true 除非因为某些原因被禁止使用约束验证,例如有 disabled 属性时。
<div id="one"></div><input id="two" type="text" /><input id="three" type="text" disabled /><script type="text/javascript"> document.getElementById('one').willValidate; //false document.getElementById('two').willValidate; //true document.getElementById('three').willValidate; //false</script> |
validity
Dom节点的validity 属性会返回一个 ValidityState对象,它包含了一系列与节点相关的布尔属性的验证结果。
customError:true标识是否使用了自定义的验证消息,可以通过setCustomValidity()来设置验证消息;<inputid="foo"type="text"/><inputid="bar"type="text"/><scripttype="text/javascript">document.getElementById('foo').validity.customError; //falsedocument.getElementById('bar').setCustomValidity('Invalid');document.getElementById('bar').validity.customError; //true</script>patternMismatch:true标识节点的value是否匹配它的pattern属性值。<inputid="foo"type="text"value="1234"pattern="[0-9]{4}"/><inputid="bar"type="text"value="ABCD"pattern="[0-9]{4}"/><scripttype="text/javascript">document.getElementById('foo').validity.patternMismatch; //falsedocument.getElementById('bar').validity.patternMismatch; //true</script>rangeOverflow:true标识是否节点的value比它的max属性值还大。<inputid="foo"type="number"value="1"max="2"/><inputid="bar"type="number"value="3"max="2"/><scripttype="text/javascript">document.getElementById('foo').validity.rangeOverflow; //falsedocument.getElementById('bar').validity.rangeOverflow; //true</script>rangeUnderflow:true标识是否节点的value比它的min属性值还小。<inputid="foo"type="number"value="3"min="2"/><inputid="bar"type="number"value="1"min="2"/><scripttype="text/javascript">document.getElementById('foo').validity.rangeUnderflow; //falsedocument.getElementById('bar').validity.rangeUnderflow; //true</script>stepMismatch:true标识是否节点的value与它的step属性值不符合.<inputid="foo"type="number"value="4"step="2"/><inputid="bar"type="number"value="3"step="2"/><scripttype="text/javascript">document.getElementById('foo').validity.stepMismatch; //falsedocument.getElementById('bar').validity.stepMismatch; //true</script>tooLong:true标识是否节点的value长度超过了maxlength属性值。所有的浏览器都会阻止用户输入超过最大长度值的内容。我在这里描述了长度会超过maxlength的情况。typeMismatch:true标识是否input节点的value与type属性值不匹配。<inputid="foo"type="url"value="http://foo.com"/><inputid="bar"type="url"value="foo"/><inputid="foo2"type="email"value="foo@foo.com"/><inputid="bar2"type="email"value="bar"/><scripttype="text/javascript">document.getElementById('foo').validity.typeMismatch; //falsedocument.getElementById('bar').validity.typeMismatch; //truedocument.getElementById('foo2').validity.typeMismatch; //falsedocument.getElementById('bar2').validity.typeMismatch; //true</script>valueMissing:true标识是否节点有required属性却没有值.。<inputid="foo"type="text"value="foo"required /><inputid="bar"type="text"value=""required/><scripttype="text/javascript">document.getElementById('foo').validity.valueMissing; //falsedocument.getElementById('bar').validity.valueMissing; //true</script>valid:true标识是否所有的验证提交都满足,有任意一条不满足则为false。<inputid="valid-1"type="text"value="foo"required /><inputid="valid-2"type="text"value=""required/><scripttype="text/javascript">document.getElementById('valid-1').validity.valid; //truedocument.getElementById('valid-2').validity.valid; //false</script>
VALIDATIONMESSAGE
Dom节点的 validationMessage 属性包含浏览器显示给用户看的错误信息,即验证出错时候的提示信息。
浏览器为这个属性提供了一个默认的本地化信息。如果DOM节点不需要验证或者节点包含正确的内容,那么validationMessage会被设置为空字符串。
注意:在写这篇文章的时候,Opera默认不会填充这个属性。
<input id="foo" type="text" required/><script type="text/javascript"> document.getElementById('foo').validationMessage; //Chrome --> 'Please fill out this field.' //Firefox --> 'Please fill out this field.' //Safari --> 'value missing' //IE10 --> 'This is a required field.' //Opera --> ''</script> |
CHECKVALIDITY()
表单元素节点(例如:input、select、textarea)的checkValidity方法在元素包含正确内容的时候会返回 true。
表单(form)节点的checkValidity方法会在其所有子节点都包含正确内容的时候返回true。
<form id="form-1"> <input id="input-1" type="text" required/></form><form id="form-2"> <input id="input-2" type="text" /></form><script type="text/javascript"> document.getElementById('form-1').checkValidity(); //false document.getElementById('input-1').checkValidity(); //false document.getElementById('form-2').checkValidity(); //true document.getElementById('input-2').checkValidity(); //true</script> |
另外,每次通过checkValidity方法验证表单元素正确性的时候且验证失败,出错节点的invalid事件会被触发。使用上面的样例代码时候,如果你想在id为input-1的节点验证失败时运行一些代码,那么你可以使用如下的代码:
document.getElementById('input-1').addEventListener('invalid', function() { //...}, false); |
虽然没有valid事件,不过你可以使用change事件来通知输入元素的验证改变事件。
document.getElementById('input-1').addEventListener('change', function(event) { if (event.target.validity.valid) { //Field contains valid data. } else { //Field contains invalid data. }}, false); |
SETCUSTOMVALIDITY()
setCustomValidity方法改变了validationMessage属性,也允许你添加自定义的验证规则。
因为它可以设置validationMessage,所以设置其为一个空字符串的时候表明验证正确,设置其他字符串的时候表明验证失败。不幸地是,没有办法设置 validationMessage 的同时,不改变表单元素的验证状态。
例如,如果你有两个密码输入框,你希望强制它们的输入内容一致。那么你可以这么写:
if (document.getElementById('password1').value != document.getElementById('password2').value) { document.getElementById('password1').setCustomValidity('Passwords must match.');} else { document.getElementById('password1').setCustomValidity('');} |
HTML属性
我们已经知道了maxlength, min, max, step, pattern和 type 属性会被浏览器用来约束输入内容。相应地约束验证也有两个附加的属性 novalidate 和 formnovalidate。
NOVALIDATE
novalidate属性会被用在表单节点(form)上。当使用这个属性的时候,就表示表单所有的输入内容在提交时不需要验证了。
<form novalidate> <input type="text" required/> <input type="submit" value="Submit" /></form> |
上例代码中因为有novalidate属性,所以尽管输入为空还是会提交表单。
FORMNOVALIDATE
formnovalidate属性是一个布尔属性,它可以被用在button和input节点上,来阻止提交时验证此节点。例如:
<form> <input type="text" required/> <input type="submit" value="Validate" /> <input type="submit" value="Do NOT Validate" formnovalidate /></form> |
当“Validate”按钮被点击来提交表单的时候,会因为输入为空而被阻止提交。然而,当“Do NOT Validate”按钮被点击来提交表单的时候不会被阻止,因为使用了 formnovalidate 属性。
CSS钩子
在写高质量的表单验证时,错误信息本身很重要,如何以一个好的体验显示错误信息给用户也是很重要的。所以浏览器提供了一个CSS钩子给开发者。
:INVALID AND :VALID
在支持此特性的浏览器中,伪类选择符:valid会匹配表单中通过验证的元素,而伪类选择符:invalid会匹配表单中没有通过验证的元素。
<form> <input id="foo" type="text" required /> <input id="bar" type="text" /></form><script type="text/javascript">// <![CDATA[ document.querySelectorAll('input[type="text"]:invalid'); //Matches input#foo document.querySelectorAll('input[type="text"]:valid'); //Matches input#bar// ]]></script> |
重置默认样式
默认FIrefox会在:invalid 元素上放一个红色的box-shadow,而IE10会显示红色的outline。
Firefox 15
基于WebKit的浏览器和Opera默认不会做任何事情。如果你喜欢保持一致性,那么你可以用如下的方法来重写默认样式:
:invalid { box-shadow: none; /* FF */ outline: 0; /* IE 10 */} |
作者已经提交了一个pull request来讨论如何在normalize.css处理其一致性。
验证气泡
一个更大的差异是浏览器显示错误域的验证气泡外观。遗憾的是,Webkit是唯一一个提供开发者自定义气泡的渲染引擎。在Webkit中,开发者可以使用4个伪元素选择符来自定义气泡样式:
::-webkit-validation-bubble {}::-webkit-validation-bubble-message {}::-webkit-validation-bubble-arrow {}::-webkit-validation-bubble-arrow-clipper {} |
移除默认气泡
因为你只可以在Webkit中修改气泡外观,如果你想要一个跨浏览器的自定义外观,那么只能取消默认的气泡并实现一个自己的样式。以下代码可以关闭一个页面上所有表单的默认验证气泡:
var forms = document.getElementsByTagName('form');for (var i = 0; i < forms.length; i++) { forms[i].addEventListener('invalid', function(e) { e.preventDefault(); //Possibly implement your own here. }, true);} |
如果你取消了默认的气泡,那么确保你会在表单错误的时候将错误显示出来。目前,气泡是浏览器唯一一种用来显示错误的方式。
目前的浏览器实现问题和显示
尽管那些新API为客户端带来了很多验证表单的功能,依旧还有一些限制。
setCustomValidity
通过setCustomValidity方法可以设置validationMessage,但是当表单变得越来越复杂的时候,setCustomValidity的一系列限制变得很明显。
问题#1:处理一个表单域的多种错误
调用一个节点上的 setCustomValidity 简单地重写它的 validationMessage属性。因此,如果你在同一个节点调用 setCustomValidity 两次,第二次会简单地重写第一次。没有一种机制来处理一个数组的错误信息或者是一种显示多个错误信息的方式。
有一种解决方法就是为节点的validationMessage 附加消息,如下:
var foo = document.getElementById('foo');foo.setCustomValidity(foo.validationMessage + ' An error occurred'); |
开发者不可以设置HTML或者是格式化的字符,所以不幸地是串联起来的字符串就会是如下的效果:
问题 #2:知道何时验证表单域
为了说明这个问题,考虑下有两个必须相同的密码输入框的表单:
<form><fieldset><legend>Change Your Password</legend><ul> <li><label for="password1">Password 1:</label> <input id="password1" type="password" required/></li> <li><label for="password2">Password 2:</label> <input id="password2" type="password" required/></li></ul> <input type="submit" /></fieldset></form> |
作者之前的意见是使用 change 事件来实现验证,如下:
var password1 = document.getElementById('password1');var password2 = document.getElementById('password2');var checkPasswordValidity = function() { if (password1.value != password2.value) { password1.setCustomValidity('Passwords must match.'); } else { password1.setCustomValidity(''); }};password1.addEventListener('change', checkPasswordValidity, false);password2.addEventListener('change', checkPasswordValidity, false); |
现在,无论哪个密码输入框的值改变了,用户的验证就会被重新检测。然而,考虑下一个自动填充密码的脚本,甚至是一个改变 pattern, required, min, max, 或step值的脚本。这些可以直接影响到密码的验证结果,同时又没有一种事件来知道这些事情发生。
底线:我们需要一种手段,在表单域验证结果修改的时候执行代码。
问题#3:知道何时用户尝试提交表单
为什么不使用表单的 submit 事件来解决问题呢?submit 事件是不会在浏览器通过所有验证之前触发的。因此没有方法知道何时用户提交了一个被浏览器阻止的表单。
知道用户尝试提交表单行为的发生是非常有必要的。你可能想要显示给用户一系列的错误信息,改变focus或者是显示帮助文字。不幸地是,你需要多做一些工作才能实现它们。
有一种方法可以实现它们,就是为表单节点添加 novalidate 属性,然后使用它的 submit 事件。因为表单的 novalidate 属性会在用户提交时阻止浏览器对数据的验证。因此,客户端脚本不得不在 submit 事件中显示地检查表单是否包含了正确的数据,并且依此来决定是否阻止提交。这里有一个扩展了密码匹配的例子,验证逻辑会在表单提交之前运行。
<form id="passwordForm" novalidate><fieldset><legend>Change Your Password</legend><ul> <li><label for="password1">Password 1:</label> <input id="password1" type="password" required/></li> <li><label for="password2">Password 2:</label> <input id="password2" type="password" required/></li></ul> <input type="submit" /></fieldset></form><script type="text/javascript"> var password1 = document.getElementById('password1'); var password2 = document.getElementById('password2'); var checkPasswordValidity = function() { if (password1.value != password2.value) { password1.setCustomValidity('Passwords must match.'); } else { password1.setCustomValidity(''); } }; password1.addEventListener('change', checkPasswordValidity, false); password2.addEventListener('change', checkPasswordValidity, false); var form = document.getElementById('passwordForm'); form.addEventListener('submit', function() { checkPasswordValidity(); if (!this.checkValidity()) { event.preventDefault(); //Implement you own means of displaying error messages to the user here. password1.focus(); } }, false);</script> |
这个方法的主要缺点是为表单添加了 novalidate 属性来阻止浏览器显示验证气泡给用户。因此如果你使用了这个技术,你必须实现自己的展示错误信息的方法。这里有一个简单的例子 展示了如何完成这一方法。
底线:我们需要一个 forminvalid 事件,它会在表单提交之前触发,不论表单中的数据是否正确。
Safari
即使是Safari支持约束验证API的情况下,在写这篇文章的时候(版本 6)Safari也不会因为表单的数据没有满足约束验证而阻止用户提交。对使用Safari的用户来说,它和其他不支持约束验证的浏览器没有区别。
最简单的解决这个问题的方法就和之前提到的一样,给所有的表单添加novalidate属性,并且使用preventDefault阻止表单提交。代码如下:
var forms = document.getElementsByTagName('form');for (var i = 0; i < forms.length; i++) { //Add the novalidate attribute. forms[i].setAttribute('novalidate', true); forms[i].addEventListener('submit', function(event) { //Prevent submission if checkValidity on the form returns false. if (!event.target.checkValidity()) { event.preventDefault(); //Implement you own means of displaying error messages to the user here. } }, false);} |
注意:现在已经有不少记录在案的bug, checkValidity 会返回错误的结果(样例1 和 样例2)。误报对上面的情况来说是特别严重的,因为用户会被困在表单验证的步骤。
显示地设置错误信息
虽然开发者可以通过setCustomValidity来设置validationMessage,但是当表单很大的时候用javascriot来设置各个表单域错误信息的开发体验很不友好。
为了让这个开发过程变的更容易,Firefox引入了一个自定义的 x-moz-errormessage 属性,它可以被用来自动设置域的 validationMessage。
<form> <input type="text" required x-moz-errormessage="Fill this out."/> <input type="submit" value="Submit" /></form> |
以上的代码在Firefox中提交时得到的结果如下:
这个特性已经被提交到了 W3C ,但是被拒绝了。因此,目前Firefox是唯一一个可以显示地自定义错误信息的浏览器。
Title属性
虽然Title属性不会改变 validationMessage,但是在patternMismatch且有title值的情况下,浏览器会把 title 属性值显示在行内气泡上。(注意:只要开发者设置了 title 值,那么Chrome会在任何出错情况下显示 title 属性值,而不仅仅是 patternMismatches 的情况。)
<form> <label for="price">Price: $</label> <input id="price" pattern="[0-9].[0-9][0-9]" title="Please enter the price in x.xx format (e.g. 3.99)" type="text" value="3" /> <input type="submit" value="Submit" /></form> |
执行的结果如下:
Chrome 21
:invalid 和 :valid
和之前讨论的一样,伪类选择符 :valid 会匹配通过了约束验证的表单元素,伪类选择符:invalid 会匹配未通过了约束验证的表单元素。不幸地是这些伪类选择符会被自动匹配,在表单被提交之前甚至是表单可以与用户交互之前就已经匹配了。看如下例子:
<style> :invalid { border: 1px solid red; } :valid { border: 1px solid green; }</style><form> <input type="text" required/> <input type="text" /></form> |
demo实现的效果是为出错域加一个红色边框,为正确域添加一个绿色边框。这个效果在表单被绘制的时候已经加上了。然而,行内表单验证的可用性测试 已经表明给用户提示的最好时机是在他们域表单域发生交互之后,不是之前。
有一种方法可以实现以上说的效果,就是在输入框产生交互之后添加一个class。并且只在有这个class的时候加上边框。
<style> .interacted:invalid { border: 1px solid red; } .interacted:valid { border: 1px solid green; }</style><form> <input type="text" required/> <input type="text" /> <input type="submit" /></form><script type="text/javascript"> var inputs = document.querySelectorAll('input[type=text]'); for (var i = 0; i < inputs.length; i++) { inputs[i].addEventListener('blur', function(event) { event.target.classList.add('interacted'); }, false); }</script> |
Firefox已经看到了这些问题并实现了两个附加的伪类选择符::moz-ui-invalid 和 :moz-ui-valid。不像:invalid 和 :valid, :moz-ui-invalid 和 :moz-ui-valid 直到表单域被修改或是表单提交(且遇到出错的表单域)之前不会匹配表单域。
最后注意,在且仅在Firefox中 :valid 和 :invalid 伪类选择符会不仅会匹配表单元素还会匹配<form> 节点。所以在添加全局的CSS规则时要注意。
不支持的浏览器
虽然支持约束验证的浏览器很棒,但是还是有一些主要浏览器没有支持。例如IE
处理不支持的浏览器
如果你尝试在线上产品中使用新的约束验证API,你不得不让不支持的浏览器也能正常使用。以作者的经验来看,主要有两种选择:
选择1:仅仅依赖服务器端验证
记住即使是有了新的API,客户端验证也没办法代替服务器端验证的地位。有些不怀好意的使用者可以很简单的绕过客户端的验证,并且HTTP请求不一定会来自于浏览器。
因此客户端验证应该只被当作是一种用户体验的增强手段;所有的表单都应该能正常使用,即使在客户端无法显示验证信息的情况下。
既然已经有了服务器短的验证,只需要服务器端返回一个合理的错误信息并显示给终端用户。开发者只需要为不支持的浏览器做降级处理即可。
选择2:优雅降级
虽然依赖服务器端验证对一些应用是合理的措施,但在不支持的浏览器中抛弃有可用性优势的客户端验证不是一种好的选择。
如果你想要用新的API并且想要让它在任何地方都能运行,那么最好的方法是使用优雅降级。为了让API在不支持的浏览器上运行,有一些降级库使用了额外的步骤并且解决了一些浏览器本地实现的issues。
优雅降级
这里有一系列的降级实现的库允许你在任何浏览器上使用约束验证API。作者在这里讨论了两种流行的选择。
Webshims
Webshims 是一系列的降级库,它包含了HTML5表单和约束验证API。
为了展示如何使用Webshims,让我们回到最初的样例中:一个有 required 属性的文字输入框。
<form> <input type="text" required value="" /> <input type="submit" value="Submit" /></form> |
为了让以上代码在所有浏览器中保持一致性,你需要引入 Webshims 和它的依赖。然后调用$.webshims.polyfill('forms') 方法。
<!-- Webshims' dependencies --><script type="text/javascript" src="js/jquery-1.8.2.js"></script><script type="text/javascript" src="js/modernizr-yepnope-custom.js"></script><!-- Webshims base --><script type="text/javascript" src="js-webshim/minified/polyfiller.js"></script><script type="text/javascript">// <![CDATA[jQuery.webshims.polyfill('forms');// ]]></script><form> <input type="text" value="" required /> <input type="submit" value="Submit" /></form> |
在支持的浏览器中结果不会有区别。不支持约束验证的浏览器也会显示不符合验证的错误信息、阻止不符合的表单提交。例如以下是Safari和IE8的显示结果:
Safari 6
为了实现优雅降级,Webshims也提供针对之前讨论过的那些约束验证限制的解决方案:
- 通过
data-errormessage属性显示的设置错误信息; - 提供了
form-ui-valid和form-ui-invalid两个class,它们与:moz-ui-valid和:moz-ui-invalid的功能一样; - 提供了
firstinvalid,lastinvalid,changedvalid, 和changedinvalid事件; - 解决了 WebKit 在表单提交时候的误报bug;
更多的信息去Webshims提供的文档看 HTML5 forms docs。
H5F
H5F是一个轻量级没有依赖的降级库,实现了所有的约束验证API和一系列的新属性。
让我们通过之前的例子来看看H5F是如何使用的:
<script type="text/javascript" src="H5F.js"></script><form> <input type="text" value="" required/> <input type="submit" value="Submit" /></form><script type="text/javascript"> H5F.setup(document.getElementsByTagName('form'));</script> |
H5F 会在所有的浏览器中阻止提交表单,但是用户只会在支持的浏览器中看到验证气泡。既然H5F提供了完全的约束验证API,开发者可以使用它来实现自己的UI,并且做任何想要事情。
如果你想找一些样例来了解它的API,H5F的样例页面中实现了在输入框右侧显示信息。作者也有一个样例,展示了如何在表单顶部 的列表中显示所有的错误信息。
为了实现约束验证API的优雅降级,H5F也提供了模仿 :moz-ui-valid 和 :moz-ui-invalid的类。默认那些类是 valid 和 error 。同时它们也可以通过为 H5F.setup 方法设置第二参数来自定义。
H5F.setup(document.getElementById('foo'), { validClass: 'valid', invalidClass: 'invalid'}); |
更多的信息可以在H5F的github页面上获得。
结论
HTML5的约束验证API使得在客户端添加验证变得快捷的同时还提供了Javascript API和CSS钩子来自定义。
尽管还是有很多浏览器实现和旧浏览器兼容的问题存在,配合好的降级库或是服务器端验证,开发者就可以在自己的表单中使用这些API。
译者言:看完之后觉得约束验证的API设计的真心不好用。。。
Posted in javascript, Other, UI
Tagged :invalid, :valid, Constraint Validation, H5F, html5, javascript,Webshims, 约束验证
posted on 2013-09-03 15:18 master2012 阅读(580) 评论(0) 收藏 举报
















浙公网安备 33010602011771号