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.

Wednesday, October 9, 2013

AngularJS with underpants

What I really meant was AngularJS with underscore but what the heck.

AngularJS as powerful as it is doesn't really have a lot utility methods and this is where underscore comes in. Underscore provides 80-odd functions that support both the usual functional suspects: map, select, invoke — as well as more specialized helpers.

To get the most of underscore with AngularJS, you have to "provide" underscore into AngularJS via a factory method:
var myapp= angular.module('underscore', []);
 myapp.factory('_', function() {
  return window._; // underscore has been loaded before this script
 }); 
With that, we can now inject underscore into our controllers:
myapp.controller("SomeCtrl",[$scope, _, function($scope, _){
    $scope.maxVal = _.max([1,2,3]); // Returns 3
});

Which leads to a couple of pretty handy stuff like checking for undefined values:
myapp.controller("SomeCtrl",[$scope, _, function($scope, _){
    if(_.isUndefined($scope.someValue){
          // do something because someValue is undefined
    }
});
Or do a search over some array using some attribute without resorting to a loop.
myapp.controller("SomeCtrl",[$scope, _, function($scope, _){
    
var data = [{model:"T", manufacturer: "Nokia"},
            {model:"S", manufacturer:"Samsung"},
            {model:"r8", manufacturer:"Cherry Mobile"},
            {model:"One", manufacturer:"HTC"}];
//Code to fetch a Cherry Mobile phone
var phone = _.where(data, {manufacturer: "Cherry Mobile"});
// phone should be [{model:"r8", manufacturer:"Cherry Mobile"}]
});
By combining these awesome frameworks, you're setting up yourself to dish out some serious can of whoop ass.