In a perfect world you'd never have to use jQuery with AngularJS, but since AngularJS does not do everything jQuery does, and perhaps most importantly does not have a plugin architecture supporting jQuery plugins, you're probably going to want to use it somewhere.
Want to use that charting library? How about that carousel widget? What about something simple in a directive, like selecting some nodes by class? Not in AngularJS, but there is jQuery to the rescue.
When working with elements within AngularJS, you never get a raw Element object. You always get an object with a jqLite (or jQuery, depending) wrapper around it. This is very handy since you are given some convenience methods, and can always access the underlying Element object anyway:
angular.module('myApp').directive('someInputTagRelatedDirective', function() {
return function(scope, elm, attrs) {
// elm.get(0) does not work in jqLite
console.log(e[0].getAttribute('type'));; // this works
};
});
As mentioned previously, if you want to see everything you can do with a jqLite object, look here: http://docs.angularjs.org/api/angular.element. If this happens to be everything you imagine you could possibly want to do with your application, great! You don't need jQuery. You are among the lucky few.
For the rest of us, note that including jQuery will actually replace jqLite. That means angular.element()
becomes an alias for jQuery or $
. As a best practice, it may be a good idea to use $ or jQuery anyway when you want to use non-jqLite functions, since if you forget to include jQuery, it will be more immediately obvious why your code is not functioning and why your functions are undefined.
The AngularUI project (http://angular-ui.github.com/) provides many helpful and handy directives, but one in particular is especially handy. This is the uiJq directive. It allows you to execute any jQuery function that you would execute against a jQuery object by placing that function on the DOM node itself. For example:
<div id="foo" ui-jq="hide">I'm hidden</div>
This will hide the div immediately. To use an animation, you can do this:
<div id="foo" ui-jq="hide" ui-options="{duration: 500}">I'm hidden</div>
This will hide the div over a period of 500ms. The equivalent in JS would be:
$('#foo').hide({duration: 500});
As stated before, you can use this directive with any jQuery function (that is applied to a jQuery object; stuff like $.map
is not applicable). That includes any jQuery plugins you may be using. You can pass any options to these functions using the uiOptions attribute object.
That brings us to one of the most popular jQuery plugins out there, Twitter Bootstrap (http://twitter.github.com/bootstrap/).
Bootstrap is a great companion to AngularJS, and the two work very well together. It just so happens that you can use virtually any Bootstrap function seamlessly with AngularUI and AngularJS. AngularUI has a sub-project, called Angular Bootstrap (https://github.com/angular-ui/bootstrap), that allows you many convenience methods for Bootstrap functionality.
In addition to this, Dean Sofer (http://deansofer.com/), one of the architects of AngularUI, has provided many examples of using Bootstrap without any extra directives here: https://gist.github.com/4464334. Either AngularUI's Bootstrap sub-project or Dean's examples will serve you well; take a look at both and decide which approach is right for you.
Another option for handling modals is to write a modal factory. This allows you to programatically launch modal dialogs, return promises, and do all sorts of fun stuff. I find this method to be more natural and powerful than using a directive to handle these cases. Here's an example, which we'll add to our blog module:
// Modal factory gives you a Modal dialog. Bootstrap required!
blog.factory('Modal', function ($q, $templateCache, $http, $compile) {
// sets some properties and initializes a deferred to play with.
// scope: Scope object to apply to template
// template_id: ID or path of AngularJS template or partial
// options: object full of options to give to Bootstrap's modal() upon open
var Modal = function (scope, template_id, options) {
options = options || {};
this.scope = scope;
this.template_id = template_id;
this.options = {};
angular.extend(this.options, {show: false}, options);
this.dfrd = $q.defer();
};
// shows a modal dialog; returns a promise to be resolved
// when the dialog is opened.
// does not check if one is already opened.
Modal.prototype.open = function () {
var template, that = this;
if (this.modal) {
this.dfrd = $q.defer();
this.modal.modal('show');
this.dfrd.resolve();
return this.dfrd.promise;
}
template = $templateCache.get(this.template_id);
// cache miss
if (angular.isUndefined(template)) {
$http.get(this.template_id).success(function (data) {
template = data;
that.modal = $compile(template)(that.scope).modal(that.options);
that.modal.modal('show');
that.dfrd.resolve();
}).error(function () {
throw new Error('unable to find template "' + that.template_id + '" anywhere. maybe template gnomes stole it?');
});
} else {
this.modal = $compile(template)(this.scope).modal(this.options);
this.modal.modal('show');
this.dfrd.resolve();
}
return that.dfrd.promise;
};
// closes a modal dialog. if no dialog exists, tosses an exception.
// does not check to see if the modal is currently open.
Modal.prototype.close = function () {
if (!this.modal) {
throw new Error('unable to close an unopened modal');
}
this.modal.modal('hide');
};
return Modal;
});
Let's work on our markup:
.. snip ..
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title ng-bind-template="{{title}}"></title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js" type="text/javascript"
charset="utf-8"></script>
<script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.2.2/js/bootstrap.min.js"></script>
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.2.2/css/bootstrap-combined.min.css" rel="stylesheet">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.js" type="text/javascript"
charset="utf-8"></script>
<script src="blog.js" type="text/javascript" charset="utf-8"></script>
</head>
.. snip ..
<ng-view></ng-view>
<hr>
<a ng-click="about()">About</a>
.. snip ..
<script type="text/ng-template" id="about">
<div class="modal fade in">
<div class="modal-header">About</div>
<div class="modal-body">This is the blog of John Jacob Jingleheimer Schmidt.</div>
<div class="modal-footer">
<button data-dismiss="modal">Got it</button>
</div>
</div>
</script>
----
OK! You can see we've included Bootstrap, its CSS, and made sure jQuery was the first thing we grab. You will immediately notice the page no longer looks like crap; now it just looks kinda bad. Thanks CSS!
The second thing we did was include a <hr> at the bottom of the page, and a link to call the about()
function in ContentCtrl
, which is here:
blog.controller('ContentCtrl', function ($scope, Posts, Post, $routeParams, Modal, $log) {
.. snip ..
// opens an "About" modal
$scope.about = function() {
var modal = new Modal($scope, 'about');
modal.open().then(function() {
$log.log("modal opened!");
});
};
.. snip ..
});
And the last thing we did was create a template called "about" which we tell our Modal object to open.
Let's show this class works as expected.
describe('The Modal Class', function () {
var scope, Modal;
beforeEach(inject(function ($rootScope, $injector) {
scope = $rootScope.$new();
// if you want to call something Modal in your local scope,
// and the service name happens to *be* Modal, you can
// just ask the injector service for it.
Modal = $injector.get('Modal');
}));
describe('the constructor', function () {
it('should extend the options', function () {
var modal = new Modal(scope, 'foo', {foo: 'bar'});
expect(modal.options).toEqual({show: false, foo: 'bar'});
});
it('should create a deferred', function () {
var modal = new Modal(scope, 'foo', {foo: 'bar'});
expect(angular.isObject(modal.dfrd.promise)).toBeTruthy();
});
});
describe('the open method', function () {
it('should throw an error if supplied a bad template id', inject(function ($httpBackend, $http, $window) {
spyOn($window, 'alert');
var modal = new Modal(scope, 'foo');
$httpBackend.expectGET('foo').respond(404);
expect(modal.open).toThrow();
// apparently you don't need to flush if you respond with an error
}));
it('should get a cached template', inject(function ($templateCache) {
$templateCache.put('foo', '<div>foo!</div>');
var modal = new Modal(scope, 'foo');
// wasn't sure how to get a spy to return its "this"
angular.element.prototype.modal = function () {
this.called = true;
return this;
};
modal.open();
expect(modal.modal.called).toBeTruthy();
}));
it('should resolve its promise', inject(function ($templateCache) {
var resolved = false, modal;
$templateCache.put('foo', '<div>foo!</div>');
modal = new Modal(scope, 'foo');
angular.element.prototype.modal = function () {
return this;
};
scope.$apply(function() {
var promise = modal.open();
promise.then(function() {
resolved = true;
});
});
expect(resolved).toBeTruthy();
}));
it('should open again if modal exists', inject(function ($templateCache) {
var modal, resolved = false;
$templateCache.put('foo', '<div>foo!</div>');
modal = new Modal(scope, 'foo');
angular.element.prototype.modal = function () {
return this;
};
scope.$apply(function() {
modal.open();
});
scope.$apply(function() {
modal.open().then(function() {
resolved = true;
});
});
expect(resolved).toBeTruthy();
}));
});
describe('the close method', function() {
it('should throw an error if it cannot find a modal', function() {
var modal = new Modal();
expect(modal.close).toThrow();
});
it('should attempt to close the modal', inject(function($templateCache) {
var modal, resolved = false, command = '';
$templateCache.put('foo', '<div>foo!</div>');
modal = new Modal(scope, 'foo');
angular.element.prototype.modal = function () {
command = arguments[0];
return this;
};
scope.$apply(function() {
modal.open().then(function() {
modal.close();
resolved = true;
});
});
expect(command).toBe('hide');
expect(resolved).toBeTruthy();
}));
});
});
A couple things to note:
We're using a promise here for one reason only: we might have to reach out over HTTP and fetch our template as a partial. If we do that, we need to be able to execute code in an asynchronous manner, because of course the fetch is going to be asynchronous. So, if we wanted to, we could place this template in a file called about.html and specified the path to that file as the second parameter to Modal()
. When we wanted to open the modal, we'd use the $http
service to fetch it and display it when it was received. When the promise resolves, we know exactly when that modal got opened.
The first parameter to Modal()
is a Scope. We're not really using it here, but we could be. Say we wanted to do $scope.name = 'John Jacob Jingleheimer Schmidt'; somewhere in ContentCtrl
. When we compile our modal's template, that scope is available to us, so we could easily plop {{name}}
in there.
This is just one way to handle The Modal Problem, and you may not like this solution as much as simply using markup and programatically flipping a switch when you want the modal to be open or shut.
It's possible to use just about any 3rd party library out there with AngularJS -- with a little effort. I see D3 (http://d3js.org) and Socket.IO (http://socket.io) mentioned often, so let's take a look at how we can use those with AngularJS.
D3 is a popular data visualization library. While the intricacies of D3 and its usage are beyond the scope of this book, we can take a simple D3 demo and Angularize it. Note that my D3-fu is weak.
First, here's the HTML and CSS we're going to use.
<div ng-controller="MyCtrl">
<div class="chart" bar-chart="data"></div>
<button ng-click="randomize()">randomize!</button>
</div>
<script src="http://d3js.org/d3.v2.js"></script>
Here is the D3 example CSS:
.chart div {
font: 10px sans-serif;
background-color: steelblue;
text-align: right;
padding: 3px;
margin: 1px;
color: white;
}
Next, the JavaScript involved:
var myApp = angular.module('myApp', []);
// provides some data and a randomize function
myApp.controller('MyCtrl', function($scope) {
$scope.data = [1, 2, 4, 8, 16, 32];
// randomizes the order of the $scope.data array.
$scope.randomize = function () {
var i = $scope.data.length,
tempi, tempj;
while (--i) {
j = Math.floor(Math.random() * (i + 1));
tempi = $scope.data[i];
tempj = $scope.data[j];
$scope.data[i] = tempj;
$scope.data[j] = tempi;
}
};
});
// puts a D3 div-based bar chart on a tag, and updates it
// as the model changes.
myApp.directive('barChart', function () {
return function (scope, elm, attrs) {
var data = scope.$eval(attrs.barChart);
// handy-dandy width function
function width(d) {
return d * 10 + 'px';
}
// grab our element and put some divs in it of varying sizes
// depending on the data. yes, there are probably sexier ways to do this.
d3.select(elm[0])
.selectAll('div')
.data(data)
.enter()
.append('div')
.style('width', width)
.text(angular.identity);
// watch the data and update the divs within the element with their
// new widths.
scope.$watch(attrs.barChart, function (new_val, old_val) {
if (new_val !== old_val) {
d3.select(elm[0])
.selectAll('div')
.data(new_val)
.style('width', width);
}
}, true);
}
});
That wasn't so bad, was it? Add some transition effects and SVG and you'll be updating your charts dynamically with D3 and AngularJS in no time. For the curious, Brian Ford has provided an excellent blog post with a more complete example at http://briantford.com/blog/angular-d3.html.
Socket.IO allows you to easily write real-time web applications against a Node.JS server. While configuring a server is beyond the scope of this text, we can discuss how to best use Socket.IO from within the context of AngularJS.
Socket.IO provides a global variable, io. You can call io.connect('http://your-server.com') to get a socket object. You now have a persistent socket connection to the Node.JS server you specified. Let's provide a factory to use Socket.IO. This will keep you out of the global scope (using dependency injection) and assist with the digest loop; remember, non-AngularJS code doesn't know when to trigger watches, so we have to tell it to.
Let's build that factory, which is very similar to what Brian Ford has come up with at http://briantford.com/blog/angular-socket-io.html:
angular.module('app').factory('connect', function ($window, $q) {
return function (scope, url) {
var socket = $window.io.socket(url);
return {
// wrapper around socket.on()
on: function (event) {
var dfrd = $q.defer();
socket.on(event, function () {
var args = arguments;
scope.$apply(function () {
dfrd.resolve.apply(dfrd, args);
});
});
return dfrd.promise;
},
// wrapper around socket.emit()
emit: function (event, data) {
var dfrd = $q.defer();
socket.on(event, data, function () {
var args = arguments;
scope.$apply(function () {
dfrd.resolve.apply(dfrd, args);
});
});
return dfrd.promise;
}
};
};
})();
And basic usage:
function MyCtrl($scope, connect) {
// remember; socket is not a real Socket.IO socket object; it's our wrapper
var socket = connect($scope, 'http://localhost.com');
socket.on('some_event').then(function() {
// do stuff
});
socket.emit('some_other_event', {foo: 'bar'}).then(function() {
// do other stuff
});
}
So you'll note that we're using promises instead of callbacks here; Socket.IO uses callbacks but we can avoid them altogether.
Next we'll talk about another popular unit testing framework, QUnit.
You are not required to use Jasmine to unit test AngularJS code. With a little bit of setup, you can use QUnit or another library (such as Mocha) just as easily.
What follows are examples of testing each kind of component using QUnit:
var myApp = angular.module('myApp', []);
myApp.controller('MyCtrl', function($scope) {
$scope.addTwo = function(n) {
return n + 2;
};
});
myApp.service('MyService', function() {
this.addThree = function(n) {
return n + 3;
};
});
myApp.directive('myDirective', function() {
return {
link: function(scope, elm, attrs) {
scope.$watch(attrs.myDirective, function(value) {
elm.text(value + 4);
});
}
}
});
myApp.filter('myfilter', function() {
return function(s) {
return s + 5;
};
});
And the tests:
var injector = angular.injector(['ng', 'myApp']);
var init = {
setup: function() {
this.$scope = injector.get('$rootScope').$new();
}
};
module('tests', init);
test('MyCtrl', function() {
var $controller = injector.get('$controller');
$controller('MyCtrl', {
$scope: this.$scope
});
equal(3, this.$scope.addTwo(1));
});
test('MyService', function() {
var MyService = injector.get('MyService');
equal(4, MyService.addThree(1));
});
test('MyDirective', function() {
var $compile = injector.get('$compile');
var element = $compile('<div my-directive="foo"></div>')(this.$scope);
this.$scope.foo = 1;
this.$scope.$apply();
equal(5, element.text());
delete this.$scope.foo;
});
test('MyFilter', function() {
var $filter = injector.get('$filter');
equal(6, $filter('myfilter')(1));
});
As you can see, it's about as easy as setting up Jasmine to test AngularJS code, just a little different. The lack of support in angular-mocks.js doesn't impede us. We simply use the injector to grab whatever we need. This should be enough to get you started with your own QUnit tests.
This concludes the book. I hope you have learned something along the way. I know I certainly did!
There has been error in communication with Booktype server. Not sure right now where is the problem.
You should refresh this page.