An AngularJS directive for handling decimals

An AngularJS directive for handling decimals

I’ve been working on a web application built with AngularJSfor a client. Angular is a great tool for working with the UI of a web application.

If you don’t know what AngularJS is you probably won’t get too much out of this blog post, but AngularJS is a JavaScript framework maintained by Google. Its strong sides are single-page applications and its two-way binding between the model and the view. It has a bit of a steep learning curve and their own tutorial isn’t really the best – there are better tutorials out there. Once you get the hang of it, though, it’s quite nice to work with – in most cases at least. Check it out at angularjs.org if you’re interested.

Numbers and decimals

This brings me to the point of this blog post. I thought working with HTML5 and Angular would make things easy concerning numbers and decimals – even when working with different locales – but no, that wasn’t the case.

The client wanted some input fields to be rounded/truncated at 2 decimals and some to 0 decimals – and the decimal point had to always be a ,. To achieve this I needed to create my own directive to format the input value when it changes to strip away the excess decimals or add missing one and be able to handle both a . and a , as the decimal point when typing the value.

To begin with I was just using a standard <input type="number> with my new directive and everything seemed to work as expected. After some testing the client came back and said my solution wasn’t working in IE11 (of course, right?). Of course I had only been testing in Chrome – I would have thought that the browsers were going to – or at least AngularJS would force them to – behave the same, but apparently not. IE11 didn’t like using a , as a decimal point even though that is the standard for my locale.

I had to revise my situation a little, but in the end I think it turned out quite well. I had to change the input’s type from number to text to get the browsers to just handle the value as text and let the directive worry about parsing and formatting the value.

You can see my final directive below including an example of how to use it. You can easily try it out yourself – you don’t need to know anything about AngularJS – just copy the two code examples to each their file and run it locally.

angular-decimal.js

var app = angular.module("angular-decimals", []);
app.directive("decimals", function ($filter) {
    return {
        restrict: "A", // Only usable as an attribute of another HTML element
        require: "?ngModel",
        scope: {
            decimals: "@",
            decimalPoint: "@"
        },
        link: function (scope, element, attr, ngModel) {
            var decimalCount = parseInt(scope.decimals) || 2;
            var decimalPoint = scope.decimalPoint || ".";

            // Run when the model is first rendered and when the model is changed from code
            ngModel.$render = function() {
                if (ngModel.$modelValue != null && ngModel.$modelValue >= 0) {
                    if (typeof decimalCount === "number") {
                        element.val(ngModel.$modelValue.toFixed(decimalCount).toString().replace(".", ","));
                    } else {
                        element.val(ngModel.$modelValue.toString().replace(".", ","));
                    }
                }
            }

            // Run when the view value changes - after each keypress
            // The returned value is then written to the model
            ngModel.$parsers.unshift(function(newValue) {
                if (typeof decimalCount === "number") {
                    var floatValue = parseFloat(newValue.replace(",", "."));
                    if (decimalCount === 0) {
                        return parseInt(floatValue);
                    }
                    return parseFloat(floatValue.toFixed(decimalCount));
                }

                return parseFloat(newValue.replace(",", "."));
            });

            // Formats the displayed value when the input field loses focus
            element.on("change", function(e) {
                var floatValue = parseFloat(element.val().replace(",", "."));
                if (!isNaN(floatValue) && typeof decimalCount === "number") {
                    if (decimalCount === 0) {
                        element.val(parseInt(floatValue));
                    } else {
                        var strValue = floatValue.toFixed(decimalCount);
                        element.val(strValue.replace(".", decimalPoint));
                    }
                }
            });
        }
    }
});

Example usage

<html>
<head>
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.min.js"></script>
  <script type="text/javascript" src="angular-decimals.js"></script>

  <script type="text/javascript">
    var myApp = angular.module("myApp", ["angular-decimals"]);
  </script>
</head>

<body ng-app="myApp">
  <div>
    <p><input type="text" ng-model="quantity" decimals="2" decimal-point="."></p>
    <p>Actual value: {{quantity}}</p>
  </div>
</body>
</html>

Using the directive is pretty straightforward as you can see from the example. You just add a decimals attribute to a <input type="text"> specifying how many decimals should be shown and optionally which character to use for decimal point (default is .).

If you want to use this directive in your own Angular app just include angular-decimals.js in your HTML after angular.js itself and remember to add angular-decimals to your app’s dependencies just as I’ve done to myApp in the example.

Conclusion

Localization, decimals, dates and time zones are never fun to work with, but at least I’ve been able to make decimals and numbers a bit easier to work with if you are using Angular and needs to control the number of decimals with this small Angular directive.