其實我沒有那麼喜歡寫程式

Validation: The Angular Way(自訂驗證Directive)

Angular除了提供HTML5 input type(email、url、number)驗證,也提供一些驗證的directive(ng-pattern、ng-maxlength、ng-minlength、required)。

但這些驗證可能無法滿足你所有的需求,所以你必須自行擴充。 比較偷懶的做法是有些人會直接在controller裡做判斷,這樣不但會讓controller越來越肥,而且你所寫的驗證並不能重複利用,萬一別的地方也需要用到相同規則的驗證的話怎麼辦?

其實擴充驗證的directive沒有想像中的複雜,只要熟悉NgModelController裡的方法就可以幫我們自訂驗證狀態。

NgModelController

在擴充directive前我們必須先稍微了解一下NgModelController的成員

$setValidity(validationErrorKey, isValid)

$setValidity能讓我們設定model的驗證狀態,它的第一個參數為string,代表驗證的類型,第二個參數為bool,表示此驗證是否通過。 $setValidity('required' false)會將required類型的驗證設為invalid。

$formatters

$formatters是一個方法陣列,每當你的model改變時,就會依序執行$formatters裡的所有方法,而每個方法都會接到一個來自上一個方法回傳的參數,最後一個回傳的值將會是view的值(其實還會在執行$render方法)。 這讓你有機會可以改變 Model To View 的值(例如轉成大寫)或改變驗證狀態。

$parsers

$parsers類似$formatters,一樣也是一組方法陣列,不同$formatters的是,它是當view的值改變時(例如更改input的值)的時候執行$parsers所有的方法,最後回傳的值將會設為model的值。這讓你有機會可以改變 View To Model 的值(例如轉成大寫)或改變驗證狀態,不過要注意得是若驗證沒通過建議回傳undefined(你不會希望驗證不過的值還給model,況且angularjs原生的驗證directive也都是這個規則)。

以下範例我們會擴充一個directive,它會驗證所輸入的值是否為整數或浮點數,注意我們使用unshift方法加入$parsers陣列,因為我們想要這個方法可以先被執行(比所有原生的驗證都還要早),所以把它插入在陣列最前面。

script
var app = angular.module('myApp', []);

app.directive('float', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      var validator = function(value){
        if ( /^\-?\d+(\.\d+)?$/.test(viewValue)) {
          ctrl.$setValidity('float', true);
          return parseFloat(viewValue.replace('.'));
        } else {
          ctrl.$setValidity('float', false);
          return undefined;
        }
      };
    
      ctrl.$parsers.unshift(validator);
      ctrl.$formatters.unshift(validator);
    }
  };
html
<form name="myForm" class="form-horizontal" novalidate ng-submit="logOn()">
    <input name="float" type="text" class="form-control" ng-model="float" float>
    <span ng-show="myForm.float.$error.float && myForm.float.$dirty">
        請輸入浮點數
    </span>
    <input type="submit" ng-disabled="myForm.$invalid" />
</form>

但總會有問題....
請看以下的範例,我在float這個input增加一個required驗證,結果....

html
<form name="myForm" class="form-horizontal" novalidate ng-submit="logOn()">
    <input name="float" type="text" class="form-control" ng-model="float" float required>
    <div ng-show="myForm.float.$error.float && myForm.float.$dirty">
        請輸入浮點數
    </div>
    <div ng-show="myForm.float.$error.required && myForm.float.$dirty">
        必填欄位
    </div>
    <input type="submit" ng-disabled="myForm.$invalid" />
</form>

明明就有輸入值,為何還是顯示必填欄位未填?
那是因為我們將float是用$parsers.unshift()加入$parsers的最前面($formatters也是),所以當float驗證沒過,就會回傳undefined,那接著下來的required驗證方法就會接到undefined,那當然驗證就不會過。 所以如果您希望float在required之後驗證,那就用$parsers.push()