epeschier/angular-numeric-directive

View on GitHub
src/numeric-directive.js

Summary

Maintainability
A
2 hrs
Test Coverage
(function () {
    'use strict';
    if(typeof module !== 'undefined') {
        module.exports = 'purplefox.numeric';
    }

    /* global angular */
    angular
        .module('purplefox.numeric', [])
        .directive('numeric', numeric);

    numeric.$inject = ['$locale'];

    function numeric($locale) {
        // Usage:
        //     <input type="text" decimals="3" min="-20" max="40" formatting="false" ></input>
        // Creates:
        // 
        var directive = {
            link: link,
            require: 'ngModel',
            restrict: 'A'
        };
        return directive;


        function link(scope, el, attrs, ngModelCtrl) {
            var decimalSeparator = $locale.NUMBER_FORMATS.DECIMAL_SEP;
            var groupSeparator = $locale.NUMBER_FORMATS.GROUP_SEP;

            // Create new regular expression with current decimal separator.
            var NUMBER_REGEXP = "^\\s*(\\-|\\+)?(\\d+|(\\d*(\\.\\d*)))\\s*$";
            var regex = new RegExp(NUMBER_REGEXP);

            var formatting = true;
            var maxInputLength = 16;            // Maximum input length. Default max ECMA script.
            var max;                            // Maximum value. Default undefined.
            var min;                            // Minimum value. Default undefined.
            var limitMax = true;                // Limit input to max value (value is capped). Default true.
            var limitMin = true;                // Limit input to min value (value is capped). Default true.
            var decimals = 2;                   // Number of decimals. Default 2.
            var lastValidValue;                 // Last valid value.

            // Create parsers and formatters.
            ngModelCtrl.$parsers.push(parseViewValue);
            ngModelCtrl.$parsers.push(minValidator);
            ngModelCtrl.$parsers.push(maxValidator);
            ngModelCtrl.$formatters.push(formatViewValue);

            el.bind('blur', onBlur);        // Event handler for the leave event.
            el.bind('focus', onFocus);      // Event handler for the focus event.

            // Put a watch on the min, max and decimal value changes in the attribute.
            scope.$watch(attrs.min, onMinChanged);
            scope.$watch(attrs.max, onMaxChanged);
            scope.$watch(attrs.limitMax, onLimitMaxChanged);
            scope.$watch(attrs.limitMin, onLimitMinChanged);
            scope.$watch(attrs.decimals, onDecimalsChanged);
            scope.$watch(attrs.formatting, onFormattingChanged);

            // Setup decimal formatting.
            if (decimals > -1) {
                ngModelCtrl.$parsers.push(function (value) {
                    return (value) ? round(value) : value;
                });
                ngModelCtrl.$formatters.push(function (value) {
                    return (value || value === 0) ? formatPrecision(value) : value;
                });
            }

            function onMinChanged(value) {
                if (!angular.isUndefined(value)) {
                    min = parseFloat(value);
                    lastValidValue = minValidator(ngModelCtrl.$modelValue);
                    ngModelCtrl.$setViewValue(formatPrecision(lastValidValue));
                    ngModelCtrl.$render();
                }
            }

            function onMaxChanged(value) {
                if (!angular.isUndefined(value)) {
                    max = parseFloat(value);
                    maxInputLength = calculateMaxLength(max);
                    lastValidValue = maxValidator(ngModelCtrl.$modelValue);
                    ngModelCtrl.$setViewValue(formatPrecision(lastValidValue));
                    ngModelCtrl.$render();
                }
            }

            function onDecimalsChanged(value) {
                if (!angular.isUndefined(value)) {
                    decimals = parseFloat(value);
                    maxInputLength = calculateMaxLength(max);
                    if (lastValidValue !== undefined) {
                        ngModelCtrl.$setViewValue(formatPrecision(lastValidValue));
                        ngModelCtrl.$render();
                    }
                }
            }

            function onFormattingChanged(value) {
                if (!angular.isUndefined(value)) {
                    formatting = (value !== false);
                    ngModelCtrl.$setViewValue(formatPrecision(lastValidValue));
                    ngModelCtrl.$render();
                }
            }

            function onLimitMinChanged(value) {
                if (!angular.isUndefined(value)) {
                    limitMin = (value == "true");
                }
            }

            function onLimitMaxChanged(value) {
                if (!angular.isUndefined(value)) {
                    limitMax = (value == "true");
                }
            }

            /**
             * Round the value to the closest decimal.
             */
            function round(value) {
                var d = Math.pow(10, decimals);
                return Math.round(value * d) / d;
            }

            /**
             * Format a number with the thousand group separator.
             */
            function numberWithCommas(value) {
                if (formatting) {
                    var parts = (""+value).split(decimalSeparator);
                    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, groupSeparator);
                    return parts.join(decimalSeparator);
                }
                else {
                    // No formatting applies.
                    return value;
                }
            }

            /**
             * Format a value with thousand group separator and correct decimal char.
             */
            function formatPrecision(value) {
                if (!(value || value === 0)) {
                    return '';
                }
                var formattedValue = parseFloat(value).toFixed(decimals);
                formattedValue = formattedValue.replace('.', decimalSeparator);
                return numberWithCommas(formattedValue);
            }

            function formatViewValue(value) {
                return ngModelCtrl.$isEmpty(value) ? '' : '' + value;
            }

            /**
             * Parse the view value.
             */
            function parseViewValue(value) {
                if (angular.isUndefined(value)) {
                    value = '';
                }
                value = (""+value).replace(decimalSeparator, '.');

                // Handle leading decimal point, like ".5"
                if (value.indexOf('.') === 0) {
                    value = '0' + value;
                }

                // Allow "-" inputs only when min < 0
                if (value.indexOf('-') === 0) {
                    if (min >= 0) {
                        value = null;
                        ngModelCtrl.$setViewValue(formatViewValue(lastValidValue));
                        ngModelCtrl.$render();
                    }
                    else if (value === '-') {
                        value = '';
                    }
                }

                var empty = ngModelCtrl.$isEmpty(value);
                if (empty) {
                    lastValidValue = '';
                    //ngModelCtrl.$modelValue = undefined;
                } 
                else {
                    if (regex.test(value) && (value.length <= maxInputLength)) {
                        if ((value > max) && limitMax) {
                            lastValidValue = max;
                        }
                        else if ((value < min) && limitMin) {
                            lastValidValue = min;
                        }
                        else {
                            lastValidValue = (value === '') ? null : parseFloat(value);
                        }
                    }
                    else {
                        // Render the last valid input in the field
                        ngModelCtrl.$setViewValue(formatViewValue(lastValidValue));
                        ngModelCtrl.$render();
                    }
                }

                return lastValidValue;
            }

            /**
             * Calculate the maximum input length in characters.
             * If no maximum the input will be limited to 16; the maximum ECMA script int.
             */
            function calculateMaxLength(value) {
                var length = 16;
                if (!angular.isUndefined(value)) {
                    length = Math.floor(value).toString().length;
                }
                if (decimals > 0) {
                    // Add extra length for the decimals plus one for the decimal separator.
                    length += decimals + 1; 
                }
                if (min < 0) {
                    // Add extra length for the - sign.
                    length++;
                }
                return length;
            }

            /**
             * Value validator for min and max.
             */
            function minmaxValidator(value, testValue, validityName, limit, compareFunction) {
                if (!angular.isUndefined(testValue) && limit) {
                    if (!ngModelCtrl.$isEmpty(value) && compareFunction) {
                        return testValue;
                    } else {
                        return value;
                    }
                }
                else {
                    if (!limit) {
                        ngModelCtrl.$setValidity(validityName, !compareFunction);
                    }
                    return value;
                }
            }

            /**
             * Minimum value validator.
             */
            function minValidator(value) {
                return minmaxValidator(value, min, 'min', limitMin, (value < min));
            }

            /**
             * Maximum value validator.
             */
            function maxValidator(value) {
                return minmaxValidator(value, max, 'max', limitMax, (value > max));
            }

            /**
             * Function for handeling the blur (leave) event on the control.
             */
            function onBlur() {
                var value = ngModelCtrl.$modelValue;
                if (!angular.isUndefined(value)) {
                    // Format the model value.
                    ngModelCtrl.$viewValue = formatPrecision(value);
                    ngModelCtrl.$render();
                }
            }
            
            /**
             * Function for handeling the focus (enter) event on the control.
             * On focus show the value without the group separators.
             */
            function onFocus() {
                var value = ngModelCtrl.$modelValue;
                if (!angular.isUndefined(value)) {
                    ngModelCtrl.$viewValue = (""+value).replace(".", decimalSeparator);
                    ngModelCtrl.$render();
                }
            }
        }
    }

})();