127

I have a text input and I don't want to allow users to use spaces, and everything typed will be turned into lowercase.

I know I'm not allowed to use filters on ng-model eg.

ng-model='tags | lowercase | no_spaces'

I looked at creating my own directive but adding functions to $parsers and $formatters didn't update the input, only other elements that had ng-model on it.

How can I change the input of that I'm currently typing in?

I'm essentially trying to create the 'tags' feature that works just like the one here on StackOverflow.

georgeawg
  • 48,608
  • 13
  • 72
  • 95
Andrew WC Brown
  • 2,240
  • 5
  • 22
  • 24
  • See if using $timeout(...,0) with ng-change helps: http://stackoverflow.com/questions/12176925/angularjs-reset-of-scope-value-doesnt-change-value-in-template-random-behav – Mark Rajcok Jan 19 '13 at 22:49

9 Answers9

207

I believe that the intention of AngularJS inputs and the ngModel direcive is that invalid input should never end up in the model. The model should always be valid. The problem with having invalid model is that we might have watchers that fire and take (inappropriate) actions based on invalid model.

As I see it, the proper solution here is to plug into the $parsers pipeline and make sure that invalid input doesn't make it into the model. I'm not sure how did you try to approach things or what exactly didn't work for you with $parsers but here is a simple directive that solves your problem (or at least my understanding of the problem):

app.directive('customValidation', function(){
   return {
     require: 'ngModel',
     link: function(scope, element, attrs, modelCtrl) {

       modelCtrl.$parsers.push(function (inputValue) {

         var transformedInput = inputValue.toLowerCase().replace(/ /g, ''); 

         if (transformedInput!=inputValue) {
           modelCtrl.$setViewValue(transformedInput);
           modelCtrl.$render();
         }         

         return transformedInput;         
       });
     }
   };
});

As soon as the above directive is declared it can be used like so:

<input ng-model="sth" ng-trim="false" custom-validation>

As in solution proposed by @Valentyn Shybanov we need to use the ng-trim directive if we want to disallow spaces at the beginning / end of the input.

The advantage of this approach is 2-fold:

  • Invalid value is not propagated to the model
  • Using a directive it is easy to add this custom validation to any input without duplicating watchers over and over again
Zanshin13
  • 980
  • 4
  • 19
  • 39
pkozlowski.opensource
  • 117,202
  • 60
  • 326
  • 286
  • 1
    I am sure that tricky part was with `modelCtrl.$setViewValue(transformedInput); modelCtrl.$render();` Useful would be link to documentation: http://docs.angularjs.org/api/ng.directive:ngModel.NgModelController One word to "protect" my solition is that scope property could be changed not only from views and my way cover this. So I think it depends on a actual situation how scope could be modified. – Valentyn Shybanov Jan 26 '13 at 13:02
  • 2
    what does 'modelCtrl' refer to in your example? – GSto May 17 '13 at 18:18
  • 4
    Where do you get the inputValue from? – Dofs May 23 '13 at 19:02
  • The best solution ever! – Mikhail Mar 12 '14 at 00:59
  • 2
    @GSto `modelCtrl` is the controller required by the directive. (`require 'ngModel'`) – Nate-Wilkins Apr 10 '14 at 14:15
  • 1
    @Dofs The input value is passed in when the model value changes - see https://github.com/angular/angular.js/blob/master/src/ng/directive/input.js#L1772 – Nate-Wilkins Apr 10 '14 at 14:18
  • Nice! This should be the accepted answer - keeping invalid data out the model is far better than watching for changes and retroactively updating. Great answer. – holmesal Aug 12 '14 at 05:36
  • 7
    Cursor jumps to the end of the textfield each time you type an invalid character, try to write 'world' and modify it to 'HeLLo world'! – Hafez Divandari Jan 17 '15 at 00:10
  • 1
    Sorry to add to a 2+ year answer but you can also set attrs.ngTrim = 'false' in the link() function and thus avoid needing to add the attribute to the element. Seems it has to be a string, not the boolean value. – Geuis Jun 02 '15 at 20:31
  • That is exactly what the built in ngList directive does – Jens Gabe Dec 22 '17 at 08:41
  • Thank you. Is it possibile to have a similar thing, without modify the model? I mean, i'd like to use the filter to show the input number as currency, without modify the model into a string – DeLac Apr 06 '18 at 08:41
  • so does this work as we type in the input? Or do we need to add a watcher in the directive? – feltspar Feb 16 '22 at 23:11
  • ignore my last comment. Just learned formatters and parsers are pipeline functions which execute when ng-model changes. The solution presented works for me using $formatters. Including solution as one of the answers. Letting the previous comment be for some lost dev like me. – feltspar Feb 16 '22 at 23:29
  • How do we write a unit test for this? – feltspar Feb 28 '22 at 00:41
29

I would suggest to watch model value and update it upon chage: http://plnkr.co/edit/Mb0uRyIIv1eK8nTg3Qng?p=preview

The only interesting issue is with spaces: In AngularJS 1.0.3 ng-model on input automatically trims string, so it does not detect that model was changed if you add spaces at the end or at start (so spaces are not automatically removed by my code). But in 1.1.1 there is 'ng-trim' directive that allows to disable this functionality (commit). So I've decided to use 1.1.1 to achieve exact functionality you described in your question.

Valentyn Shybanov
  • 19,331
  • 7
  • 66
  • 59
  • This was exactly what I looking for. It turns out I'm already using angularjs 1.1.1 – Andrew WC Brown Jan 20 '13 at 00:30
  • @Valentyn, your solution applied to SO question I referenced in the comment above. Thanks. http://stackoverflow.com/questions/12176925/angularjs-reset-of-scope-value-doesnt-change-value-in-template-random-behav – Mark Rajcok Jan 20 '13 at 00:54
  • this solution can have bad side effects, see other answer below, you should use a directive for this – pilavdzice Oct 01 '14 at 00:18
  • Reassigning scope variable from within `$watch` forces the listener to be invoked again. In simple cases (where your filter is idempotent) you will end up with the filter executing twice on every modification. – BorisOkunskiy Dec 05 '14 at 11:57
23

A solution to this problem could be to apply the filters on controller side :

$scope.tags = $filter('lowercase')($scope.tags);

Don't forget to declare $filter as dependency.

  • 4
    But you'd need a $watch on it if you want it to update properly. – Mr Mikkél Jun 09 '14 at 16:30
  • this is only executed once. and adding to a watch is not the right solution because it, even initially, allows the model to become invalid - the correct solution is to add to the model's $parsers. – icfantv Feb 26 '16 at 17:11
  • 4
    You don't have to like my answer, but that doesn't mean it's wrong. Check your ego before you downvote. – icfantv Feb 28 '16 at 17:02
10

If you are using read only input field, you can use ng-value with filter.

for example:

ng-value="price | number:8"
Edward D. Wilson
  • 351
  • 1
  • 4
  • 11
  • 1
    +1 This may not be an answer to the exact question that was asked, but including it is helpful to others whose situation could be slightly different. – ksadowski Dec 22 '20 at 14:54
5

Use a directive which adds to both the $formatters and $parsers collections to ensure that the transformation is performed in both directions.

See this other answer for more details including a link to jsfiddle.

Community
  • 1
  • 1
Scott Munro
  • 13,369
  • 3
  • 74
  • 80
3

I had a similar problem and used

ng-change="handler(objectInScope)" 

in my handler I call a method of the objectInScope to modify itself correctly (coarse input). In the controller I have initiated somewhere that

$scope.objectInScope = myObject; 

I know this doesn't use any fancy filters or watchers... but it's simple and works great. The only down-side to this is that the objectInScope is sent in the call to the handler...

wojjas
  • 1,046
  • 1
  • 9
  • 21
1

If you are doing complex, async input validation it might be worth it to abstract ng-model up one level as part of a custom class with its own validation methods.

https://plnkr.co/edit/gUnUjs0qHQwkq2vPZlpO?p=preview

html

<div>

  <label for="a">input a</label>
  <input 
    ng-class="{'is-valid': vm.store.a.isValid == true, 'is-invalid': vm.store.a.isValid == false}"
    ng-keyup="vm.store.a.validate(['isEmpty'])"
    ng-model="vm.store.a.model"
    placeholder="{{vm.store.a.isValid === false ? vm.store.a.warning : ''}}"
    id="a" />

  <label for="b">input b</label>
  <input 
    ng-class="{'is-valid': vm.store.b.isValid == true, 'is-invalid': vm.store.b.isValid == false}"
    ng-keyup="vm.store.b.validate(['isEmpty'])"
    ng-model="vm.store.b.model"
    placeholder="{{vm.store.b.isValid === false ? vm.store.b.warning : ''}}"
    id="b" />

</div>

code

(function() {

  const _ = window._;

  angular
    .module('app', [])
    .directive('componentLayout', layout)
    .controller('Layout', ['Validator', Layout])
    .factory('Validator', function() { return Validator; });

  /** Layout controller */

  function Layout(Validator) {
    this.store = {
      a: new Validator({title: 'input a'}),
      b: new Validator({title: 'input b'})
    };
  }

  /** layout directive */

  function layout() {
    return {
      restrict: 'EA',
      templateUrl: 'layout.html',
      controller: 'Layout',
      controllerAs: 'vm',
      bindToController: true
    };
  }

  /** Validator factory */  

  function Validator(config) {
    this.model = null;
    this.isValid = null;
    this.title = config.title;
  }

  Validator.prototype.isEmpty = function(checkName) {
    return new Promise((resolve, reject) => {
      if (/^\s+$/.test(this.model) || this.model.length === 0) {
        this.isValid = false;
        this.warning = `${this.title} cannot be empty`;
        reject(_.merge(this, {test: checkName}));
      }
      else {
        this.isValid = true;
        resolve(_.merge(this, {test: checkName}));
      }
    });
  };

  /**
   * @memberof Validator
   * @param {array} checks - array of strings, must match defined Validator class methods
   */

  Validator.prototype.validate = function(checks) {
    Promise
      .all(checks.map(check => this[check](check)))
      .then(res => { console.log('pass', res)  })
      .catch(e => { console.log('fail', e) })
  };

})();
Daniel Lizik
  • 3,058
  • 2
  • 20
  • 42
0

You can try this

$scope.$watch('tags ',function(){

    $scope.tags = $filter('lowercase')($scope.tags);

});
Nikhil Mahirrao
  • 3,547
  • 1
  • 25
  • 20
0

I came here to look for a solution that would actively change an input text and mask it with * for all but last 4 digits as we type. This is achieved by $formatters

eg: Account Num input box: 1234567890AHSB1 should display in the input box as **********AHSB

Answer is just a slight variation of that given by @pkozlowski.opensource above.

angular.module('myApp').directive('npiMask', function() {
  return {
    require: 'ngModel',
    link: function($scope, element, attrs, modelCtrl) {
      modelCtrl.$formatters.push(function(inputValue) {
        var transformedInput = inputValue.toString().replace(/.(?=.{4,}$)/g, '*');
        if (transformedInput !== inputValue) {
          modelCtrl.$setViewValue(transformedInput);
          modelCtrl.$render();
        }
        return transformedInput;
      });
    }
  };
});
<input-text 
 name="accountNum" 
 label="{{'LOAN_REPAY.ADD_LOAN.ACCOUNT_NUM_LABEL' | translate}}" 
 ng-model="vm.model.formData.loanDetails.accountNum" 
 is-required="true" 
 maxlength="35" 
 size="4" 
 npi-mask>
</input-text>
feltspar
  • 303
  • 1
  • 3
  • 15