Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to prevent model to be invalid?

I am a strong advocate of best practices, especially when it comes 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.

like image 990
glepretre Avatar asked Dec 15 '22 18:12

glepretre


2 Answers

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

like image 189
Michal Charemza Avatar answered Dec 17 '22 07:12

Michal Charemza


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

like image 41
New Dev Avatar answered Dec 17 '22 08:12

New Dev