programming

Leverage Angular without Starting from Scratch

There are days in a life that emerge as stand outs amongst the rest. Some of those days are expected; a wedding perhaps or the birth of a child. Others happen without fanfare. The day I found Angular was one of those days.

I was in the midst of working on the Arch LMS. It had become laborious. The UI I had envisioned was becoming a mess of ajax calls glued together with jQuery. It worked, but it was fragile. That wasn't really jQuery's fault. I used it for something it wasn't meant to do and tracking all those events was impossible.

Seeking something else, I found myself watching a presentation on Knockout. Not bad. It did lead me to Underscore.js, but for some reason it wasn't clicking for me. Perhaps if I'd given it more time. Still, I thought there had to be a better way. Then I found Angular.

Maybe it was because of my background in WebObjects, but the idea of creating directives (components in wo-speak) on the client side was exciting. And all those directives need to work, was a bit of json. Lovely. After 16 years, Web development could be fun again.

I wanted to scrap the whole application and start from scratch, but quickly realized I couldn't. That would take too long and there was too much to do. Instead, I had to find a way to begin incorporating Angular immediately, while keeping my existing codebase intact.

My existing application utilized a lot of document level events that I could trigger from any page with click handlers. I simply passed the required options through those. For example:

// main script
$(document).on('edit-plan', function(e, options){
	// edit the lesson plan with options.id
});

// somewhere else, perhaps the lesson plans page
$('.edit-plan').click(function(e){
	e.preventDefault(); 
	$(document).trigger('edit-plan', {
		id: $(this).data('id')
	});
});

All I needed to do to begin using Angular was tap into the edit-plan event. Already using RequireJS, I wrote a function that allowed me to inject a module into the dom and bootstrap it. Here is the function:

/**
 * xg-bootstrap.js
 *
 * Injects an Angular module into the DOM asynchronously and returns a promise 
 * to resolve when the injection is complete. 
 */ 
define(['angular'], function(angular) {
	return function(app, appEl, $selector){ 
		var d; 
		angular.injector(['ng']).invoke(['$q', function($q){ 
			d = $q.defer(); 
			angular.element($selector || 'body').append(appEl); 			angular.bootstrap(appEl, [app.name]);
			d.resolve({ 
				'app': app, 
				'appEl': appEl, 
				'scope': appEl.scope() 
			}); 
		}]); 
		return d.promise;
	}; 
});

I could then create a module or mini-app unencumbered by any other dependencies and inject it like this:

// mods/alert/app.js
define(['angular', 'libs/xg-bootstrap', 'text!mods/alert/template.html'], function(angular, xgBootstrap, template){
	return function module(){
		var appEl = angular.element(template);
		var app = angular.module('AlertApp', []);
		app.controller('AlertAppCtrl', ['$scope', '$sce', function($scope, $sce){
			$scope.message = "";
			$scope.close = function(){
				appEl.remove();
			};
			$scope.$watch('message', function(){
				$scope.messageHtml = $sce.trustAsHtml($scope.message);
			});
		}]);
		return xgBootstrap(app, appEl);
	};
});
<div class="xg-modal">
	<div class="xg-alert"><button ng-click="close()">Close</button></div>
</div>

And to invoke the app:

// main.js 
$(document).on('alert', function(event, options){ 
	requirejs(["mods/alert/app"], function(module){ 
		var promise = new module(); 
		promise.then(function(app){ 
			app.scope.$apply(function(){ 
				app.scope.message = options.message; 
			});
		}); 
	}); 
});
// within some html page 
$('#alert-button').click(function(event){ 
	event.preventDefault(); 
	$(document).trigger('alert', { 
		message: "À tout le monde, hello." 
	}); 
});

So that's what I did, one by one replacing my jQuery-based dialogs and code with Angular based ones. I was able to immediately use Angular, while moving forward with the application. And it was fun.