smnbbrv
smnbbrv

Reputation: 24581

Angularjs change ng-model within a directive on the same element with validations

In the most primitive angular app I am trying to create a directive for an input field which changes parent's ng-model value.

HTML:

<form novalidate>
  <input ng-model="ctrl.myvalue" mydirective minlength="19" />

  {{ ctrl.myvalue }}
</form>

JS:

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

app.directive('mydirective', function(){
    return {
        scope: { ngModel: '=' },
        link: function(scope, el) {
           el.on('input', function(e) {
              this.value = this.value.replace(/ /g,'');

              scope.ngModel = this.value;
           })
        }
    }
})

app.controller('MyController', function(){
  this.myvalue = '';
})

Plunker

The problem is that if I use this directive together with minlength or pattern for an input validation it gets a specific behavior: every second letter you type in the input disappears; also ng-model gets undefined value. Without validations the code works perfectly.

I also tried to create a custom validation as a workaround but it has the same effect.

Could you explain that or propose the way to go?

Upvotes: 3

Views: 1808

Answers (2)

Ra&#250;l Mart&#237;n
Ra&#250;l Mart&#237;n

Reputation: 4689

You can use unsift, as well render for the first iteration. Usually you can use ctrl.$setViewValue but you can be sure no relaunch when the value don;t change...

var testModule = angular.module('myModule', []);

testModule.controller('testCntrl', ['$scope', function ($scope) {
    $scope.test = 'sdfsd  fsdf sdfsd sdf';

}]);

testModule.directive('cleanSpaces', [function () {
    return {
        require: '?ngModel',
        link: function (scope, $elem, attrs, ctrl) {
            if (!ctrl) return;

            var filterSpaces = function (str) {
                return str.replace(/ /g, '');
            }

            ctrl.$parsers.unshift(function (viewValue) {

                var elem = $elem[0],
                    pos = elem.selectionStart,
                    value = '';

                if (pos !== viewValue.length) {
                    var valueInit = filterSpaces(
                    viewValue.substring(0, elem.selectionStart));
                    pos = valueInit.length;
                }

                //I launch the regular expression, 
                // maybe you prefer parse the rest 
                // of the substring and concat.

                value = filterSpaces(viewValue);
                $elem.val(value);

                elem.setSelectionRange(pos, pos);

                ctrl.$setViewValue(value);

                return value;
            });

            ctrl.$render = function () {
                if (ctrl.$viewValue) {
                    ctrl.$setViewValue(filterSpaces(ctrl.$viewValue));
                }
            };
        }
    };
}]);

http://jsfiddle.net/luarmr/m4dmz0tn/

UPDATE I update the fiddle with the last code and a validation example in angular and update the html with ng-trim (ngModel.$parsers ingore whitespace at the end of ng-model value).

Upvotes: 2

Patrick
Patrick

Reputation: 6958

Use Angular's NgModelController. I'm just adding to the $parsers (the functions that execute as the view is updated, but before the value is persisted to the model). Here, I am pushing the function onto the $parsers pipeline. Keep in mind that the model won't be populated until the minlength validation has been satisfied. The code snippet shows both the $viewValue and the modelValue

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

app.directive('mydirective', function() {
  return {
    require: 'ngModel',
    priority: 100,
    link: function(scope, el, attrs, ngModelCtrl) {
      // $parsers from view/DOM to model
      ngModelCtrl.$parsers.push(function(value) {
        console.log(value);
        return value && value.replace(/ /g, '');
      });
    }
  }
})

app.controller('MyController', function() {
  this.myvalue = '';
})
<script src="https://code.angularjs.org/1.4.0/angular.min.js"></script>
<div ng-app="app" ng-controller="MyController as ctrl">
  <form name="myForm" novalidate>
    <input ng-model="ctrl.myvalue" name="myValue" mydirective minlength="19" /><br /><br />Model Value: {{ ctrl.myvalue }}<br /><br />
    View Value: {{ myForm.myValue.$viewValue }}
  </form>
</div>

Update: If you are trying to perform custom validation, just forget about the minlength/required stuff and just write your own. It's probably not the nicest behavior to alter the text as the user types. This example will put spaces into the viewValue on the blur event. I still think ngModelController is the way to go, but I don't know enough of what you are trying to accomplish to give you something closer to what you are looking for.

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

app.directive('creditCardValidator', function() {
  return {
    require: 'ngModel',
    priority: 100,
    link: function(scope, el, attrs, ngModelCtrl) {
      // 16 characters
      attrs.$set('maxlength', 16);

      var noSpaces = function noSpaces(value) {
        return value.replace(/ /g, '');
      }
      var withSpaces = function withSpaces(value) {
        if (ngModelCtrl.$isEmpty(value)) {
          return;
        }

        var spacedValue = value.replace(/(\d{4})(\d{4})(\d{4})(\d{4})/, '$1 $2 $3 $4');
        return spacedValue || undefined;
      }

      ngModelCtrl.$parsers.push(noSpaces);
      ngModelCtrl.$formatters.push(withSpaces);

      ngModelCtrl.$validators.validCreditCard = function(modelValue, viewValue) {
        var value = noSpaces(modelValue || viewValue);
        var valid = /^\d{16}$/.test(value);
        return valid;
      };

      el.on('blur', function() {
        if (ngModelCtrl.$valid) {
          ngModelCtrl.$setViewValue(withSpaces(ngModelCtrl.$modelValue));
          ngModelCtrl.$render();
        }
      });
    }
  }
})

app.controller('MyController', function() {
  this.myvalue = '';
})
<script src="https://code.angularjs.org/1.4.0/angular.min.js"></script>
<div ng-app="app" ng-controller="MyController as ctrl">
  <form name="myForm" novalidate>
    <input ng-model="ctrl.myvalue" name="myValue" ng-model-options="{allowInvalid: true}" credit-card-validator />
    <br />
    <br />Model Value: {{ ctrl.myvalue }}
    <br />
    <br />View Value: {{ myForm.myValue.$viewValue }}
    <br />
    <br />Error: {{ myForm.myValue.$error }}
  </form>
</div>

Upvotes: 2

Related Questions