最近手上維護的組件剩下的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){//創建子scopevar 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)差不多。