admin管理员组

文章数量:1290982

I am a strong advocate of best practices, especially when it es to angular but I can't manage to use the brand new $validators pipeline feature as it should be.

The case is quite simple: 1 input enhanced by a directive using $parser, $formatter and some $validators:

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

Here is the (simplified) directive:

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 :)

The behavior I am trying to achieve is: Considering that the initial value stored in the scope is valid, prevent it from being corrupted if the user input is invalid. Keep the old one until a new valid one is set.

NB: Before angular 1.3, I was able to do this using ngModelController API directly in $parser/$formatter. I can still do that with 1.3, but that would not be "angular-way".

NB2: In my app I am not really using numbers, but quantities.The problem remains the same.

I am a strong advocate of best practices, especially when it es to angular but I can't manage to use the brand new $validators pipeline feature as it should be.

The case is quite simple: 1 input enhanced by a directive using $parser, $formatter and some $validators:

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

Here is the (simplified) directive:

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 :)

The behavior I am trying to achieve is: Considering that the initial value stored in the scope is valid, prevent it from being corrupted if the user input is invalid. Keep the old one until a new valid one is set.

NB: Before angular 1.3, I was able to do this using ngModelController API directly in $parser/$formatter. I can still do that with 1.3, but that would not be "angular-way".

NB2: In my app I am not really using numbers, but quantities.The problem remains the same.

Share Improve this question edited Mar 5, 2015 at 15:14 user57508 asked Nov 12, 2014 at 17:25 glepretreglepretre 8,1675 gold badges46 silver badges58 bronze badges 9
  • @MikeChamberlain I took my example's inspiration from your answer – glepretre Commented Nov 12, 2014 at 17:28
  • Tricky question... it looks like you want some parsing to happen after validation (setting the model to the last valid value rather than one derived from the view). However, I think the 1.3 pipeline works the other way around: parsing happens before validation. I'm not sure quite on your definition of best practice, but if you mean what is remended/allowed by the docs, they say in most cases it should be sufficient to use the ngModel.$validators, which leaves room for other cases, of which yours could be one. – Michal Charemza Commented Mar 5, 2015 at 9:15
  • @MichalCharemza What I would like is to keep my model always valid. And the issue I have with the $validators is 1) If I write "42" in my input, the parser will return 42, then the maxNumber validator will return false and set the model value to undefined. I would prefer keeping the last valid value in the model. 2) the $validator function receive 2 arguments when called internally by angular modelValue, viewValue and I have not found the way to identify the current validation: after parse or after formatting. In my real case, it matters :/ – glepretre Commented Mar 5, 2015 at 12:14
  • 1 Yes, I think I understand your problem/question. My point is doing it just using the $parser pipeline, as you would in 1.2, is ok, because it's not possible (or at least, I haven't worked out how ;-) to use the $validators to achieve what you want. – Michal Charemza Commented Mar 5, 2015 at 12:59
  • @MichalCharemza About my 1st issue: I found that ng-model-options="{allowInvalid: true}" prevents this unexpected behavior (but add more HTML...). About the 2nd issue: I can use a switch to determine the previous latest operation (parse or format). – glepretre Commented Mar 5, 2015 at 15:21
 |  Show 4 more ments

4 Answers 4

Reset to default 6 +150

It looks like you want some parsing to happen after validation, setting the model to the last valid value rather than one derived from the view. However, I think the 1.3 pipeline works the other way around: parsing happens before validation.

So my answer is to just do it as you would do it in 1.2: using $parsers to set the validation keys and to transform the user's input back to the most recent valid value.

The following directive does this, with an array of validators specified within the directive that are run in order. If any of the previous validators fails, then the later ones don't run: it assumes one validation error can happen at a time.

Most relevant to your question, is that it maintains the last valid value in the model, and only overwrites if there are no validation errors occur.

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;
        }
      }];
    }
  };
}]);

This can be seen working at http://plnkr.co/edit/iUbUCfJYDesX6SNGsAcg?p=preview

I think you are over-thinking this in terms of Angular-way vs. not Angular-way. Before 1.3 using $parsers pipeline was the Angular-way and now it's not?

Well, the Angular-way is also that ng-model sets the model to undefined (by default) for invalid values. Follow that Angular-way direction and define another variable to store the "lastValid" value:

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

No need for a special directive and it works across the board in a way that doesn't try to circumvent what Angular is doing natively - i.e. the Angular-way. :)

As of Angular 1.3 you can use the ngModelOptions directive to have greater control as to when your model value updates. Take a look at this updated Plunker to show you how to achieve the functionality you are looking for: http://plnkr.co/edit/DoWbvlFMEtqF9gvJCjPF?p=preview

Basically you define the model as a getterSetter and only return the new value if it is valid:

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

Then to use this code update your as follows:

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

I really hope this answers all of your questions, please let me know if I can help any more.

Leon.

Here is my plnkr with the relevant code:

$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);

Can it be a valid solution? the only way i think you can intercept the angular validation flow is override the $$runValidators. Maybe this code need a little bit of tweaking but works.

本文标签: javascriptHow to prevent model to be invalidStack Overflow