年底了越來越懶散,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渲染到頁面上
經過這兩步,我們就自定義了一個跟標準表單控制元件一樣的元素,可以進行資料的雙向繫結,表單驗證通通沒有問題。