Sunday, July 7, 2013

Seamless numeric localization with AngularJS

Have you seen seamless numeric localization with jQuery?

Neither do I, so I'll just offer you an AngularJS approach.

To make an input locale-aware we just need to add an attribute to make it capable as such. And make a directive for that attribute.

Localization with AngularJS is very easy. And we don't have to change anything on the controller to make it work on other language.

To make an input locale-aware we just need to add an attribute to make it capable as such. And make a directive for that attribute.


Old code:
<input type="text" ng-model="untaintedNumber"/>

Locale-aware code:
<input type="text" ng-model="untaintedNumber" numeric decimal-places="decPlaces" ng-change="showInLog()">



This is the definition for numeric and decimal-places attribute:

module.directive('numeric', function($filter, $locale) {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function(scope, element, attr, ngModel) {
        
            var decN = scope.$eval(attr.decimalPlaces); // this is the decimal-places attribute

        
            // http://stackoverflow.com/questions/10454518/javascript-how-to-retrieve-the-number-of-decimals-of-a-string-number
            function theDecimalPlaces(num) {
                   var match = (''+num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
                   if (!match) { return 0; }
                      return Math.max(
                       0,
                       // Number of digits right of decimal point.
                       (match[1] ? match[1].length : 0)
                       // Adjust for scientific notation.
                       - (match[2] ? +match[2] : 0));
            }
        
            function fromUser(text) {                   
                var x = text.replaceAll($locale.NUMBER_FORMATS.GROUP_SEP, '');
                var y = x.replaceAll($locale.NUMBER_FORMATS.DECIMAL_SEP, '.');
                                           
                return Number(y); // return a model-centric value from user input y
            }

            function toUser(n) {
                return $filter('number')(n, decN); // locale-aware formatting
            }
        
            ngModel.$parsers.push(fromUser);
            ngModel.$formatters.push(toUser);
        
            element.bind('blur', function() {
                element.val(toUser(ngModel.$modelValue));
            });
        
            element.bind('focus', function() {            
                var n = ngModel.$modelValue;
                var formattedN = $filter('number')(n, theDecimalPlaces(n));
                element.val(formattedN);
            });
        
        } // link
    }; // return
}); // module


String replaceAll definition:

String.prototype.replaceAll = function(stringToFind,stringToReplace){
    if (stringToFind === stringToReplace) return this;

    var temp = this;
    var index = temp.indexOf(stringToFind);
        while(index != -1){
            temp = temp.replace(stringToFind,stringToReplace);
            index = temp.indexOf(stringToFind);
        }
        return temp;
};


This is the controller, nothing needed be changed. The controller action changeValue keeps the old code.

function Demo($scope) {

    $scope.decPlaces = 2;

    $scope.untaintedNumber = 1234567.8912;

    $scope.changeValue = function() {
    
        // The model didn't change to string type, hence we can do business as usual with numbers.
        // The proof that it doesn't even change to string type is we don't even need 
        // to use parseFloat on the untaintedNumber when adding a 7 on it. 
        // Otherwise if the model's type mutated to string type, 
        // the plus operator will be interpreted as concatenation operator: http://jsfiddle.net/vuYZp/
        // Luckily we are using AngularJS :-)        
        $scope.untaintedNumber = $scope.untaintedNumber + 7;
    
        // contrast that with jQuery where everything are string:
        // you need to call both $('elem').val() and Globalize's parseFloat, 
        // then to set the value back, you need to call Globalize's format.
    
        /*              
        var floatValue = Globalize.parseFloat($('#uxInput').val());
        floatValue = floatValue * 2;        
        var strValue = Globalize.format(floatValue, "n4");
        $('#uxInput').val(strValue);
        */
    
    };

    $scope.showInLog = function() {
        console.log($scope.untaintedNumber);     
    };

}


Live Code: http://jsfiddle.net/F5PuQ/

1 comment: