如何防止模型无效?

时间:2014-11-12 17:25:33

标签: javascript angularjs validation

我是最佳实践的坚定拥护者,特别是在角度方面,但我无法设法使用全新的$validators管道功能。

案例很简单:使用$parser$formatter和一些$validators的指令增强了1个输入:

<input name="number" type="text" ng-model="number" number> 

这是(简化)指令:

myApp.directive('number', [function() {
  return {
    restrict: 'A',
    require: 'ngModel',
    /*
     * Must have higher priority than ngModel directive to make
     * number (post)link function run after ngModel's one.
     * ngModel's priority is 1.
     */
    priority: 2,
    link: function($scope, $element, $attrs, $controller) {
      $controller.$parsers.push(function (value) {
        return isFinite(value)? parseInt(value): undefined;
      });

      $controller.$formatters.push(function (value) {
        return value.toString() || '';
      });

      $controller.$validators.minNumber = function(value) {
        return value && value >= 1;
      };

      $controller.$validators.maxNumber = function(value) {
        return value && value <= 10;
      };
    }
  };
}]);

I made a little plunk to play with :)

我想要实现的行为是:考虑到存储在作用域中的初始值是有效的,如果用户输入无效,则防止它被破坏。保留旧版本,直到设置新的有效版本。

注意:在角度1.3之前,我可以直接在ngModelController中使用$parser/$formatter API执行此操作。我仍然可以用1.3做到这一点,但这不会是“角度方式”。

NB2:在我的应用中,我并不是真的使用数字,而是quantities。问题仍然存在。

4 个答案:

答案 0 :(得分:6)

看起来您希望在验证后进行一些解析,将模型设置为最后一个有效值,而不是从视图派生的值。但是,我认为1.3管道的工作方式相反:解析在验证之前发生。

所以我的答案是按照你在1.2中的方式做到这一点:使用$parsers设置验证密钥并将用户的输入转换回最新的有效值。

以下指令执行此操作,在指令中指定的validators数组按顺序运行。如果以前的任何验证器都失败了,那么后面的验证器就不会运行:它假设一次可能发生一次验证错误。

与您的问题最相关的是,它维护模型中的最后一个有效值,并且只有在没有发生验证错误时才会覆盖。

myApp.directive('number', [function() {
  return {
    restrict: 'A',
    require: 'ngModel',
    /*
     * Must have higher priority than ngModel directive to make
     * number (post)link function run after ngModel's one.
     * ngModel's priority is 1.
     */
    priority: 2,
    link: function($scope, $element, $attrs, $controller) {
      var lastValid;

      $controller.$parsers.push(function(value) {
        value = parseInt(value);
        lastValid = $controller.$modelValue;

        var skip = false;
        validators.forEach(function(validator) {
          var isValid = skip || validator.validatorFn(value);
          $controller.$setValidity(validator.key, isValid);
          skip = skip || !isValid;
        });
        if ($controller.$valid) {
          lastValid = value;
        }
        return lastValid;
      });

      $controller.$formatters.push(function(value) {
        return value.toString() || '';
      });

      var validators = [{
        key: 'isNumber',
        validatorFn: function(value) {
          return isFinite(value);
        }
      }, {
        key: 'minNumber',
        validatorFn: function(value) {
          return value >= 1;
        }
      }, {
        key: 'maxNumber',
        validatorFn: function(value) {
          return value <= 10;
        }
      }];
    }
  };
}]);

可以看到http://plnkr.co/edit/iUbUCfJYDesX6SNGsAcg?p=preview

答案 1 :(得分:3)

我认为你从Angular-way而不是Angular-way方面过度思考。 1.3之前使用$parsers管道是Angular-way,现在不是吗?

嗯,Angular-way也是ng-model将模型设置为undefined(默认情况下)为无效值。遵循Angular-way方向并定义另一个变量来存储“lastValid”值:

<input ng-model="foo" ng-maxlength="3" 
       ng-change="lastValidFoo = foo !== undefined ? foo : lastValidFoo"
       ng-init="foo = lastValidFoo">

不需要特殊的指令,并且它不会试图绕过Angular本身正在做的事情 - 即Angular-way。 :)

答案 2 :(得分:2)

从Angular 1.3开始,您可以使用ngModelOptions指令更好地控制模型值何时更新。看看这个更新的Plunker,向您展示如何实现您正在寻找的功能:http://plnkr.co/edit/DoWbvlFMEtqF9gvJCjPF?p=preview

基本上,您将模型定义为getterSetter,并且仅在新值有效时才返回该值:

 $scope.validNumber = function(value) {
  return angular.isDefined(value) ? ($scope.number = value) :  $scope.number;
}
$scope.modelOptions = {
  getterSetter: true,
  allowInvalid: false
};

然后使用此代码更新您的内容如下:

 <input name="number" type="text" ng-model="validNumber" ng-model-options="modelOptions" number>

我真的希望这能回答你所有的问题,如果我能再帮忙,请告诉我。

莱昂。

答案 3 :(得分:1)

以下是我的plnkr及相关代码:

$controller.$$runValidators = function(originalRun) {
        var lastModelValue, lastViewValue;
        return function() {
          var ctrl = this;
          var doneCallback = arguments[arguments.length-1];
          arguments[arguments.length-1] = function(allValid) {
              doneCallback(allValid);
              console.log(allValid);
              console.log('valid:' +allValid+ ' value:' +ctrl.$viewValue);
              if (ctrl.$viewValue) {
                lastViewValue= allValid ? ctrl.$viewValue : lastViewValue | '';
                lastModelValue= allValid ? ctrl.$modelValue : lastModelValue;
                ctrl.$modelValue = allValid ? ctrl.$modelValue : lastModelValue;
                ctrl.$$writeModelToScope();
                ctrl.$viewValue = ctrl.$$lastCommittedViewValue = lastViewValue;
                ctrl.$render();

              }
              console.log(ctrl.$viewValue + '  '+lastViewValue);

             // console.log( ctrl.$modelValue);
          };
          originalRun.apply(this, arguments);
        }
      }($controller.$$runValidators);

它可以是有效的解决方案吗? 我认为你可以拦截角度验证流程的唯一方法是覆盖$$ runValidators。也许这段代码需要一些调整但是有效。