走進AngularJs(九)表單及表單驗證

呂大豹發表於2014-01-26

  年底了越來越懶散,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>

  有幾點需要注意:

  1. 在<form>上加了一個novalidate,用來禁止掉瀏覽器預設的驗證行為,因為ng已經對HTML5的幾種表單新特性做了相容處理。
  2. 表單元素必須有ng-model,否則無法觸發驗證
  3. 在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驗證,可以發現生效了。其實關鍵程式碼就是進行了資料的雙向繫結處理,包括兩步:

  1. 從view向model繫結,監聽keyup事件,然後呼叫ctrl.$setViewValue方法把viewValue儲存下來
  2. 從model想view繫結,呼叫ctrl.$render方法,將viewValue渲染到頁面上

  經過這兩步,我們就自定義了一個跟標準表單控制元件一樣的元素,可以進行資料的雙向繫結,表單驗證通通沒有問題。

相關文章