Tuesday, October 15, 2013

Knockout style computed fields in AngularJS (sort of)

AngularJS has a few peculiar things. Here's a pair of them.

Suppose you have 3 input text fields - f1, f2, f3. And want you want is to have f3 a "computed" field based on what's the values on f1 and f2. Simple enough, right? Not exactly.

If you do did something like this:

<div ng-app>  
   <div ng-controller="CTRL">  
     <input type="text" ng-model="f1" />  
     <input type="text" ng-model="f2" />  
     <input type="text" value="{{total()}}" />  
     <p>{{'Angular works!'}}</p>  
   </div>  
 </div> 

And your Angular script is like this:

function CTRL ($scope) {
    $scope.f1= 3;
    $scope.f2= 4;
    $scope.total = function() {return $scope.f1 + $scope.f2;};
}

You are in for a bad time. You will notice it will work at the start but when you change the value on either f1 or f2, the total field is showing a concatenated string and not a sum. DAFUQ!  Peculiarity #1. The fix is actually pretty easy if you use a directive.

var app = angular.module('intDirective', []);

app.directive('integer', function(){
    return {
        require: 'ngModel',
        link: function(scope, ele, attr, ctrl){
            ctrl.$parsers.unshift(function(viewValue){
                return parseInt(viewValue);
            });
        }
    };
});

To use this is to add a ng-app="intDirective" property to the root div and the input tags should look like this:

<div ng-app='intDirective'>  
   <div ng-controller="CTRL">  
     <input type="text" ng-model="f1" integer/>  
     <input type="text" ng-model="f2" integer/>  
     <input type="text" value="{{total()}}" />  
     <p>{{'Angular works!'}}</p>  
   </div>  
 </div> 

OK, its looking good but try typing in a character on either f1 or f2? Yes, another thing we have do. We have to check the value being typed it is not shit (sometimes called Validation).

var app = angular.module('intDirective', []);
var INTEGER_REGEXP = /^\-?\d*$/;
app.directive('integer', function(){
    return {
        require: 'ngModel',
        link: function(scope, ele, attr, ctrl){
            ctrl.$parsers.unshift(function(viewValue){
                ctrl.$parsers.unshift(function(viewValue) {
                if (INTEGER_REGEXP.test(viewValue)) {
                   // it is valid
                   ctrl.$setValidity('integer', true);
                   return parseInt(viewValue);
                } else {
                   // it is invalid, return undefined (no model update)
                   ctrl.$setValidity('integer', false);
                   return undefined;
                }
            });
        }
    };
});

What's left is a simple $watch function to mimic Knockout computed fields (Peculiarity #2). Just add this snippet inside the controller.

    $scope.$watch(function(){
        return $scope.val1 + $scope.val2;
    }, function(newValue, oldValue){
        $scope.computed = newValue;
    });

There.