由浅入深剖析Angular表单验证
最近手上维护的组件剩下的bug都是表单验证,而且公司的表单验证那块代码经历的几代人,里面的逻辑开始变得不清晰,而且代码结构不是很angular。
是很有必要深入了解表单验证。
<body ng-controller="maincontroller"> <form name="form" novalidate="novalidate"> <input name="text" type="email" ng-model="name"> </form> </body>
ngmodel是angular的黑魔法,实现双向绑定,当name的值变化的时候,input的value也会跟着变化。
当用户在input修改value的时候,name的值也会跟着变化。
novalidate="novalidate"的目的是去除系统自带的表单验证。
上面那段代码解析完,angular会在maincontroller的$scope下面生成一个变量"form",$scope.form,这个变量的名称跟html中form.name一致。
而$scope.form.text为文本输入框的model,继承自ngmodelcontroller。
其中$scope.form实例自formcontroller。其内容为:
文本输入框的model(也就是$scope.form.text)为:
其中$dirty/$pristine,$valid/$invalid,$error为常用属性。尤其是$error。
最简单的表单验证:
了解了form和输入框,就可以先撸个最简单的显示错误的指令。
html内容如下:
<form name="form" novalidate="novalidate"> <input name="text" type="email" ng-model="name" error-tip> </form>
指令代码如下:
// 当输入框出错,就显示错误 directive("errortip",function($compile){ return { restrict:"a", require:"ngmodel", link:function($scope,$element,$attrs,$ngmodel){ //创建子scope var subscope = $scope.$new(), //错误标签的字符串,有错误的时候,显示错误内容 tip = '<span ng-if="haserror()">{{errors() | json}}</span>'; //脏,而且无效,当然属于错误了 $scope.haserror = function(){ return $ngmodel.$invalid && $ngmodel.$dirty; } //放回ngmodel的错误内容,其实就是一个对象{email:true,xxx:true,xxxx:trie} $scope.errors = function(){ return $ngmodel.$error; } //编译错误的指令,放到输入框后面 $element.after($compile(tip)(subscope)); } } });
先看看执行结果:
输入无效的邮箱地址的时候:
输入正确的邮箱地址的时候:
errortip指令一开始通过 require:"ngmodel" 获取ngmodelcontroller。然后创建用于显示错误的元素到输入框。
这里使用了$compile,$compile用于动态编译显示html内容的。
当有错误内容的时候,错误的元素就会显示。
为什么subscope可以访问haserror和errors方法?
因为原型链。看下图就知道了。
自定义错误内容
好了,很明显现在的表单验证是不能投入使用的,我们必须自定义显示的错误内容,而且要显示的错误不仅仅只有一个。
显示多个错误使用ng-repeat即可,也就是把"errortip"指令中的
tip = '<span ng-if="haserror()">{{errors() | json}}</span>';
改成:
tip = '<ul ng-if="haserror()" ng-repeat="(errorkey,errorvalue) in errors()">' + '<span ng-if="errorvalue">{{errorkey | errorfilter}}</span>' + '</ul>';
其中errorfilter是一个过滤器,用于自定义显示错误信息的。过滤器其实是个函数。
其代码如下:
.filter("errorfilter",function(){ return function(input){ var errormessagesmap = { email:"请输入正确的邮箱地址", xxoo:"少儿不宜" } return errormessagesmap[input]; } });
结果如下:
好了,到这里就能够处理“简单”的表单验证了。对,简单的。我们还必须继续深入。
自定义表单验证!
那我们就来实现一个不能输入“帅哥”的表单验证吧。
指令如下:
.directive("donotinputhandsomeboy",function($compile){ return { restrict:"a", require:"ngmodel", link:function($scope,$element,$attrs,$ngmodel){ $ngmodel.$parsers.push(function(value){ if(value === "帅哥"){ //设置handsome为无效,设置它为无效之后,$error就会变成{handsome:true} $ngmodel.$setvalidity("handsome",false); } return value; }) } } })
结果如下:
这里有两个关键的东西,$ngmodel.$parsers和$ngmodel.$setvalidity.
$ngmodel.$parsers是一个数组,当在输入框输入内容的时候,都会遍历并执行$parsers里面的函数。
$ngmodel.$setvalidity("handsome",false);设置handsome为无效,会设置$ngmodel.$error["handsome"] = true;
也会设置delete $ngmodel.$$success["handsome"],具体可以翻翻源码。
这里我总结一下流程。
-->用户输入
-->angular执行所有$parsers中的函数
-->遇到$setvalidity("xxoo",false);那么就会把xxoo当做一个key设置到$ngmodel.$error["xxoo"]
-->然后errortip指令会ng-repeat $ngmodel.$error
-->errorfilter会对错误信息转义
-->最后显示错误的信息
自定义输入框的显示内容
很多时候开发,不是简简单单验证错误显示错误那么简单。有些时候我们要格式化输入框的内容。
例如,"1000"显示成"1,000"
"hello"显示成"hello"
现在让我们实现自动首字母大写。
源码如下:
<form name="form" novalidate="novalidate"> <input name="text" type="text" ng-model="name" upper-case> </form> .directive("uppercase",function(){ return { restrict:"a", require:"ngmodel", link:function($scope,$element,$attrs,$ngmodel){ $ngmodel.$parsers.push(function(value){ var viewvalue; if(angular.isundefined(value)){ viewvalue = ""; }else{ viewvalue = "" + value; } viewvalue = viewvalue[0].touppercase() + viewvalue.substring(1); //设置界面内容 $ngmodel.$setviewvalue(viewvalue); //渲染到界面上,这个函数很重要 $ngmodel.$render(); return value; }) } } });
这里我们使用了$setviewvalue和$render,$setviewvalue设置viewvalue为指定的值,$render把viewvalue显示到界面上。
很多人以为使用了$setviewvalue就能更新界面了,没有使用$render,最后不管怎么搞,界面都没刷新。
如果只使用了$ngmodel.$parsers是不够的,$parsers只在用户在输入框输入新内容的时候触发,还有一种情况是需要重新刷新输入框的内容的:
那就是双向绑定,例如刚才的输入框绑定的是maincontroller中的$scope.name,当用户通过其他方式把$scope.name改成"hello",输入框中看不到首字母大写。
这时候就要使用$formatters,还是先看个例子吧.
<body ng-controller="maincontroller"> <form name="form" novalidate="novalidate"> <button ng-click="random()">随机</button> <input name="text" type="text" ng-model="name" upper-case> </form> </body>
maincontroller的内容:
angular.module("app", []) .controller("maincontroller", function ($scope, $timeout) { $scope.random = function(){ $scope.name = "hello" + math.random(); } })
够简单吧,点击按钮的时候,$scope.name变成hello开头的随机内容.
很明显,hello的首字母没大写,不是我们想要的内容。
我们修改下指令的内容:
.directive("uppercase",function(){ return { restrict:"a", require:"ngmodel", link:function($scope,$element,$attrs,$ngmodel){ $ngmodel.$parsers.push(function(value){ var viewvalue = uppercasefirstword(handleemptyvalue(value)); //设置界面内容 $ngmodel.$setviewvalue(viewvalue); //渲染到界面上,这个函数很重要 $ngmodel.$render(); return value; }) //当过外部设置modelvalue的时候,会自动调用$formatters里面函数 $ngmodel.$formatters.push(function(value){ return uppercasefirstword(handleemptyvalue(value)); }) //防止undefined,把所有的内容转换成字符串 function handleemptyvalue(value){ return angular.isundefined(value) ? "" : "" + value; } //首字母大写 function uppercasefirstword(value){ return value.length > 0 ? value[0].touppercase() + value.substring(1) : ""; } } } });
总结一下:
1.
-->用户在输入框输入内容
-->angular遍历$ngmodel.$parsers里面的函数转换输入的内容,然后设置到$ngmodel.$modelvalue
-->在$ngmodel.$parsers数组中的函数里,我们修改了$ngmodel.$viewvalue,然后$ngmode.$render()渲染内容。
2.
-->通过按钮生成随机的字符串设置到name
-->每次脏检测都会判断name的值是否跟$ngmodel.$modelvalue不一致(这里是使用$watch实现的),不一致就反序遍历$formaters里面的所有函数并执行,把最终返回值赋值到$ngmodel.$viewvalue
-->刷新输入框内容
“自定义输入框的显示内容”的例子能不能优化?
为什么要优化?
原因很简单,为了实现“自定义内容”,使用了$parsers和$formatters,其实两者的内容很像!这一点很关键。
怎么优化?
使用$ngmodel.$validators。
好,现在把例子再改一下。
.directive("uppercase",function(){ return { restrict:"a", require:"ngmodel", link:function($scope,$element,$attrs,$ngmodel){ //1.3才支持,不管手动输入还是通过其他地方更新modelvalue,都会执行这里 $ngmodel.$validators.uppercase = function(modelvalue,viewvalue){ var viewvalue = uppercasefirstword(handleemptyvalue(modelvalue)); //设置界面内容 $ngmodel.$setviewvalue(viewvalue); //渲染到界面上,这个函数很重要 $ngmodel.$render(); //返回true,表示验证通过,在这里是没啥意义 return true; } //防止undefined,把所有的内容转换成字符串 function handleemptyvalue(value){ return angular.isundefined(value) ? "" : "" + value; } //首字母大写 function uppercasefirstword(value){ return value.length > 0 ? value[0].touppercase() + value.substring(1) : ""; } } } })
代码简洁了很多,$ngmodel.$validators在1.3以上的版本才支持。
$ngmodel.$validators.uppercase函数的返回值如果是false,那么$ngmodel.$error['uppercase']=true。这一点跟$ngmodel.$setvalidity("uppercase",false)差不多。