Date Mask using angular ui mask

Posted By : Milind Ahuja | 09-Aug-2016

Angular UI Mask

This pure AngularJS date mask will allow the users to enter only pre-defined pattern or format and is a great way to prevent them from typing the invalid values. 

To apply a date mask on input field you require a angular module "angular-ui-mask". You can download it from the following link:

https://github.com/angular-ui/ui-mask.

After loading the script files in your application and adding the module to your dependencies you have to do the following:

First of all, replace the file mask.js in the dist folder with the following:

mask.js

      /*
 Attaches input mask onto input element
 */
angular.module('ui.mask', [])
    .value('uiMaskConfig', {
        'maskDefinitions': {
            '1': /[0-1]/,
            '2': /[0-2]/,
            '3': /[0-3]/,
            '9': /\d/,
            'A': /[a-zA-Z]/,
            '*': /[a-zA-Z0-9]/
        }
    })
    .value('uiMaskRaw', true)
    .directive('uiMask', ['uiMaskConfig', 'uiMaskRaw', '$parse', function(maskConfig, maskRaw, $parse) {
        return {
            priority: 100,
            require: 'ngModel',
            restrict: 'A',
            compile: function uiMaskCompilingFunction() {
                var options = maskConfig,
                    globalMaskRaw = maskRaw;

                return function uiMaskLinkingFunction(scope, iElement, iAttrs, controller) {
                    var maskProcessed = false,
                        eventsBound = false,
                        maskCaretMap, maskPatterns, uiMaskFormat, maskComponents,
                        // Minimum required length of the value to be considered valid
                        minRequiredLength,
                        value, valueMasked, isValid,
                        // Vars for initializing/uninitializing
                        originalMaxlength = iAttrs.maxlength,
                        // Vars used exclusively in eventHandler()
                        oldValue, oldValueUnmasked, oldCaretPosition, oldSelectionLength,
                        // Var to override locally the uiMaskRaw global config.
                        localMaskRaw = globalMaskRaw;

                    function initialize(maskAttr) {
                        if (!angular.isDefined(maskAttr)) {
                            return uninitialize();
                        }
                        processRawMask(maskAttr);
                        if (!maskProcessed) {
                            return uninitialize();
                        }
                        initializeElement();
                        bindEventListeners();
                        return true;
                    }

                    function initUiMaskFormat(uiMaskFormatAttr) {
                        if (!angular.isDefined(uiMaskFormatAttr)) {
                            return;
                        }

                        uiMaskFormat = uiMaskFormatAttr;

                        // If the mask is processed, then we need to update the value
                        if (maskProcessed) {
                            eventHandler();
                        }
                    }

                    function formatter(fromModelValue) {
                        if (!maskProcessed) {
                            return fromModelValue;
                        }
                        value = unmaskValue(fromModelValue || '');
                        isValid = validateValue(value);
                        controller.$setValidity('mask', isValid);
                        return isValid && value.length ? maskValue(value) : undefined;
                    }

                    function parser(fromViewValue) {
                        if (!maskProcessed) {
                            return fromViewValue;
                        }
                        value = unmaskValue(fromViewValue || '');
                        isValid = validateValue(value);
                        // We have to set viewValue manually as the reformatting of the input
                        // value performed by eventHandler() doesn't happen until after
                        // this parser is called, which causes what the user sees in the input
                        // to be out-of-sync with what the controller's $viewValue is set to.
                        if (localMaskRaw === true) {
                            controller.$viewValue = value.length ? maskValue(value) : '';
                        } else {
                            value = value.length ? maskValue(value) : '';
                            controller.$viewValue = value;
                        }
                        controller.$setValidity('mask', isValid);
                        if (value === '' && iAttrs.required) {
                            controller.$setValidity('required', false);
                        }
                        return isValid ? value : undefined;
                    }

                    var linkOptions = {};

                    if (iAttrs.uiOptions) {
                        linkOptions = scope.$eval('[' + iAttrs.uiOptions + ']');
                        if (angular.isObject(linkOptions[0])) {
                            // we can't use angular.copy nor angular.extend, they lack the power to do a deep merge
                            linkOptions = (function(original, current) {
                                for (var i in original) {
                                    if (Object.prototype.hasOwnProperty.call(original, i)) {
                                        if (!current[i]) {
                                            current[i] = angular.copy(original[i]);
                                        } else {
                                            angular.extend(current[i], original[i]);
                                        }
                                    }
                                }
                                return current;
                            })(options, linkOptions[0]);
                        }
                    } else {
                        linkOptions = options;
                    }

                    iAttrs.$observe('uiMask', initialize);
                    iAttrs.$observe('uiMaskRaw', function(val) {
                        if (val !== undefined)
                            localMaskRaw = (val != 'false');
                    });
                    iAttrs.$observe('uiMaskFormat', initUiMaskFormat);
                    var modelViewValue = false;
                    iAttrs.$observe('modelViewValue', function(val) {
                        if (val === 'true') {
                            modelViewValue = true;
                        }
                    });
                    scope.$watch(iAttrs.ngModel, function(val) {
                        if (modelViewValue && val) {
                            var model = $parse(iAttrs.ngModel);
                            model.assign(scope, controller.$viewValue);
                        }
                    });
                    controller.$formatters.push(formatter);
                    controller.$parsers.push(parser);

                    function uninitialize() {
                        maskProcessed = false;
                        unbindEventListeners();

                        if (angular.isDefined(originalMaxlength)) {
                            iElement.attr('maxlength', originalMaxlength);
                        } else {
                            iElement.removeAttr('maxlength');
                        }

                        iElement.val(controller.$modelValue);
                        controller.$viewValue = controller.$modelValue;
                        return false;
                    }

                    function initializeElement() {
                        value = oldValueUnmasked = unmaskValue(controller.$modelValue || '');
                        valueMasked = oldValue = maskValue(value);
                        isValid = validateValue(value);
                        var viewValue = isValid && value.length ? valueMasked : '';
                        if (iAttrs.maxlength) { // Double maxlength to allow pasting new val at end of mask
                            iElement.attr('maxlength', maskCaretMap[maskCaretMap.length - 1] * 2);
                        }
                        iElement.val(viewValue);
                        controller.$viewValue = viewValue;
                        // Not using $setViewValue so we don't clobber the model value and dirty the form
                        // without any kind of user interaction.
                    }

                    function bindEventListeners() {
                        if (eventsBound) {
                            return;
                        }
                        iElement.bind('blur', blurHandler);
                        iElement.bind('mousedown mouseup', mouseDownUpHandler);
                        iElement.bind('input keyup click focus', eventHandler);
                        eventsBound = true;
                    }

                    function unbindEventListeners() {
                        if (!eventsBound) {
                            return;
                        }
                        iElement.unbind('blur', blurHandler);
                        iElement.unbind('mousedown', mouseDownUpHandler);
                        iElement.unbind('mouseup', mouseDownUpHandler);
                        iElement.unbind('input', eventHandler);
                        iElement.unbind('keypress', eventHandler); //custom by Milind
                        iElement.unbind('keyup', eventHandler);
                        iElement.unbind('click', eventHandler);
                        iElement.unbind('focus', eventHandler);
                        eventsBound = false;
                    }

                    function validateValue(value) {
                        // Zero-length value validity is ngRequired's determination
                        return value.length ? value.length >= minRequiredLength : true;
                    }

                    function unmaskValue(value) {
                        var valueUnmasked = '',
                            maskPatternsCopy = maskPatterns.slice();
                        // Preprocess by stripping mask components from value
                        value = value.toString();
                        angular.forEach(maskComponents, function(component) {
                            value = value.replace(component, '');
                        });
                        angular.forEach(value.split(''), function(chr) {
                            if (maskPatternsCopy.length && maskPatternsCopy[0].test(chr)) {
                                valueUnmasked += chr;
                                maskPatternsCopy.shift();
                            }
                        });
                        return valueUnmasked;
                    }

                    function maskValue(unmaskedValue) {
                        var valueMasked = '',
                            maskCaretMapCopy = maskCaretMap.slice();

                        angular.forEach(uiMaskFormat.split(''), function(chr, i) {
                            if (unmaskedValue.length && i === maskCaretMapCopy[0]) {
                                valueMasked += unmaskedValue.charAt(0) || ' ';
                                unmaskedValue = unmaskedValue.substr(1);
                                maskCaretMapCopy.shift();
                            } else {
                                valueMasked += chr;
                            }
                        });
                        return valueMasked;
                    }

                    function getMaskFormatChar(i) {
                        var maskFormat = iAttrs.uiMaskFormat;

                        if (typeof maskFormat !== 'undefined' && maskFormat[i]) {
                            return maskFormat[i];
                        } else {
                            return ' ';
                        }
                    }

                    // Generate array of mask components that will be stripped from a masked value
                    // before processing to prevent mask components from being added to the unmasked value.
                    // E.g., a mask pattern of '+7 9999' won't have the 7 bleed into the unmasked value.
                    // If a maskable char is followed by a mask char and has a mask
                    // char behind it, we'll split it into it's own component so if
                    // a user is aggressively deleting in the input and a char ahead
                    // of the maskable char gets deleted, we'll still be able to strip
                    // it in the unmaskValue() preprocessing.
                    function getMaskComponents() {
                        return uiMaskFormat.replace(/[_]+/g, '_').replace(/([^_]+)([a-zA-Z0-9])([^_])/g, '$1$2_$3').split('_');
                    }

                    function processRawMask(mask) {
                        var characterCount = 0;

                        maskCaretMap = [];
                        maskPatterns = [];
                        uiMaskFormat = '';

                        if (typeof mask === 'string') {
                            minRequiredLength = 0;

                            var isOptional = false,
                                splitMask = mask.split('');

                            angular.forEach(splitMask, function(chr, i) {
                                if (linkOptions.maskDefinitions[chr]) {

                                    maskCaretMap.push(characterCount);

                                    uiMaskFormat += getMaskFormatChar(i);
                                    maskPatterns.push(linkOptions.maskDefinitions[chr]);

                                    characterCount++;
                                    if (!isOptional) {
                                        minRequiredLength++;
                                    }
                                } else if (chr === '?') {
                                    isOptional = true;
                                } else {
                                    uiMaskFormat += chr;
                                    characterCount++;
                                }
                            });
                        }
                        // Caret position immediately following last position is valid.
                        maskCaretMap.push(maskCaretMap.slice().pop() + 1);

                        maskComponents = getMaskComponents();
                        maskProcessed = maskCaretMap.length > 1 ? true : false;
                    }

                    function blurHandler() {
                        oldCaretPosition = 0;
                        oldSelectionLength = 0;
                        if (value.length === 0) {
                            valueMasked = '';
                            iElement.val('');
                            scope.$apply(function() {
                                controller.$setViewValue('');
                            });
                        }
                    }

                    function mouseDownUpHandler(e) {
                        if (e.type === 'mousedown') {
                            iElement.bind('mouseout', mouseoutHandler);
                        } else {
                            iElement.unbind('mouseout', mouseoutHandler);
                        }
                    }

                    iElement.bind('mousedown mouseup', mouseDownUpHandler);

                    function mouseoutHandler() {
                        /*jshint validthis: true */
                        oldSelectionLength = getSelectionLength(this);
                        iElement.unbind('mouseout', mouseoutHandler);
                    }

                    function eventHandler(e) {
                        /*jshint validthis: true */
                        e = e || {};
                        // Allows more efficient minification
                        var eventWhich = e.which,
                            eventType = e.type;

                        // Prevent shift and ctrl from mucking with old values
                        if (eventWhich === 16 || eventWhich === 91) {
                            return;
                        }

                        var val = iElement.val(),
                            valOld = oldValue,
                            valMasked,
                            valUnmasked = unmaskValue(val),
                            valUnmaskedOld = oldValueUnmasked,
                            valAltered = false,


                            //              caretPos = getCaretPosition(this) || 0,
                            caretPos = getCaretPosition(this, e.keyCode) || 0, //custom code by Milind
                            caretPosOld = oldCaretPosition || 0,
                            caretPosDelta = caretPos - caretPosOld,
                            caretPosMin = maskCaretMap[0],
                            caretPosMax = maskCaretMap[valUnmasked.length] || maskCaretMap.slice().shift(),

                            selectionLenOld = oldSelectionLength || 0,
                            isSelected = getSelectionLength(this) > 0,
                            wasSelected = selectionLenOld > 0,

                            // Case: Typing a character to overwrite a selection
                            isAddition = (val.length > valOld.length) || (selectionLenOld && val.length > valOld.length - selectionLenOld),
                            // Case: Delete and backspace behave identically on a selection
                            isDeletion = (val.length < valOld.length) || (selectionLenOld && val.length === valOld.length - selectionLenOld),
                            isSelection = (eventWhich >= 37 && eventWhich <= 40) && e.shiftKey, // Arrow key codes

                            isKeyLeftArrow = eventWhich === 37,
                            // Necessary due to "input" event not providing a key code
                            isKeyBackspace = eventWhich === 8 || (eventType !== 'keyup' && isDeletion && (caretPosDelta === -1)),
                            isKeyDelete = eventWhich === 46 || (eventType !== 'keyup' && isDeletion && (caretPosDelta === 0) && !wasSelected),

                            // Handles cases where caret is moved and placed in front of invalid maskCaretMap position. Logic below
                            // ensures that, on click or leftward caret placement, caret is moved leftward until directly right of
                            // non-mask character. Also applied to click since users are (arguably) more likely to backspace
                            // a character when clicking within a filled input.
                            caretBumpBack = (isKeyLeftArrow || isKeyBackspace || eventType === 'click') && caretPos > caretPosMin;

                        oldSelectionLength = getSelectionLength(this);

                        // These events don't require any action
                        if (isSelection || (isSelected && (eventType === 'click' || eventType === 'keyup'))) {
                            return;
                        }

                        // Value Handling
                        // ==============

                        // User attempted to delete but raw value was unaffected--correct this grievous offense
                        if ((eventType === 'input') && isDeletion && !wasSelected && valUnmasked === valUnmaskedOld) {
                            while (isKeyBackspace && caretPos > caretPosMin && !isValidCaretPosition(caretPos)) {
                                caretPos--;
                            }
                            while (isKeyDelete && caretPos < caretPosMax && maskCaretMap.indexOf(caretPos) === -1) {
                                caretPos++;
                            }
                            var charIndex = maskCaretMap.indexOf(caretPos);
                            // Strip out non-mask character that user would have deleted if mask hadn't been in the way.
                            valUnmasked = valUnmasked.substring(0, charIndex) + valUnmasked.substring(charIndex + 1);
                            valAltered = true;
                        }

                        // Update values
                        valMasked = maskValue(valUnmasked);

                        oldValue = valMasked;
                        oldValueUnmasked = valUnmasked;
                        iElement.val(valMasked);
                        if (valAltered) {
                            // We've altered the raw value after it's been $digest'ed, we need to $apply the new value.
                            scope.$apply(function() {
                                controller.$setViewValue(valUnmasked);
                            });
                        }

                        // Caret Repositioning
                        // ===================

                        // Ensure that typing always places caret ahead of typed character in cases where the first char of
                        // the input is a mask char and the caret is placed at the 0 position.
                        if (isAddition && (caretPos <= caretPosMin)) {
                            caretPos = caretPosMin + 1;
                        }

                        if (caretBumpBack) {
                            caretPos--;
                        }

                        // Make sure caret is within min and max position limits
                        caretPos = caretPos > caretPosMax ? caretPosMax : caretPos < caretPosMin ? caretPosMin : caretPos;

                        // Scoot the caret back or forth until it's in a non-mask position and within min/max position limits
                        while (!isValidCaretPosition(caretPos) && caretPos > caretPosMin && caretPos < caretPosMax) {
                            caretPos += caretBumpBack ? -1 : 1;
                        }

                        if ((caretBumpBack && caretPos < caretPosMax) || (isAddition && !isValidCaretPosition(caretPosOld))) {
                            caretPos++;
                        }
                        //            oldCaretPosition = caretPos;
                        if (eventType !== 'input') { // custom code by Milind
                            oldCaretPosition = caretPos;
                        }
                        setCaretPosition(this, caretPos);
                    }

                    function isValidCaretPosition(pos) {
                        return maskCaretMap.indexOf(pos) > -1;
                    }

                    //          function getCaretPosition(input) {
                    function getCaretPosition(input, eventWhich) { //custom code by Milind
                        if (!input) return 0;
                        var isRemoving = eventWhich === 8 || eventWhich === 46; //custom code by Milind
                        if (input.selectionStart !== undefined) {
                            if (!isRemoving) { //custom code by Milind
                                input.selectionStart = input.selectionEnd;
                            }
                            return input.selectionStart;
                        } else if (document.selection) {
                            // Curse you IE
                            input.focus();
                            var selection = document.selection.createRange();
                            selection.moveStart('character', input.value ? -input.value.length : 0);
                            return selection.text.length;
                        }
                        return 0;
                    }

                    function setCaretPosition(input, pos) {
                        if (!input) return 0;
                        if (input.offsetWidth === 0 || input.offsetHeight === 0) {
                            return; // Input's hidden
                        }
                        if (input.setSelectionRange) {
                            input.focus();
                            input.setSelectionRange(pos, pos);
                        } else if (input.createTextRange) {
                            // Curse you IE
                            var range = input.createTextRange();
                            range.collapse(true);
                            range.moveEnd('character', pos);
                            range.moveStart('character', pos);
                            range.select();
                        }
                    }

                    function getSelectionLength(input) {
                        if (!input) return 0;
                        if (input.selectionStart !== undefined) {
                            return (input.selectionEnd - input.selectionStart);
                        }
                        if (document.selection) {
                            return (document.selection.createRange().text.length);
                        }
                        return 0;
                    }

                    // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf
                    if (!Array.prototype.indexOf) {
                        Array.prototype.indexOf = function(searchElement /*, fromIndex */ ) {
                            if (this === null) {
                                throw new TypeError();
                            }
                            var t = Object(this);
                            var len = t.length >>> 0;
                            if (len === 0) {
                                return -1;
                            }
                            var n = 0;
                            if (arguments.length > 1) {
                                n = Number(arguments[1]);
                                if (n !== n) { // shortcut for verifying if it's NaN
                                    n = 0;
                                } else if (n !== 0 && n !== Infinity && n !== -Infinity) {
                                    n = (n > 0 || -1) * Math.floor(Math.abs(n));
                                }
                            }
                            if (n >= len) {
                                return -1;
                            }
                            var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
                            for (; k < len; k++) {
                                if (k in t && t[k] === searchElement) {
                                    return k;
                                }
                            }
                            return -1;
                        };
                    }

                };
            }
        };
    }]);
   

Secondly, make a directive for date in your directive.js to define the pattern, which will allow users to enter only valid date and prevents invalid entries.

directive.js

.directive('myDate', function($filter, $parse) {
return {
    restrict: 'A',
    require: 'ngModel',
    replace: true,
    transclude: true,
    template: '',
    link: function(scope, element, attrs, controller) {
      scope.limitToValidDate = limitToValidDate;
      var dateFilter = $filter("date");
      var today = new Date();
      var date = {};

      function isValidMonth(month) {
        return month >= 0 && month < 12;
      }

      function isValidDay(day) {
        return day > 0 && day < 32;
      }

      function isValidYear(year) {
        return year > (today.getFullYear() - 115) && year < (today.getFullYear() + 115);
      }

      function isValidDate(inputDate) {
        inputDate = new Date(formatDate(inputDate));
//        if (!angular.isDate(inputDate)) {
//          return false;
//        }
        date.month = inputDate.getMonth();
        date.day = inputDate.getDate();
        date.year = inputDate.getFullYear();
        return (isValidMonth(date.month) && isValidDay(date.day) && isValidYear(date.year));
      }

      function formatDate(newDate) {
        var modelDate = $parse(attrs.ngModel);
        newDate = dateFilter(newDate, "MM/dd/yyyy");
        modelDate.assign(scope, newDate);
        return newDate;
      }

      var pattern = "^(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])(19|20)\\d\\d$" +
        "|^(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])(19|20)\\d$" +
        "|^(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])(19|20)$" +
        "|^(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])[12]$" +
        "|^(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$" +
        "|^(0[1-9]|1[012])([0-3])$" +
        "|^(0[1-9]|1[012])$" +
        "|^[01]$";
      var regexp = new RegExp(pattern);

      function limitToValidDate(event) {
        var key = event.charCode ? event.charCode : event.keyCode;
        if ((key >= 48 && key <= 57) || key === 9 || key === 46) {
          var character = String.fromCharCode(event.which);
          var start = element[0].selectionStart;
          var end = element[0].selectionEnd;
          var testValue = (element.val().slice(0, start) + character + element.val().slice(end)).replace(/\s|\//g, "");
          if (!(regexp.test(testValue))) {
            event.preventDefault();
          }
        }
      }
    }
  }
});
        

At last, use this directive in a input field, you want to apply a date mask in your HTML file:

HTML

  
        

THANKS

About Author

Author Image
Milind Ahuja

Milind is a bright Lead Frontend Developer and have knowledge of HTML, CSS, JavaScript, AngularJS, Angular 2+ Versions and photoshop. His hobbies are learning new computer techniques, fitness and interest in different languages.

Request for Proposal

Name is required

Comment is required

Sending message..