filters on ng-model in an input
Asked Answered
O

9

128

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.

Ornithomancy answered 19/1, 2013 at 22:38 Comment(1)
See if using $timeout(...,0) with ng-change helps: #12177425Freitag
S
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.

Shults answered 19/1, 2013 at 23:31 Comment(4)
This was exactly what I looking for. It turns out I'm already using angularjs 1.1.1Ornithomancy
@Valentyn, your solution applied to SO question I referenced in the comment above. Thanks. #12177425Freitag
this solution can have bad side effects, see other answer below, you should use a directive for thisHoudon
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.Regatta
W
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
Wellheeled answered 20/1, 2013 at 13:31 Comment(14)
I am sure that tricky part was with modelCtrl.$setViewValue(transformedInput); modelCtrl.$render(); Useful would be link to documentation: 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.Shults
what does 'modelCtrl' refer to in your example?Newport
Where do you get the inputValue from?Cannady
The best solution ever!Roice
@Newport modelCtrl is the controller required by the directive. (require 'ngModel')Brunobruns
@Cannady The input value is passed in when the model value changes - see github.com/angular/angular.js/blob/master/src/ng/directive/…Brunobruns
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.Indefensible
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'!Jamshid
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.Lifeblood
That is exactly what the built in ngList directive doesFendig
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 stringMenton
so does this work as we type in the input? Or do we need to add a watcher in the directive?Quintie
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.Quintie
How do we write a unit test for this?Quintie
S
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.

Shults answered 19/1, 2013 at 23:31 Comment(4)
This was exactly what I looking for. It turns out I'm already using angularjs 1.1.1Ornithomancy
@Valentyn, your solution applied to SO question I referenced in the comment above. Thanks. #12177425Freitag
this solution can have bad side effects, see other answer below, you should use a directive for thisHoudon
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.Regatta
I
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.

Incondensable answered 14/1, 2014 at 11:16 Comment(3)
But you'd need a $watch on it if you want it to update properly.Thomasthomasa
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.Almita
You don't have to like my answer, but that doesn't mean it's wrong. Check your ego before you downvote.Almita
S
10

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

for example:

ng-value="price | number:8"
Selsyn answered 10/12, 2017 at 22:36 Comment(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.Isolationist
W
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.

Worsham answered 20/10, 2015 at 12:34 Comment(0)
E
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...

Eades answered 7/10, 2014 at 17:25 Comment(0)
K
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) })
  };

})();
Kilbride answered 2/5, 2016 at 17:24 Comment(0)
A
0

You can try this

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

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

});
Agitator answered 19/1, 2016 at 6:50 Comment(0)
Q
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>
Quintie answered 16/2, 2022 at 23:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.