從淺入深剖析angular表單驗證

最近手上維護的組件剩下的BUG都是表單驗證,並且公司的表單驗證那塊代碼經歷的幾代人,裏面的邏輯開始變得不清晰,並且代碼結構不是很angular。html

是頗有必要深刻了解表單驗證。json

 

入門以前,我以爲應該先了解angular內置的表單驗證有哪些:數組

1,必填項app

驗證某個表單是否已經填寫,只要在元素上標記required便可:dom

<input type="text"  required>函數

 

2,最小長度優化

驗證表單輸入框的內容是否大於某個最小值。ui

<input ng-minlength="5">url

 

3,最大長度spa

驗證表單輸入框的內容是否小於某個最大值、

<input ng-maxlength="5">

 

4,匹配正則

確保輸入的內容匹配某個正則。

<input ng-pattern="[a-zA-Z]">

 

5,電子郵件

驗證內容是不是電子郵件。

<input type="email">

 

6,數字

驗證輸入內容是不是數字.

<input type="number">

 

7,URL

驗證輸入內容是不是URL。

<input type="url">

 

瞭解內置的表單驗證的頗有必要的,能夠避免重複開發。

 

接着就能夠看看最簡單的表單驗證例子。

 

<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內容的。具體原理能夠看這裏:http://www.cnblogs.com/accordion/p/5156553.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)差很少。

 

 

 

內容不完整的話,請拍磚,我繼續加。

晚點補上源碼剖析部分。

相關文章
相關標籤/搜索