3

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?

1
  • You should really databind to something else than attribute ngModel, it's confusing. Commented Aug 11, 2015 at 12:31

2 Answers 2

2

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

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you for the solution. What I currently face in your fiddle it does not filter the spaces in the end of the input; also it has the same funny behavior when I try to insert a space inside of the input. First space is "eaten" and second one is printed
the second space is not eaten.. i can't reproduce. The last space, It is a strange behaviour in angular. You need add ng-trim. I update the fiddler. Remember update your version of angular.
@simon I think the both issue are fixed. can you have a look. I update the model in the parser ctrl.$setViewValue(value);. I was scare that update the model there intruce a limitation with other directive but i add another directive remove-letter-f and works perfect
2

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>

1 Comment

Hello Patrick, my problem is actually not in passing the value to the module only, but also to change the view value. More deeply, I need to separate the parts of the credit card into 4 pieces with spaces in between. I already tried the parsers+formatters solution but I wasn't satisfied with their results. It was almost working but if I changed the number in the middle the cursor was jumping in the end of the input. So that is why I am forced to not use parsers / formatters.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.