(翻译)Angular 1.3中的验证器管道

原文地址:VALIDATORS PIPELINE IN ANGULAR 1.3

我们知道在Angular中操作表单是很爽的。因为Angular本身的作用域模型,我们总能在相应的作用域中获取到表单当前的状态,这使得在我们的视图中可以很容易得访问到特定字段的值或者表述表单当前的状态。

在构建表单的时候如果要说最费时费力的事情,那就是表单的验证。我们知道,为了处理给定的用户数据,在服务端(server-side)进行验证总是必要的,这可能会阻塞我们的APP。但是我们也想提供一种更好的用户体验,这就是客户端(client-side)验证。我们已经学过了ngModelOptions,在这篇文章我们准备来谈谈Angular1.2中表单数据的验证以及在Angular1.3中使用验证器管道(validators pipeline)怎么使表单数据验证变得更容易。

内置表单验证

在我们讨论新版本的Angular(1.3及以上)中关于表单验证的新内容之前,我们先来看一看之前我们都有哪些可用的功能,以及我们为什么要对其进行改进。

我们可以使用HTML5提供的验证属性让浏览器验证我们的表单控件。比如,如果我们想要用原生的验证属性验证email的输入值,我们所能做的仅仅是在这个表单元素上添加required属性。

<input type="email" required>

你可能还知道其他的一些验证属性,比如minlength、maxlength和pattern。然而,在当下,事实证明这些API(这里指的是原生的H5验证属性)在所有的浏览器和平台上表现得并不一致,甚至有些还不支持。这就是为什么Angular用它的ngModel指令和控制器对HTML5的验证属性做了基本的实现,从而使他们在不同浏览器之间表现一致。

下面是一些得到实现的验证属性:

  • ng-required
  • ng-minlength
  • ng-maxlength
  • ng-min
  • ng-max
  • ng-pattern

另外,Angular自动帮我们验证了某些输入类型。下面的代码展示了一个简单的表单,这个表单只有一个email类型的字段。在其上添加ng-model指令以便Angular可以监听到它。注意到,表单上的name属性,这个属性将表单的FormController实例发布到对应的作用域。

<form name="myForm">
  <input type="email" name="emailField">
  <p ng-if="myForm.emailField.$error.email">Email address is not valid!</p>
</form>

在浏览器中运行这段代码,你可以看到验证自动地运行了。错误信息的接口甚至暴露在FormController的$error对象上,这让显示错误信息这件事变得轻而易举。

当然,email并不是唯一的能自动验证的类型。url、number类型也是可以出发自动验证的类型。在1.3版本中,又添加了对日期和时间输入的支持,比如:date、time、datetime-local、week和month等。

旧方法中的自定义验证—1.2版本

内置的验证器很nice,但是在某些情况下我们需要的验证器超出了基本的功能,也就是说我们的需求超出了边界。那么这时候就要引入自定义验证器了。

在Angular中验证的关键部分是ngModelController,因为它控制了在DOM(DOM value)和作用域(scope express)之间来回传值的逻辑。

在1.3版本之前,我们可以通过使用ngModelController和它提供的$formatters和$parsers管道构建自定义验证器。

假如我们想要实现一个自定义验证器,用来检查用户传入的值是否是一个整数。为了实现这个验证器,首先我们要创建一个新的指令,并在这个指令中引入ngModelController。像下面这样:

app.directive('validateInteger', function () {
  return {
    require: 'ngModel',
    link: function (scope, element, attrs, ctrl) {
      // add validation to ctrl
    }
  };
});

通过require属性,在指令的link函数中,我们可以访问到其它指令的控制器。上面代码中的ctrl指向的是ngModelController的一个实例,这个实例有$formatters属性和$parsers属性。这两个属性是数组,就像管道一样,当特定的事情发生时可以被调用。

这里说的特定的事情包括:

  1. model to view update – 当绑定的数据模型变化时,为了格式化这个值并且更改有效性状态,所有在$formatters中的函数将一个接一个地被调用。
  2. view to model update – 当用户与表单控件发生交互时,调用ngModelController的$setViewValue方法,然后为了转换值,并且更改相应的有效性状态会调用$parsers数组中的所有函数。

基于以上信息,我们知道有一种管道把值从model传送到view,有另一种管道把值从view传递到model。因此,如果我们想要检查在我们的控件中给定的值是否是一个整数,我们就需要在被执行的管道中进行检查。对于通过view 更新model,就要使用$parsers数组。

现在我们所要做的就是在$parsers中添加一个新的函数,这个函数用来完成必要的检查并根据检查结果通过调用$setValidity()方法设置相应的有效性状态。为了确保我们的验证函数在管道中可以被第一个调用,我们使用Array.prototype.unshift将验证函数添加到管道中。

app.directive('validateInteger', function () {
  var REGEX = /^\-?\d+$/;
  return {
    require: 'ngModel',
    link: function (scope, element, attrs, ctrl) {
      ctrl.$parsers.unshift(function (viewValue) {
        if (REGEX.test(viewValue)) {
          ctrl.$setValidity('integer', true);
          return viewValue;
        } else {
          ctrl.$setValidity('integer', false);
          // if invalid, return undefined
          // (no model update happens)
          return undefined;
        }
      });
    }
  };
});

我们检查这个新值是否与我们的正则表达式相匹配。如果相匹配,我们把integer的有效性设置为true并且返回这个viewVale,这样之后这个值就可以继续被传递给管道中的下一个函数。

如果这个新值不和我们的正则表达式相匹配,我们把它的有效性设置为false,同时在FormController的$error对象上暴露一个integer接口,这样我们就可以展示相应的错误信息了。为了 阻止值继续在管道中传递,我们会显式地返回undefined。

然后我们可以像使用其他指令一样使用它:

<form name="myForm">
  <input type="text" validate-integer>
  <p ng-if="myForm.$error.integer">Oups error.</p>
</form>

正如你看到的,当编写自定义验证器的时候有许多要注意的地方。我们需要知道$formatters管道和$parsers管道。我们还要使用$setValidity()方法显式地设置一个值的有效性状态。

另外,由于HTML5的原生的表单验证,一些输入类型可能不会显示输入的值,直到输入合法的值。

那么在Angular1.3是怎么更好地完成这项工作的呢?

认识$validators管道

在Angular1.3中还加入了另一种管道,$validators管道,区别与$formatters管道和$parsers管道。与$formatters和$parsers不同的是,validators 管道能够同时获取到viewValue和modelValue。因为一旦$parsers和$formatters成功运行,才会调用$validators管道。
另一个不同是,$validators不是一个数组,而是一个对象。这个对象的每一个键都描述了一个验证器(validator)。下面让我们用$validators管道来实现我们的integer自定义验证器。

app.directive('validateInteger', function () {
  var REGEX = /^\-?\d+$/;
  return {        
    require: 'ngModel',
    link: function (scope, element, attrs, ctrl) {
      ctrl.$validators.integer = function (modelValue, viewValue) {
        if (REGEX.test(viewValue)) {
          return true
        }
        return false;
      };
    }
  };
});

正如你所看到的,我们不用再时刻留意有没有调用$setValidity()方法。Angular会在内部帮我们调用$setValidity()方法,而且在$validators管道中的验证器函数的返回值是true或false。
当然,如果值是不合法的,那么相应的,在FormController上回暴露出一个$error接口。

异步验证器

在1.3版本中,Angular甚至向前迈得更远,使异步验证成为了可能。想像这样一种情形,你有一个用来输入用户名的输入框,当用户输入了一个名字时,你需要在你的服务器上执行一些验证。那么,直到服务器响应之前,你的应用将一直处于等待状态。

这就是为什么继$validators之后还需要另外一种叫做$asyncValidators的验证器对象。

除了异步验证器是异步的并且是基于promise(约定,承诺)的之外,异步验证器($asyncValidators)与同步验证器($validators)的工作方式很像。异步验证器不返回true或者false,它返回的是保存异步代码执行结果的一个promise。

它可能长得像下面这样:

 

app.directive('validateUsername', function ($q, userService) {
  return {
    require: 'ngModel',
    link: function (scope, element, attrs, ctrl) {
      ctrl.$asyncValidators.username = function (modelValue, viewValue) {
        return $q(function (resolve, reject) {
          userService.checkValidity(viewValue).then(function () {
              resolve();
            }, function () {
              reject();
            });
        });
      };
    }
  };
});

在同步验证器成功执行之后,异步验证器才会被调用。在异步验证器运行期间,一个$pending对象将会被暴露在这个字段的ngModelController上;同时,$valid和$invalid标示(flag)会被设置为undefined。
我们可以像下面这样展示一个正在加载的信息(注意,该字段上的name属性用于暴露它对应的控制器):

<form name="myForm">
  <input type="text" name="username" validate-username>
  <p ng-if="myForm.username.$pending">Validating user name...</p>
</form>

现在我们学习了$validators和$asyncValidayors,那是不是意味着$parsers和$formatters就没有存在的必要了呢?
答案显然是否定的。验证器管道($validators和$asyncValidayors)已经很好的与之前的管道($parsers和$formatters)结合在了一起,开发者要能根据功能的不同区分这几种管道。
根据我们了解到的内容,验证器管道有更简单的API。我们不用再关心用$setValidity()方法设置有效性状态。而且,最重要的是,我们可以愉快地使用异步验证器了。

下面是对上文中异步验证器例子的补完(下面的例子在控制台会有一个报错,还没有找到原因):

HTML代码(注意省了ng-app):

<div ng-controller="oneCtrl">
    <form name="myForm">
        <input check-name type="text" ng-model="one.username" name="username" />
        <button type="button" ng-click="submitForm()" value="submit">提交</button>
    </form>
</div>

js代码:

 

var myModule = angular.module('deferApp',[]);
myModule.controller('oneCtrl',function($scope,$q,userService) {
    $scope.submitForm = function() {
        if($scope.myForm.$valid) {
            alert('恭喜你,用户名注册成功!!');
        } else {
            alert('抱歉,表单验证未通过,数据无法提交!!');
        }
    }
});

myModule.directive('checkName',function($q,userService) {
    return {
        require: 'ngModel',
        link: function (scope, element, attrs, ctrl) {
            ctrl.$asyncValidators.username = function (modelValue, viewValue) {
                console.log(viewValue);
                console.log(userService.checkValidity(viewValue));
                return $q(function (resolve, reject) {
                    userService.checkValidity(viewValue).then(function (result) {
                        console.log(result);
                        resolve(result);
                        console.log(scope.myForm);
                        console.log(scope.myForm.username);
                    }, function (error) {
                        console.log(error);
                        reject(error);
                        console.log(scope.myForm);
                        console.log(scope.myForm.username);
                    });
                });
            };
        }
    };
});

myModule.factory('userService',function($http,$q) {
    var userService = {};   // 返回的服务
    userService.checkValidity = function (value) {
        var defer = $q.defer();//defer用于注册resolve或者reject时的提示信息
        var promise = defer.promise;//promise用于注册resolve或者reject时的回调函数
        $http({
            method:'GET',
            url:'./json/data.json'

        }).then(function(data) {
            console.log(data);
            if(!value) {
                defer.reject({
                        mess:'请输入用户名',
                        bool:false
                });
                return;
            }
            var arr = data.data,result = '';
            for(var i = 0; i < arr.length; i++) {
                if(arr[i].username === value) {
                    result = {
                        mess:'该用户名已经存在!',
                        bool:false
                    };
                    break;
                }
            }
            if(result) {
                defer.reject(result);
            } else {
                result = {
                        mess:'该用户名可用。',
                        bool:true
                    };
                defer.resolve(result);
            }

        },function(error) {
            console.log(error);
            defer.reject({
                        mess:'服务器返回了一个错误。',
                        bool:false
            });
        });
        return promise;
    };
    return userService;
});

json:

[
  {
    "username":"trecerface",
    "age":49
  }
 ,{
  "username":"starboy",
  "age":25
}
 ,{
  "username":"gelut",
  "age":5
}
 ,{
  "username":"rocket",
  "age":30
}
 ,{
  "username":"kamoler",
  "age":23
}
 ,{
  "username":"doom",
  "age":35
}
]

 

参考文章:

1、给你一个承诺 - 玩转 AngularJS 的 Promise

2、形象的讲解angular中的$q与promise

3、AngularJS 中的Promise --- $q服务详解

相关资料:

Promise - JavaScript | MDN

posted @ 2017-10-05 16:43  Fogwind  阅读(486)  评论(0编辑  收藏  举报