Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS directive is breaking based on first character of directive name

I wrote an Angular directive that is exhibiting some strange behavior. The directive adds a function to $parsers to limit what the user types based on a regex pattern. If the current text doesn't match the patter, the parser reverts the text back to the previous value of the field.

As a result, when the text is reverted, Angular detects this as a change in the field value and enters the parser again. This is normally fine since the value passed into the parser is now valid but I am experiencing one very strange quirk.

After I got this directive working I decided to change its name. I did this and suddenly the validation was failing. My error-handler was reporting too much recursion. When I debugged the code I found that the 2nd call into the parser after entering an invalid character showed the field value parameter as 'undefined'. As a result, my code treated the value as invalid and tried to revert back again, which caused another call into the parser with an 'undefined' value, etc, etc until a stack overflow occurred.

I changed the directive name back, debugged again, and everything suddenly started working fine! The second call into the parser had the correct value instead of 'undefined'.

I did a little playing around and discovered that I could recreate this bug by altering the first character of the directive name. Directive names that began with the characters 'a' through 'm' worked fine, but names beginning with 'n' through 'z' broke (OK, I confess, I didn't try all 26 characters, but a sampling of characters showed that all the names in the sample where the first letter of the directive name was in the first half of the alphabet worked and all runs where the first letter was in the 2nd half of the alphabet failed).

I put together a plunker with my code to demonstrate it:

http://plnkr.co/edit/k8Hpk2jsMCS6xjOKiES5

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

app.controller('MainCtrl', function($scope) {
    $scope.someNumber;
});

app.directive('formattedWithPattern', function () {
    // Formats an input field as a positive integer.
    // Usage:
    //  <input type="text" data-integer-format>
    // Creates:
    //  <input type="text" data-integer-format>
    return {
        require: 'ngModel',
        link: function (scope, element, attr, ctrl) {
            if (!ctrl) return;

            var pattern = attr.ngPattern;
            // get the pattern between the slashes and any modifiers
            var r = new RegExp("^/(.*)/(.*)$");
            var matches = r.exec(pattern);
            var regex;
            if (matches) {
                regex = new RegExp('^' + matches[1] + '$', matches[2]);
            }
            var lastText = '';
            var reverted = false;

            function fromUser(text) {
                var m = regex.exec(text);
                if (m) {
                    // join matches together into a single string
                    lastText = m[0];
                    if (lastText != text) {
                        // the original text contained some invalid characters
                        ctrl.$setViewValue(lastText);
                        ctrl.$render();
                    }
                }
                else {
                    // nothing in the text matched the regular expression...
                    // revert to the last good value
                    if (text != lastText) {
                        ctrl.$setViewValue(lastText);
                        ctrl.$render();
                    }
                }
                return lastText;
            }

            ctrl.$parsers.unshift(fromUser);
        }
    };
});

Here's an example of it in use (also from the plunker):

<body ng-controller="MainCtrl">
    <input type="text" name="testNumber" id="testNumber" 
       data-ng-model="someNumber" data-ng-required="true"
       ng-pattern="/[\+\-]?[0-9]*(\.[0-9]*)?/" 
       formatted-with-pattern />
    {{zip}}
</body>

For some reason, the plunker behaves SLIGHTLY differently than what I saw when testing on my computer. All failures are still in the upper range of the alphabet, but the failures start at 'o' instead of 'n'.

If you change the name of the directive in the app.js and index.html to begin with any character 'o' through 'z' and rerun the plunker you can easily see the behavior. The above directive is using a numeric pattern, so when the directive name is "valid" the directive doesn't allow any characters other than 0-9, ., +, and -. When the name is "invalid" the directive also allows characters because the recursive call into the parser is breaking out without actually changing the input field value.

This strikes me as VERY bizarre behavior. I didn't find any other mentions of this online so I thought I'd throw it out here. Has anyone else ever encountered anything like this? Is this a bug in AngularJS? Does anyone know a work-around other than just making sure my directive name begins with a character a through m?

like image 589
tchnologist Avatar asked Oct 21 '22 03:10

tchnologist


1 Answers

You could raise a directive priority: to ensure the execution order like this:

return {
  require: 'ngModel',
  priority: 1, // default is 0
  link: function (scope, element, attr, ctrl) {
    ...
  }
};

It will ensure that your directive postLink function will be run after ng-required and ng-pattern.

Example Plunker: http://plnkr.co/edit/dy1zCq9F8EejYWo1m4Oe?p=preview

You could also use ctrl.$viewValue directly to avoid the directive execution order problem.

Actually, IMHO, it make more sense to use the ctrl.$viewValue since you would like to re-render the view if it is invalid, so you need a real view value, not a value that already passed other parsers.

To do so, you could change your parser from:

function fromUser(text) {
  ...
}

to this instead:

function fromUser() {
  var text = ctrl.$viewValue;
  ...
}

Example Plunker: http://plnkr.co/edit/FA2Fq4aNlUrcdUMoopuX?p=preview

Hope this helps.

like image 82
runTarm Avatar answered Oct 23 '22 00:10

runTarm