Hacking AngularJS

Alexander Todorov / Monday, June 17, 2013

Updated the sample app to use Angular 1.2 :)

In this blog post, i would like to cover two things:

1) How I've extended AngularJS in order to support transaction logs (detailed diffs) for arrays. I've forked the project on github:

https://github.com/attodorov/angular.js

2) How I've created a custom Angular directive for the Ignite UI grid

As a result, you will see how the IgniteUI Grid integrates very nicely with Angular, and also supports full two-way databinding. 

The attached project contains everything you need to see it in action for yourself, just unzip and run angular.html. 

Let's start with some background information on the way AngularJS performs updates between models and views. I wouldn't like to elaborate on this myself, because it is already covered in great detail in the following SO post:

http://stackoverflow.com/questions/9682092/databinding-in-angularjs

It's a very elegant approach, a lot more elegant than change listeners. Unfortunately, if you are binding to a two-dimensional array of objects, and you modify a property of some object, there is no way for you to know what exactly got changed. Angular gives you the old array and the new array, and you have the choice to diff them again, but that's not optimal. That's why I've changed the equals and $digest functions in Angular in order to support passing detailed information about what has actually changed. This way, if a single property changes somewhere in my array, and i've bound it to the Ignite UI grid using Angular, i can re-render only the cell which binds to that prop, without applying equals recursively to the whole array again. Here is a snippet of how this works, notice the "diff" argument:

scope.$watch(attrs.source, function (value, last, currentValue, diff) {
	if (Array.isArray(diff)) {
		for (var i = 0; i < diff.length; i++) {
			// update cell values
			if (!diff[i].txlog) {
				continue;
			}
			for (var j = 0; j < diff[i].txlog.length; j++) {
				// get the td
				var colIndex = $("#" + element.attr("id") + "_" + diff[i].txlog[j]["key"]).index();
				var key = scope[attrs.source][diff[i].index][attrs.primarykey];
				var td = element.find("tr[data-id='" + key + "']").children().get(colIndex);
				$(td).html(diff[i].txlog[j]["newVal"]);
			}
		}
	}
}, true);

The "diff" argument is an array of objects which have the following structure:

{index: , txlog: []}

where each tx has the following format:

{key: , oldVal: , newVal: }

Let's go through our page step by step. We first need to reference the modified AngularJS library (you can obtain it from my forked project, i've included the "build" folder which contains combined & minified resources). You also need to reference the controllers script, as well as the script which contains the custom igniteui directives. 

<head>
	<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" type="text/css"></link>
	<link rel="stylesheet" href="css/themes/infragistics/infragistics.theme.css" type="text/css"></link>
	<link rel="stylesheet" href="css/structure/infragistics.css" type="text/css"></link>
	<script type="text/javascript" src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
	<script type="text/javascript" src="http://code.jquery.com/ui/1.10.3/jquery-ui.js"></script>
	<script type="text/javascript" src="js/angular.min.js"></script>
	<script type="text/javascript" src="js/infragistics.core.js"></script>
	<script type="text/javascript" src="js/infragistics.lob.js"></script>
	<script src="js/controllers.js"></script>
	<script src="js/igniteui-directives.js"></script>
	<title>Angular.JS and Ignite UI</title>
</head>

The controllers.js script has a really simple format, it just defines the NorthwindCtrl which we will later specify in the HTML, following Angular conventions, and it returns the data source in a JSON format - the datasource itself is hardcoded into the controller implementation, for simplicity.

function NorthwindCtrl($scope) {
    $scope.northwind = [ ];

}

The next thing we will do is set the ng-controller directive for the body, which in our case will be NorthwindCtrl:

<body ng-controller="NorthwindCtrl">

Note that we also need to set the ng-app directive to the html tag, so that Angular can parse custom and reserved directives and templates correctly:

We then declare our custom ignitegrid directive in the following way:

id="grid1" data-source="northwind" data-height="400px" data-updating="true" data-primarykey="ProductID">
</ignitegrid>


I am also adding a simple table to the page, so that we can see how two-way updates work in real time between the two UIs. 

<table id="simpletable">
	<tbody>
		<tr ng-repeat="product in northwind">
			<td>{{product.ProductID}}</td>
			<td><input type="text" ng-model="product.ProductName"></input></td>
			<td>{{product.QuantityPerUnit}}</td>
			<td>{{product.UnitPrice}}</td>
		 </tr>
    </tbody>
</table>

You can see that the way the ignite grid is defined is by using a completely custom html tag, as well as some data-* attributes that reflect the widget options. Let's have a look at our custom directives implementation, as well as the way we propagate updates from grid's Updating to the angular model:

angular.module('igApp', []).directive('ignitegrid', function () {
	return {
		restrict: "E",
		template: "<table></table>",
		replace: true,

The restrict: "E" declaration means that our custom directive is a custom element, not an attribute. We also want the default tag and its child contents to be replaced with an emptytag, where the actual grid widget is going to be initialized.

Going further, the most important part of our custom directive is the "link" function, which generates the initialization options and then instantiates the igGrid widget on the

element. It also handles the iggridupdatingeditrowended client-side event where  we set new values in the angular model, and then call $apply so that all associated listeners are fired and the DOM that binds to those objects is updated. "northwind",  which we reference from the scope, is basically defined in our Angular controller.

angular.module('igApp', []).directive('ignitegrid', function () {
	return {
		restrict: "E",
		template: "
",
		replace: true,
		link: function (scope, element, attrs) {
			//initialize an ignite UI grid on element, using attrs.igniteuiModel as the data source
			if (!scope.hasOwnProperty(attrs.source)) {
				throw new Error("The data source (dataSource) does not exist in the current context");
			}
			var ds = scope[attrs.source], opts = {};
			if (typeof (attrs.autogeneratecolumns) !== "undefined") {
				opts.autoGenerateColumns = attrs.autogeneratecolumns === "true" ? true : false;
			}
			opts.dataSource = ds;
			//opts.columns = scope.columns;
			if (attrs.height) {
				opts.height = attrs.height;
			}
			if (attrs.updating && attrs.updating === "true") {
				if (!Array.isArray(opts.features)) {
					opts.features = [];
				}
				opts.autoCommit = true;
				opts.features.push({name: "Updating"});
				// we need to listen for updates in order to support two-way databinding in the grid
				// ensure that we don't handle it twice or create any recursion 
				// the same can/should be done for cell editing, adding and deleting rows
				element.on("iggridupdatingeditrowended", function (e, args) {
					// we need the data source from the scope, but without triggering $digest for the grid itself
					// note that there may be other subscribers
					var ds = angular.element(element).scope()[attrs.source];
					for (var i = 0; i < ds.length; i++) {
						if (ds[i][attrs.primarykey] === args.rowID) {
							ds[i] = args.values;
							break;
						}
					}
					// force $apply
					angular.element(element).scope().$apply();
				});
			}
			if (attrs.primarykey) {
				opts.primaryKey = attrs.primarykey;
			}
			element.igGrid(opts);
			// watch for changes from the data source to the view
			scope.$watch(attrs.source, function (value, last, currentValue, diff) {
				if (Array.isArray(diff)) {
					for (var i = 0; i < diff.length; i++) {
						// update cell values
						if (!diff[i].txlog) {
							continue;
						}
						for (var j = 0; j < diff[i].txlog.length; j++) {
							// get the td
							var colIndex = $("#" + element.attr("id") + "_" + diff[i].txlog[j]["key"]).index();
							var key = scope[attrs.source][diff[i].index][attrs.primarykey];
							var td = element.find("tr[data-id='" + key + "']").children().get(colIndex);
							$(td).html(diff[i].txlog[j]["newVal"]);
						}
					}
				}
			}, true);
		}
	}
});

To sum up - using the approach outlined in this blog post, and the forked version of angularJS, you get seamless support of two-way updating and nice integration with the ignite UI grid using custom directives. You can apply the same approach to other IgniteUI widgets. 

angular-app-1.2.rar