年底了越來越懶散,AngularJs的學習落了一段時間,博客最近也沒更新。慚愧~前段時間有試了一下用yeoman構建Angular項目,感覺學的差不多了想做個項目練練手,誰知遇到了一系列問題。yeoman是基於node.js的一套工具包,由於我一直在windows下編程,而且node.js對於windows環境的支持也在慢慢加強,所以想嘗試在windows下用yeoman跟搭建一個項目。過程遠比想象的坎坷多了,各種報錯,各種搜資料解決問題,最終還是無法解決一些編譯出錯,以失敗告終,轉戰Linux。在此也提醒大家如果想在windows下使用yeoman,還是謹慎為好!
今天來學習一下一直被我忽視掉的表單驗證。ng的強項是開發CRUD應用,也就是與用戶操作多、交互比較頻繁的應用。表單是與用戶交互的一個重要角色,所以萬萬不能忽視。在學習之后發現這部分知識不僅僅是想象中的那么簡單,比起其他特性,我們一直不怎么重視的表單驗證,其實也可以做的很簡單,而且易維護。下面就開始吧~
ng中的表單與Controller
看這個小標題也行你會差異,表單驗證,怎么跟controller扯上關系了。ng中的form已經不同於我們平時用的form標簽,做了增強。form是FormController的一個實例。如何理解這句話呢?想想我們使用ng-controller指令的情景:
<div ng-controller="testC"> <input type="test" ng-model="a" /> </div> <scritp> function testC($scope){ //............. } </script>
應用了ng-controller的div就是testC的一個實例,我們可以在模板中使用定義在$scopt上的任何屬性和方法,而testC的定義也是由我們自己實現的。當我們使用<form>的時候也是這樣的道理,FormController由ng為我們定義好了,有一系列屬性和方法提供給我們完成驗證工作,form實例通過name屬性來進行標識,我們可以通過此標識來訪問form實例的屬性和方法,如:
<form name="myform"> {{myform.$valid}} </form>
form提供的屬性都是用來表示表單的驗證狀態的,包括:$pristine(表單沒有填寫記錄)、$dirty(表單有填寫記錄)、$valid(通過驗證)、$invalid(未通過驗證)、$error(驗證錯誤信息)。除$error外,前四個的值為true或false表示相應的狀態。$error的值為一個js對象,包含了以下驗證內容的狀態:
email
max
maxlength
min
minlength
number
pattern
required
url
這些內容我們會在稍后的例子中看到。FormController還提供了一些方法,我們一般不手工調用它們,都是系統自己調用。可參考官方文檔:http://docs.angularjs.org/api/ng.directive:form.FormController
表單元素,如input、checkbox、radio等也不是普通的表單元素了,它們通通是NgModelController的實例。與form一樣,也是通過name屬性來標識。FormController擁有的那五個屬性,NgModelController也同樣擁有,除此之外,還有許多額外的屬性和方法,我們稍后也在示例中展示,可參考官方文檔:http://docs.angularjs.org/api/ng.directive:ngModel.NgModelController
還有一個特性需要了解,一個表單中的表單元素,會作為這個form的屬性自動加在上面,通過name標識就可以訪問到,如:
<form name="myform"> <input type="text" name="myname" /> {{myform.myname.$valid}} </form>
ng內置的驗證規則
ng框架提供了非常方便的驗證機制,你只需要在標簽上加點指令,像使用HTML5提供的驗證那樣,然后在css中根據規則定義好正確/錯誤的樣式就OK了,例如我們要讓一個文本框為必填項,使用required:
<form name="myform novalidate> <input type="text" ng-model="a" required /> </form>
有幾點需要注意:
- 在<form>上加了一個novalidate,用來禁止掉瀏覽器默認的驗證行為,因為ng已經對HTML5的幾種表單新特性做了兼容處理。
- 表單元素必須有ng-model,否則無法觸發驗證
- 在css中分別定義.ng-pristine、.ng-valid、.ng-invalid、.ng-dirty這四種樣式,ng會根據相應的狀態自動加上樣式。
這部分還是相當簡單的,下面我們寫例子來測一下這幾種驗證機制,HTML代碼如下:
<div ng-app="MyApp"> <div ng-controller="testC"> <form name="myform" novalidate> required: <input type="text" name="test1" ng-model="test1" required><br /> ng-minlength(3): <input type="text" name="test2" ng-model="test2" ng-minlength="3"><br /> ng-maxlength(10): <input type="text" name="test3" ng-model="test3" ng-maxlength="10"><br /> ng-pattern(/[a-f]/): <input type="text" name="test4" ng-model="test4" ng-pattern="/[a-f]/"><br /> type="number"(2-8): <input type="number" name="test5" max="8" min="2" ng-model="test5"><br /> type="url": <input type="url" name="test6" ng-model="test6"><br /> type="email": <input type="email" name="test7" ng-model="test7"><br /> </form> <div> <h2>表單驗證結果:</h2> myform.$invalid : {{myform.$invalid}}<br /> myform.$valid : {{myform.$valid}}<br /> myform.$pristine : {{myform.$pristine}}<br /> myform.$dirty : {{myform.$dirty}}<br /> myform.$error : {{myform.$error}}<br /> <h2>表單項驗證結果</h2> required:<br /> myform.test1.$invalid : {{myform.test1.$invalid}}<br /> myform.test1.$valid : {{myform.test1.$valid}}<br /> myform.test1.$pristine : {{myform.test1.$pristine}}<br /> myform.test1.$dirty : {{myform.test1.$dirty}}<br /> myform.test1.$error : {{myform.test1.$error}}<br /> myform.test2.$error : {{myform.test2.$error}}<br /> </div> </div> </div>
CSS代碼,為不同的狀態設置不同的背景色:
input.ng-pristine { background-color: white; } input.ng-dirty { background-color: lightyellow; } input.ng-valid { background-color: lightgreen; } input.ng-invalid { background-color: pink; }
js代碼,進行controller的初始化:
var app = angular.module('MyApp',[]); app.controller('testC',function($scope){ $scope.test1=''; $scope.test2=''; $scope.test3=''; $scope.test4=''; $scope.test5=''; $scope.test6=''; $scope.test7=''; });
結果如下:
該示例編寫在runjs上,點擊查看http://runjs.cn/code/gspvlfrw
在上面的代碼中,你也看到了我從FormConroller實例myform訪問到的屬性,還有從NgModelController訪問到的屬性。這些屬性是非常有用的,比如你可以給表單的按鈕加上:ng-disabled="myform.$invalid",這樣在表單未通過驗證的時候,提交按鈕始終是不可點的。另外也可以根據表單元素的這些屬性,來控制具體的錯誤提示信息,比如郵箱輸錯了,讓"請輸入正確的郵箱“這行字顯示出來,如果你順着我的思路,應該立馬能想象到。
自定義驗證規則
除了內置的這些驗證規則,你還可以自己定義。方法就是寫一個指令,加在表單元素上。聽起來好簡單的樣子,但是這指令與一般的指令可不同,我們需要按一定的規則來寫,這樣才可以融入ng的驗證機制,讓你自定義的跟內置的一樣可以便捷的工作和管理。這個時候,NgModelController提供的方法就派上用場了。我們從一個例子來開始吧。
我想讓我的輸入框只允許輸入偶數,我們來定義一個名為even-num的指令,在頁面上使用的時候像這樣:
<input type="number" ng-model="test1" even-num />
完整的js代碼如下:
var app = angular.module('MyApp',[]); app.controller('testC',function($scope){ $scope.test1 = ''; }); app.directive('evenNum',function(){ return { require: 'ngModel', link: function(scope, elm, attrs, ctrl) { ctrl.$parsers.push(function(viewValue) { if (viewValue % 2 == 0) { ctrl.$setValidity('evenNum', true); return viewValue; } else { ctrl.$setValidity('evenNum', false); return viewValue; } }); } }; });
運行結果:
上面的例子編寫在runjs上,點擊查看http://runjs.cn/code/gdq8m0gb
現在來解釋一下上面的代碼。自定義指令的方式如果你不熟悉,可以先看一下我之前寫的自定義指令部分。因為我們的指令要依賴NgModelController,所以寫上了require:'ngModel',注意書寫方式。 另外在link函數中,通過ctrl引用到了我們注入的NgModelController,然后向它的$parsers屬性中push了一個函數進去。這個$parsers是什么東西呢?很明顯它是一個數組,因為我們可以push東西進去。在解釋之前,我們先清楚兩個概念:我們把模板中的數據,像{{aa}}這樣的,叫做viewValue,故名思義,視圖中的數據。我們把模型/controller中的數據,叫做modelValue。ng所說的雙向綁定,就是把這兩者進行綁定。這個$parsers保存了從viewValue向modelValue綁定過程中的處理函數,它們將來會依次執行。因為我們的驗證是從用戶輸入開始,即view發生了變化,所以我們的驗證邏輯就加在這里。在驗證結果中,我們調用ctrl.$setValidity方法,將結果保存,這樣框架就能完成接下來的一系列工作。
與$parsers相對的,還有一個屬性叫$formatters,它保存的是從modelValue向viewValue綁定過程中的處理函數。那我們定義的這個驗證函數,要不要也push進$formatters里去呢?這取決於你的需要。如果你對兩者的區別還不太清楚,看了下面這個例子就明白了:
上面的例子編寫在runjs上,點擊查看http://runjs.cn/code/9vde2r0w
兩個input的ng-model指向的是同一個,所以數據會同步變化,但是$formatters里沒有push進去驗證函數,所以在從modelValue向ViewValue綁定的過程中,副本並沒有進行驗證。如果把驗證函數push進$formatters,那么副本也會跟着驗證了。
自定義表單元素
我們都知道,在表單元素上使用ng-model可以進行雙向綁定。但是雙向綁定只能用於input、checkbox這些標准表單控件上,你給一個div加ng-model是不能雙向綁定的,因為系統不知道該如何綁定。所以話說過來,要想給非標准表單控件雙向綁定,代碼還得自己來寫,說白了就是自定義一個指令。其實這部分內容放在自定義指令中也是合適的,但是官網在這里提到了,我也來介紹一下。
我們直接從例子開始,大家一定見過自適應的文本區域,就是隨着輸入內容的增加,會自動變高的textarea。用<textarea>標簽做的話,需要加js代碼才可以實現。更好的是純css的方案,使用HTML5的新屬性contenteditable,用一個div來模擬文本區域,div的高度默認就是自適應的,正好可以滿足需求。基本的HTML代碼和css代碼如下:
<style> .smarttextarea{ width: 400px; min-height: 100px; max-height: 400px; border: 1px solid; overflow: auto; padding:5px 10px 20px; } </style> <div contenteditable=”true" class="smarttextarea"></div>
我們現在要做的就是,讓這個模擬出來的文本區域跟真正的textarea那樣,可以進行數據的雙向綁定,這樣就可以進行驗證了。我定義了一個名為smarttextarea的指令,使用起來像這樣:
<smarttextarea contenteditable="true" class="smarttextarea" ng-model="test3" required></smarttextarea>
指令的定義如下:
app.directive('smarttextarea',function(){ var link = function(scope, elm, attrs, ctrl) { //view=>model數據綁定 elm.bind('keyup', function() { scope.$apply(function() { ctrl.$setViewValue(elm.html()); }); }); //model=>view數據綁定 ctrl.$render = function() { elm.html(ctrl.$viewValue); }; ctrl.$setViewValue(elm.html()); }; return { template : '<div></div>', replace : true, require: 'ngModel', restrict: 'E', link : link }; });
看一下效果:
上面的例子編寫在runjs上,點擊查看http://runjs.cn/code/lhysp5vh
我給模擬出來的textarea加了required驗證,可以發現生效了。其實關鍵代碼就是進行了數據的雙向綁定處理,包括兩步:
- 從view向model綁定,監聽keyup事件,然后調用ctrl.$setViewValue方法把viewValue保存下來
- 從model想view綁定,調用ctrl.$render方法,將viewValue渲染到頁面上
經過這兩步,我們就自定義了一個跟標准表單控件一樣的元素,可以進行數據的雙向綁定,表單驗證通通沒有問題。