0

Doing a quick POC in AngularJS to only allow specific input into a text box.

The goal is to check the value every time the user types a new character, if it fails the regular expression check, we need to either reject the character or roll it back to the previous value.

The way I see it, here are my 2 options: 1. Bind to keypress event, check what the new value would be against a regex, and return false if it fails, preventing the character from being accepted into the text box 2. Bind to keyup event, check what the new value is against a regex, and if it fails, revert it to the previous value

How can I accomplish this from my directive?

var currencyRegEx = /^\$?\-?([1-9]{1}[0-9]{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))$|^\-?\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))$|^\(\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))\)$/;
app.directive("currencyInput", function () {
    return {
        restrict: "A",
        scope: {
        },
        require: 'ngModel',
        link: function (scope, element, attrs, ngModelCtrl) {
            $(element).bind('keypress', function (event) {
                // TODO: Get what new value would be
                var newValue = "...";
                return currencyRegEx.test(newValue);

            });
            $(element).bind('keyup', function (event) {
                var newValue = $(this).val();
                if (!currencyRegEx.test(newValue)) {                
                    // TODO: Revert to previous value
                }
            });
        }
    }
});

<input type="text" class="form-control" ng-model="item.paymentAmount" currency-input />

EDIT w/ SOLUTION Here is the current solution we have in place in order to prevent non-digit input and rollback invalid currency value.

First, we created a new property "scope.prevValue" to hold the last valid value entered by the user. Then, on "keypress" we check to make sure the user typed a digit, comma, or period. Finally, on "keyup", we check the new value against the currency regex and rollback if needed.

var currencyRegEx = /^\$?\-?([1-9]{1}[0-9]{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))$|^\-?\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))$|^\(\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))\)$/;
var digitRegex = /^[0-9]*$/;
app.directive("currencyInput", function () {
    return {
        restrict: "A",
        scope: {},
        require: 'ngModel',
        link: function (scope, element, attrs, ngModelCtrl) {
            scope.prevValue = '';

            $(element).on('keypress', function(event) {
                var validAlphaChars = ['.', ','];
                var enteredCharacter = String.fromCharCode(event.charCode != null ? event.charCode : event.keyCode);
                if (validAlphaChars.indexOf(enteredCharacter) < 0 && !digitRegex.test(enteredCharacter)) {
                    return false;
                }
            });

            $(element).on('keyup', function (event) {
                var newValue = $(element).val();
                if (newValue.length > 0 && !currencyRegEx.test(newValue)) {
                    $(element).val(scope.prevValue);
                    return false;
                } else {
                    scope.prevValue = $(element).val();
                }
            });
        }
    });

EDIT w/ SOLUTION #2 (using Steve_at_IDV's approach on accepted answer)

var currencyRegEx = /^\$?\-?([1-9]{1}[0-9]{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))$|^\-?\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))$|^\(\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))\)$/;
app.directive("currencyInput", function () {
    return {
        restrict: "A",
        scope: {},
        require: 'ngModel',
        link: function (scope, element, attrs, ngModelCtrl) {
            ngModelCtrl.$parsers.push(function (value) {
                if (value.length > 0 && value != '.' && !currencyRegEx.test(value)) {
                    var prevValue = ngModelCtrl.$modelValue;
                    ngModelCtrl.$setViewValue(prevValue)
                    ngModelCtrl.$render();
                    return prevValue;
                }

                return value;
            });
        }
    }
});
thiag0
  • 2,199
  • 5
  • 31
  • 51

2 Answers2

2

This would be a good time to use ngModelCtrl.$parsers instead of binding to keypresses manually. Try something like this in your link function:

ngModelCtrl.$parsers.push( function (value) {
    // do some validation logic...it fails
    if (validationFails) {
        var prevValue = ctrl.$modelValue;
        ctrl.$setViewValue(prevValue); // set view
        ctrl.$render(); // render view
        return prevValue; // set model
    }

    // otherwise we're good!
    return value;
} );

Here is a Plunker which demonstrates. The input field will reject a lowercase z from being entered.

See the $parsers section of https://docs.angularjs.org/api/ng/type/ngModel.NgModelController for more info.

Steve Wakeford
  • 331
  • 1
  • 8
  • Thanks for your reply. I gave this a try but it doesn't revert back to the old value when I return false. – thiag0 May 20 '16 at 14:44
  • Ah yes sorry $validators is to modify the validity of the form input, I'll modify the answer to use $parsers. – Steve Wakeford May 20 '16 at 15:41
  • Try this new answer out and see if that helps ya. – Steve Wakeford May 20 '16 at 15:44
  • Gave this a try, I am able to maintain the correct "prevValue" but it still shows the bad value in the UI. Can I prevent the model update from this function? – thiag0 May 20 '16 at 17:32
  • As I read the docs more, maybe you need to call ngModelCtrl.$setViewValue(prevValue) in the validationFails block before returning prevValue. I might set up a plunkr to test this. – Steve Wakeford May 23 '16 at 19:04
  • I actually did give this a try with $setViewValue but was not able to get it working. – thiag0 May 23 '16 at 19:07
  • Finally figured it out, we need to call $render afterwards. Also in my Plunker we just look at the value in ngModel instead of looking to another local variable. See here: https://plnkr.co/edit/csjTB8tBQbdSLnezEjp2?p=preview where entering 'z' will prevent update. I'll update the answer. – Steve Wakeford May 23 '16 at 19:59
  • Great job man, tested on my side and it's working as well. Thanks for the help! – thiag0 May 23 '16 at 21:32
0

Firstly, I think you shouldn't modify the input of the user. I personnaly find it bad on a UX point of view. It's better to indicate that the input is in an error state by bordering in red for example.

Secondly, there is a directive that can fit your need, ng-pattern.

<input type="text" 
       class="form-control" 
       ng-model="item.paymentAmount" 
       ng-pattern="currencyRegEx" />

Some similar questions :

Community
  • 1
  • 1
Cyril Gandon
  • 16,830
  • 14
  • 78
  • 122
  • Thanks for your reply. I tried using ng-pattern but it doesn't seem to be what I need. I agree that we shouldn't modify the user input, but this is a requirement for our project :( – thiag0 May 20 '16 at 14:46