Search code examples
regexangularjsangularjs-directiveangular-ngmodel

Angular Input Restriction Directive - Negating Regular Expressions


EDIT: Please feel free to add additional validations that would be useful for others, using this simple directive.

--

I'm trying to create an Angular Directive that limits the characters input into a text box. I've been successful with a couple common use cases (alphbetical, alphanumeric and numeric) but using popular methods for validating email addresses, dates and currency I can't get the directive to work since I need it negate the regex. At least that's what I think it needs to do.

Any assistance for currency (optional thousand separator and cents), date (mm/dd/yyyy) and email is greatly appreciated. I'm not strong with regular expressions at all.

Here's what I have currently: http://jsfiddle.net/corydorning/bs05ys69/

HTML

<div ng-app="example">
<h1>Validate Directive</h1>

<p>The Validate directive allow us to restrict the characters an input can accept.</p>

<h3><code>alphabetical</code> <span style="color: green">(works)</span></h3>
<p>Restricts input to alphabetical (A-Z, a-z) characters only.</p>
<label><input type="text" validate="alphabetical" ng-model="validate.alphabetical"/></label>

<h3><code>alphanumeric</code> <span style="color: green">(works)</span></h3>
<p>Restricts input to alphanumeric (A-Z, a-z, 0-9) characters only.</p>
<label><input type="text" validate="alphanumeric" ng-model="validate.alphanumeric" /></label>

<h3><code>currency</code> <span style="color: red">(doesn't work)</span></h3>
<p>Restricts input to US currency characters with comma for thousand separator (optional) and cents (optional).</p>
<label><input type="text" validate="currency.us" ng-model="validate.currency" /></label>

<h3><code>date</code> <span style="color: red">(doesn't work)</span></h3>
<p>Restricts input to the mm/dd/yyyy date format only.</p>
<label><input type="text" validate="date" ng-model="validate.date" /></label>

<h3><code>email</code> <span style="color: red">(doesn't work)</span></h3>
<p>Restricts input to email format only.</p>
<label><input type="text" validate="email" ng-model="validate.email" /></label>

<h3><code>numeric</code> <span style="color: green">(works)</span></h3>
<p>Restricts input to numeric (0-9) characters only.</p>
<label><input type="text" validate="numeric" ng-model="validate.numeric" /></label>

JavaScript

angular.module('example', [])
  .directive('validate', function () {
    var validations = {
      // works
      alphabetical: /[^a-zA-Z]*$/,

      // works
      alphanumeric: /[^a-zA-Z0-9]*$/,

      // doesn't work - need to negate?
      // taken from: http://stackoverflow.com/questions/354044/what-is-the-best-u-s-currency-regex
      currency: /^[+-]?[0-9]{1,3}(?:,?[0-9]{3})*(?:\.[0-9]{2})?$/,

      // doesn't work - need to negate?
      // taken from here: http://stackoverflow.com/questions/15196451/regular-expression-to-validate-datetime-format-mm-dd-yyyy
      date: /(?:0[1-9]|1[0-2])\/(?:0[1-9]|[12][0-9]|3[01])\/(?:19|20)[0-9]{2}/,

      // doesn't work - need to negate?
      // taken from: http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
      email: /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i,

      // works
      numeric: /[^0-9]*$/
    };

  return {
    require: 'ngModel',

    scope: {
      validate: '@'
    },

    link: function (scope, element, attrs, modelCtrl) {
      var pattern = validations[scope.validate] || scope.validate
      ;

      modelCtrl.$parsers.push(function (inputValue) {
        var transformedInput = inputValue.replace(pattern, '')
        ;

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

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

Solution

  • I am pretty sure, there is better way, probably regex is also not best tool for that, but here is mine proposition.

    This way you can only restrict which characters are allowed for input and to force user to use proper format, but you will need to also validate final input after user will finish typing, but this is another story.

    The alphabetic, numeric and alphanumeric are quite simple, for input and validating input, as it is clear what you can type, and what is a proper final input. But with dates, mails, currency, you cannot validate input with regex for full valid input, as user need to type it in first, and in a meanwhile the input need to by invalid in terms of final valid input. So, this is one thing to for example restrict user to type just digits and / for a date format, like: 12/12/1988, but in the end you need to check if he typed proper date or just 12/12/126 for example. This need to be checked when answer is submited by user, or when text field lost focus, etc.

    To just validate typed character, you can try with this:

    JSFiddle DEMO

    First change:

    var transformedInput = inputValue.replace(pattern, '')
    

    to

    var transformedInput = inputValue.replace(pattern, '$1')
    

    then use regular expressions:

    • /^([a-zA-Z]*(?=[^a-zA-Z]))./ - alphabetic
    • /^([a-zA-Z0-9]*(?=[^a-zA-Z0-9]))./ - alphanumeric
    • /(\.((?=[^\d])|\d{2}(?![^,\d.]))|,((?=[^\d])|\d{3}(?=[^,.$])|(?=\d{1,2}[^\d]))|\$(?=.)|\d{4,}(?=,)).|[^\d,.$]|^\$/- currency (allow string like: 343243.34, 1,123,345.34, .05 with or without $)
    • ^(((0[1-9]|1[012])|(\d{2}\/\d{2}))(?=[^\/])|((\d)|(\d{2}\/\d{2}\/\d{1,3})|(.+\/))(?=[^\d])|\d{2}\/\d{2}\/\d{4}(?=.)).|^(1[3-9]|[2-9]\d)|((?!^)(3[2-9]|[4-9]\d)\/)|[3-9]\d{3}|2[1-9]\d{2}|(?!^)\/\d\/|^\/|[^\d/] - date (00-12/00-31/0000-2099)
    • /^(\d*(?=[^\d]))./ - numeric
    • /^([\w.$-]+\@[\w.]+(?=[^\w.])|[\w.$-]+\@(?=[^\w.-])|[\w.@-]+(?=[^\w.$@-])).$|\.(?=[^\w-@]).|[^\w.$@-]|^[^\w]|\.(?=@).|@(?=\.)./i - email

    Generally, it use this pattern:

    ([valid characters or structure] captured in group $1)(?= positive lookahead for not allowed characters) any character
    

    in effect it will capture all valid character in group $1, and if user type in an invalid character, whole string is replaced with already captured valid characters from group $1. It is complemented by part which shall exclude some obvious invalid character(s), like @@ in a mail, or 34...2 in currency.

    With understanding how these regular expression works, despite that it looks quite complex, I think it easy to extend it, by adding additional allowed/not allowed characters.

    Regular expression for validating currency, dates and mails are easy to find, so I find it redundant to post them here.

    OffTopic. Whats more the currency part in your demo is not working, it is bacause of: validate="currency.us" instead of validate="currency", or at least it works after this modification.