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

app.controller('MainCtrl', function($scope) {
  
});
<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script data-require="angular.js@1.4.x" src="https://code.angularjs.org/1.4.9/angular.js" data-semver="1.4.9"></script>
    <script src="app.js"></script>
    <script src="ng-pattern-restrict.js"></script>
  </head>

  <body ng-controller="MainCtrl">
    <h1>ng-pattern-restrict demo</h1>
    <p>Full project at <a href="https://github.com/AlphaGit/ng-pattern-restrict" target="_blank">https://github.com/AlphaGit/ng-pattern-restrict</a>.</p>
    
    <div class="demo-container correct">
      <h2>Common scenarios</h2>
      
      <h3>Restricting to alphanumeric characters: <code>^[A-Za-z0-9]*$</code></h3>
      <input type="text" ng-pattern-restrict="^[A-Za-z0-9]*$" />
      <!-- Notice how without ^ and $ the regex will allow anything -->
      
      <h3>Restricting a date format: yyyy-mm-dd: <code>^\d{0,4}(-\d{0,2}(-\d{0,2})?)?$</code></h3>
      <p>Not validating for real dates, only numbers that follow the format. Notice that every sub-expression
      will allow the regex to validates even if that portion is missing.</p>
      <input type="text" ng-pattern-restrict="^\d{0,4}(-\d{0,2}(-\d{0,2})?)?$" />
    </div>
    
    <hr />
    
    <div class="demo-container incorrect">
      <h2>Examples of common misconceptions:</h2>
      
      <h3>Restricting for numbers only: <code>\d+</code></h3>
      <p>Notice that you can't erase the last character because the regex doesn't validate against empty string</p>
      <input type="text" ng-pattern-restrict="\d+" />
      
      <h3>Restricting -- incorrectly -- for alphabetic only: <code>[A-Za-z]</code></h3>
      <p>This one is incorrect because it will allow numbers as long as a letter between A-Z or a-z is present.
      So as long as the string contains a letter, it is allowed to contain any other character.</p>
      <input type="text" ng-pattern-restrict="[A-Za-z]" />
      
      <h3>Restricting for a date, yyyy-mm-dd (incorrect): \d{4}-\d{2}-\d{2}</h3>
      <p>This example is incorrect because it does not consider the intermediate states in which a user is
      typing the string. As such, it will never validate unless the user pastes the whole date completely.</p>
      <input type="text" ng-pattern-restrict="\d{4}-\d{2}-\d{2}" />
    </div>
  </body>

</html>
.correct {
  background-color: #92FFB7;
}

.incorrect {
  background-color: #FF9595;
}

.demo-container {
  padding: 1em;
}

  .demo-container h2 {
    margin-top: 0;
  }
/*jslint browser: true, plusplus: true, indent: 2 */

// This will be removed by uglify, along with the DEBUG code
if (typeof DEBUG === 'undefined') {
  DEBUG = true;
}

// Logic and fallbacks based on the following SO answers:
// - Getting caret position cross browser: http://stackoverflow.com/a/9370239/147507
// - Selection API on non input-text fields: http://stackoverflow.com/a/24247942/147507
// - Set cursor position on input text: http://stackoverflow.com/q/5755826/147507
angular.module('ngPatternRestrict', [])
  .directive('ngPatternRestrict', ['$log', function ($log) {
    'use strict';

    function showDebugInfo() {
      $log.debug("[ngPatternRestrict] " + Array.prototype.join.call(arguments, ' '));
    }

    return {
      restrict: 'A',
      require: "?ngModel",
      compile: function uiPatternRestrictCompile() {
        DEBUG && showDebugInfo("Loaded");

        return function ngPatternRestrictLinking(scope, iElement, iAttrs, ngModelController) {
          var regex, // validation regex object
            oldValue, // keeping track of the previous value of the element
            caretPosition, // keeping track of where the caret is at to avoid jumpiness
            // housekeeping
            initialized = false, // have we initialized our directive yet?
            eventsBound = false, // have we bound our events yet?
            // functions
            getCaretPosition, // function to get the caret position, set in detectGetCaretPositionMethods
            setCaretPosition; // function to set the caret position, set in detectSetCaretPositionMethods

          //-------------------------------------------------------------------
          // caret position
          function getCaretPositionWithInputSelectionStart() {
            return iElement[0].selectionStart; // we need to go under jqlite
          }

          function getCaretPositionWithDocumentSelection() {
            // create a selection range from where we are to the beggining
            // and measure how much we moved
            var range = document.selection.createRange();
            range.moveStart('character', -iElement.val().length);
            return range.text.length;
          }

          function getCaretPositionWithWindowSelection() {
            var s = window.getSelection(),
              originalSelectionLength = String(s).length,
              selectionLength,
              didReachZero = false,
              detectedCaretPosition,
              restorePositionCounter;

            do {
              selectionLength = String(s).length;
              s.modify('extend', 'backward', 'character');
              // we're undoing a selection, and starting a new one towards the beggining of the string
              if (String(s).length === 0) {
                didReachZero = true;
              }
            } while (selectionLength !== String(s).length);

            detectedCaretPosition = didReachZero ? selectionLength : selectionLength - originalSelectionLength;
            s.collapseToStart();

            restorePositionCounter = detectedCaretPosition;
            while (restorePositionCounter-- > 0) {
              s.modify('move', 'forward', 'character');
            }
            while (originalSelectionLength-- > 0) {
              s.modify('extend', 'forward', 'character');
            }

            return detectedCaretPosition;
          }

          function setCaretPositionWithSetSelectionRange(position) {
            iElement[0].setSelectionRange(position, position);
          }

          function setCaretPositionWithCreateTextRange(position) {
            var textRange = iElement[0].createTextRange();
            textRange.collapse(true);
            textRange.moveEnd('character', position);
            textRange.moveStart('character', position);
            textRange.select();
          }

          function setCaretPositionWithWindowSelection(position) {
            var s = window.getSelection(),
              selectionLength;

            do {
              selectionLength = String(s).length;
              s.modify('extend', 'backward', 'line');
            } while (selectionLength !== String(s).length);
            s.collapseToStart();

            while (position--) {
              s.modify('move', 'forward', 'character');
            }
          }

          // HACK: Opera 12 won't give us a wrong validity status although the input is invalid
          // we can select the whole text and check the selection size
          // Congratulations to IE 11 for doing the same but not returning the selection.
          function getValueLengthThroughSelection(input) {
            // only do this on opera, since it'll mess up the caret position
            // and break Firefox functionality
            if (!/Opera/i.test(navigator.userAgent)) {
              return 0;
            }

            input.focus();
            document.execCommand("selectAll");
            var focusNode = window.getSelection().focusNode;
            return (focusNode || {}).selectionStart || 0;
          }

          //-------------------------------------------------------------------
          // event handlers
          function revertToPreviousValue() {
            if (ngModelController) {
              scope.$apply(function () {
                ngModelController.$setViewValue(oldValue);
              });
            }
            iElement.val(oldValue);

            if (!angular.isUndefined(caretPosition)) {
              setCaretPosition(caretPosition);
            }
          }

          function updateCurrentValue(newValue) {
            oldValue = newValue;
            caretPosition = getCaretPosition();
          }

          function genericEventHandler(evt) {
            DEBUG && showDebugInfo("Reacting to event:", evt.type);

            //HACK Chrome returns an empty string as value if user inputs a non-numeric string into a number type input
            // and this may happen with other non-text inputs soon enough. As such, if getting the string only gives us an
            // empty string, we don't have the chance of validating it against a regex. All we can do is assume it's wrong,
            // since the browser is rejecting it either way.
            var newValue = iElement.val(),
              inputValidity = iElement.prop("validity");
            if (newValue === "" && iElement.attr("type") !== "text" && inputValidity && inputValidity.badInput) {
              DEBUG && showDebugInfo("Value cannot be verified. Should be invalid. Reverting back to:", oldValue);
              evt.preventDefault();
              revertToPreviousValue();
            } else if (newValue === "" && getValueLengthThroughSelection(iElement[0]) !== 0) {
              DEBUG && showDebugInfo("Invalid input. Reverting back to:", oldValue);
              evt.preventDefault();
              revertToPreviousValue();
            } else if (regex.test(newValue)) {
              DEBUG && showDebugInfo("New value passed validation against", regex, newValue);
              updateCurrentValue(newValue);
            } else {
              DEBUG && showDebugInfo("New value did NOT pass validation against", regex, newValue, "Reverting back to:", oldValue);
              evt.preventDefault();
              revertToPreviousValue();
            }
          }

          //-------------------------------------------------------------------
          // setup based on attributes
          function tryParseRegex(regexString) {
            try {
              regex = new RegExp(regexString);
            } catch (e) {
              throw "Invalid RegEx string parsed for ngPatternRestrict: " + regexString;
            }
          }

          //-------------------------------------------------------------------
          // setup events
          function bindListeners() {
            if (eventsBound) {
              return;
            }

            iElement.bind('input keyup click', genericEventHandler);

            DEBUG && showDebugInfo("Bound events: input, keyup, click");
          }

          function unbindListeners() {
            if (!eventsBound) {
              return;
            }

            iElement.unbind('input', genericEventHandler);
            //input: HTML5 spec, changes in content

            iElement.unbind('keyup', genericEventHandler);
            //keyup: DOM L3 spec, key released (possibly changing content)

            iElement.unbind('click', genericEventHandler);
            //click: DOM L3 spec, mouse clicked and released (possibly changing content)

            DEBUG && showDebugInfo("Unbound events: input, keyup, click");

            eventsBound = false;
          }

          //-------------------------------------------------------------------
          // initialization
          function readPattern() {
            var entryRegex = !!iAttrs.ngPatternRestrict ? iAttrs.ngPatternRestrict : iAttrs.pattern;
            DEBUG && showDebugInfo("RegEx to use:", entryRegex);
            tryParseRegex(entryRegex);
          }

          function notThrows(testFn, shouldReturnTruthy) {
          	try {
          		return testFn() || !shouldReturnTruthy;
          	} catch (e) {
          		return false;
          	}
          }

          function detectGetCaretPositionMethods() {
            var input = iElement[0];

            // Chrome will throw on input.selectionStart of input type=number
            // See http://stackoverflow.com/a/21959157/147507
            if (notThrows(function () { return input.selectionStart; })) {
              getCaretPosition = getCaretPositionWithInputSelectionStart;
            } else {
              // IE 9- will use document.selection
              // TODO support IE 11+ with document.getSelection()
              if (notThrows(function () { return document.selection; }, true)) {
                getCaretPosition = getCaretPositionWithDocumentSelection;
              } else {
                getCaretPosition = getCaretPositionWithWindowSelection;
              }
            }
          }

          function detectSetCaretPositionMethods() {
            var input = iElement[0];
            if (typeof input.setSelectionRange === 'function') {
              setCaretPosition = setCaretPositionWithSetSelectionRange;
            } else if (typeof input.createTextRange === 'function') {
              setCaretPosition = setCaretPositionWithCreateTextRange;
            } else {
              setCaretPosition = setCaretPositionWithWindowSelection;
            }
          }

          function initialize() {
            if (initialized) {
              return;
            }
            DEBUG && showDebugInfo("Initializing");

            readPattern();

            oldValue = iElement.val();
            if (!oldValue) {
              oldValue = "";
            }
            DEBUG && showDebugInfo("Original value:", oldValue);

            bindListeners();

            detectGetCaretPositionMethods();
            detectSetCaretPositionMethods();

            initialized = true;
          }

          function uninitialize() {
            DEBUG && showDebugInfo("Uninitializing");
            unbindListeners();
          }

          iAttrs.$observe("ngPatternRestrict", readPattern);
          iAttrs.$observe("pattern", readPattern);

          scope.$on("$destroy", uninitialize);

          initialize();
        };
      }
    };
  }]);