Creating A RequireJS Service For AngularJS Applications
Yesterday, I blogged about Tiny Test JS, my personal JavaScript unit testing framework for experimenting with object modeling and Object Oriented Programming (OOP). Tiny Test JS is an AngularJS application that runs unit tests that have been defined using RequireJS. It also happens to be loaded and bootstrapped within a RequireJS callback, but that's another topic. To help AngularJS run the unit tests, I created an RequireJS "service" that would keep the asynchronous module-loading workflow in step with the AngularJS lifecycle.
View this demo in my JavaScript-Demos project on GitHub.
As far as AngularJS is concerned, everything "happens" inside of the $apply / $digest lifecycle. This is the period in which AngularJS performs its "dirty checking" of the $scope data, which may or may not precipitate the invocation of various watchers and data-bindings. As such, if your code doesn't trigger an $apply / $digest, AngularJS doesn't know that your code has changed anything.
When you load RequireJS modules, RequireJS loads the target JavaScript files asynchronously before invoking your callbacks asynchronously. This asynchronous nature means that your RequireJS callbacks will be invoked outside of the AngularJS lifecycle. As such, you have to explicitly call $scope.$apply() within your callbacks so that AngularJS will propagate your asynchronous changes throughout the rest of the application.
To encapsulate this requirement (no pun intended), I created a Require service that simply proxies the require() function and takes care of calling $scope.$apply() within the callbacks. Not only does this turn the global RequireJS reference into an AngularJS-injectable, it also leaves your require() statements free of the necessary lifecycle cruft.
To see this in action, I created a RequireJS module that defines a list of friends. This list is then loaded using RequireJS and rendered using AngularJS:
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Creating A RequireJS Service For AngularJS
</title>
<style type="text/css">
a[ ng-click ] {
cursor: pointer ;
text-decoration: underline ;
}
</style>
</head>
<body ng-controller="AppController">
<h1>
Creating A RequireJS Service For AngularJS
</h1>
<p>
<a ng-click="loadData()">Load Remote Data with RequireJS</a>
</p>
<!-- BEGIN: Friends List. -->
<ul ng-show="friends.length">
<li ng-repeat="friend in friends">
{{ friend.name }} — <em>"{{ friend.catchPhrase }}"</em>
</li>
</ul>
<!-- END: Friends List. -->
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.0.7.min.js"></script>
<script type="text/javascript" src="../../vendor/require/require-2.1.9.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope, require ) {
// Default to an empty list of friends - this data
// will be loaded from the remote data module using
// require.
$scope.friends = [];
// ---
// PUBLIC METHODS.
// ---
// I load the remote friends data.
$scope.loadData = function() {
require(
[ "friends" ],
function( newFriends ) {
$scope.friends = newFriends;
}
);
};
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I am the Require service that proxies the global RequireJS
// reference and helps it integrate into the AngularJS workflow.
app.factory(
"require",
function( $rootScope ) {
// Since the callbacks in the RequireJS module will
// take the control-flow outside of the normal
// AngularJS context, we need to create a proxy that
// will automatically alert AngularJS to the execution
// of the callback. Plus, this gives us an opportunity
// to add some error handling.
function requireProxy( dependencies, successCallback, errorCallback ) {
// Make sure the callbacks are defined - this makes
// the logic easier down below.
successCallback = ( successCallback || angular.noop );
errorCallback = ( errorCallback || angular.noop );
// NOTE: This "require" reference is the core,
// global reference to RequireJS.
require(
( dependencies || [] ),
function successCallbackProxy() {
var args = arguments;
$rootScope.$apply(
function() {
successCallback.apply( this, args );
}
);
},
function errorCallbackProxy() {
var args = arguments;
$rootScope.$apply(
function() {
errorCallback.apply( this, args );
}
);
}
);
}
return( requireProxy );
}
);
</script>
</body>
</html>
As you can see, the Require service is rather straightforward. It simply calls the core require() function and then invokes your callbacks within an $apply() callback. This tells AngularJS to perform a "dirty check" on the $scope data after your RequireJS callbacks have finished executing.
The interplay between AngularJS and RequireJS is fairly fascinating. This is my first foray into the topic; so hopefully, there will be more to come in the future. In particular, I'd love to explore the lazy-loading of AngularJS modules.
Want to use code from this post? Check out the license.
Reader Comments
Hi Ben,
Just to confirm my earlier post to your first example of lazy loading, the version of AngularJS (v1.0.7) you use in the demo page there and here work just fine. I tried the current production version 1.2.18 of AngularJS, and it too errors out here and in the first example.
So it looks like something has changed. Sorry I can't help more.
Thanks for this! I created a factory for lazy loading routes using requirejs and stumbled upon this when I was trying to write a test for it. I was trying to figure out how to "mock" requirejs. (see below for my impl)
I hadn't yet used my version of the require service inside a controller but thought it might come in handy; so I implemented the $scope.$apply(...); Works like a charm!
My require-proxy test:
describe("RequireProxy:", function() {
define("test-file", {success: true});
var target, oldWindowRequire;
beforeEach(function(done) {
oldWindowRequire = window.require; // capture before jasmine man-handles it
spyOn(window, "require");
done();
});
beforeEach(inject(function(require) { // my require-proxy service
target = require;
});
afterEach(function() {
window.require = oldWindowRequire; // reset so nothing else is impacted by our spy
});
it("should call success callback", function(done) {
var successSpy = jasmine.createSpy("successSpy");
target(["test-file"], successSpy);
// success callback is wrapped in a proxy, so it should call our successSpy -- the second argument in list
window.require.calls.mostRecent().args[1]();
expect(successSpy).toHaveBeenCalled();
done(); // trigger my "$q" -- used for my dependencies in routes
});
...
});
Thanks a lot for this article!!!!!!
I need to call a legacy code from Angular and not the other way around like everyone is asking - so thanks to you I found a solution - how did u figure this out...?