jenryb jenryb - 4 months ago 16
AngularJS Question

Money formatting directive in Angular

I need a directive for filtering a field for currency, so a user just needs to type and the decimal is implied.

Needs:


  1. Format decimal field as user types -



Start at the hundredths place as the user types. So they would type "4" and see "0.04", type "42" and see "0.42", type 298023 and see "2980.23"


  1. Field must be a number

  2. Must allow negatives
    -

  3. Allow 0.00 as a number input

  4. Ideally would use type="number" but "type=text" is okay

  5. You should be able to clear the field to be empty.



The ng-currency filter does not fulfill these requirements as is. Please see behaviour in plunkers to see what I mean.

My First Plunker has `input = text' and allows negative numbers. One problem is that you cannot type a negative as the very first number. When you clear the field, it returns to '0.00' but it should completely clear.

app.directive('format', ['$filter', function ($filter) {
return {
require: 'ngModel', //there must be ng-model in the html
link: function (scope, elem, attr, ctrl) {
if (!ctrl) return;

ctrl.$parsers.unshift(function (viewValue, modelValue) {
var plainNumber = viewValue.replace(/[^-+0-9]/g,'');
var newVal = plainNumber.charAt(plainNumber.length-1);
var positive = plainNumber.charAt(0) != '-';
if(isNaN(plainNumber.charAt(plainNumber.length-1))){
plainNumber = plainNumber.substr(0,plainNumber.length-1)
}
//use angular internal 'number' filter
plainNumber = $filter('number')(plainNumber / 100, 2).replace(/,/g, '');
if(positive && newVal == '-'){
plainNumber = '-' + plainNumber;
}
else if(!positive && newVal == '+'){
plainNumber = plainNumber.substr(1);
}
plainNumber.replace('.', ',');

//update the $viewValue
ctrl.$setViewValue(plainNumber);
//reflect on the DOM element
ctrl.$render();
//return the modified value to next parser
return plainNumber;
});
}
};

}]);


My Second Plunker has
input = text
and allows for negative input. Like the first plunker, it won't allow a negative as the first character, only after numbers are typed. The second is that it starts at the tenths place instead of the hundredths. (if you type '3' you should see '0.03' but here it shows '0.3')

app.directive('inputRestrictor', [function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, ngModelCtrl) {
var pattern = /[^.0-9+-]/g;

function fromUser(text) {
if (!text)
return text;

var rep = /[+]/g;
var rem = /[-]/g;
rep.exec(text);
rem.exec(text);

var indexp = rep.lastIndex;
var indexm = rem.lastIndex;
text = text.replace(/[+.-]/g, '');
if (indexp > 0 || indexm > 0) {
if (indexp > indexm) text = "+" + text; // plus sign?
else text = "-" + text;
}

var transformedInput = text.replace(pattern, '');
transformedInput = transformedInput.replace(/([0-9]{1,2}$)/, ".$1")
ngModelCtrl.$setViewValue(transformedInput);
ngModelCtrl.$render();
return transformedInput;
}

ngModelCtrl.$parsers.push(fromUser);
}
};
}]);


How can I reconcile these solutions or tailor one to meet the requirements? I want to avoid extra libraries or add-ons. I have been told that the best approach would be to study the source for the currency filter, and recreate that filter with the additional requirements.I would love to do this, but I really don't have the skills for it right now. These two directives are what I have.

Answer

.:: Updated Answer - July 14 ::.


Check this simple directive:

app.directive('price', [function () {
    return {
        require: 'ngModel',
        link: function (scope, element, attrs, ngModel) {
            attrs.$set('ngTrim', "false");

            var formatter = function(str, isNum) {
                str = String( Number(str || 0) / (isNum?1:100) );
                str = (str=='0'?'0.0':str).split('.');
                str[1] = str[1] || '0';
                return str[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') + '.' + (str[1].length==1?str[1]+'0':str[1]);
            }
            var updateView = function(val) {
                scope.$applyAsync(function () {
                    ngModel.$setViewValue(val || '');
                    ngModel.$render();
                });
            }
            var parseNumber = function(val) {
                var modelString = formatter(ngModel.$modelValue, true);
                var sign = {
                    pos: /[+]/.test(val),
                    neg: /[-]/.test(val)
                }
                sign.has = sign.pos || sign.neg;
                sign.both = sign.pos && sign.neg;

                if (!val || sign.has && val.length==1 || ngModel.$modelValue && Number(val)===0) {
                    var newVal = (!val || ngModel.$modelValue && Number()===0?'':val);
                    if (ngModel.$modelValue !== newVal)
                        updateView(newVal);

                    return '';
                }
                else {
                    var valString = String(val || '');
                    var newSign = (sign.both && ngModel.$modelValue>=0 || !sign.both && sign.neg?'-':'');
                    var newVal = valString.replace(/[^0-9]/g,'');
                    var viewVal = newSign + formatter(angular.copy(newVal));

                    if (modelString !== valString)
                        updateView(viewVal);

                    return (Number(newSign + newVal) / 100) || 0;
                }
            }
            var formatNumber = function(val) {
                if (val) {
                    var str = String(val).split('.');
                    str[1] = str[1] || '0';
                    val = str[0] + '.' + (str[1].length==1?str[1]+'0':str[1]);
                }
                return parseNumber(val);
            }

            ngModel.$parsers.push(parseNumber);
            ngModel.$formatters.push(formatNumber);
        }
    };
}]);

And use it like this:

<input type="text" ng-model="number" price >

See it live in this PLUNKER (July 14)

Comments