AngularJS
■
■
■
■
Up & Running
ENHANCED PRODUCTIVITY
WITH STRUCTURED WEB APPS
■
■
■
■
Shyam Seshadri & Brad Green
www.it-ebooks.info
AngularJS: Up and Running
If want to get started with AngularJS, either as a side project, an additional
tool, or for your main work, this practical guide teaches you how to use this
meta-framework step-by-step, from the basics to advanced concepts. By
the end of the book, you’ll understand how to develop a large, maintainable,
and performant application with AngularJS.
Guided by two engineers who worked on AngularJS at Google, you’ll
learn the components needed to build data-driven applications, using
declarative programming and the Model–view–controller pattern. You’ll
also learn how to conduct unit tests on each part of your application.
■
■
■
■
■
■
■
■
Learn how to use controllers for moving data to and from views
Understand when to use AngularJS services instead of
controllers
Communicate with the server to store, fetch, and update data
asynchronously
Know when to use AngularJS ilters for converting data and
values to diferent formats
hardly more than
“I'm
an amateur JavaScript
developer and I had zero
problems understanding
this book. I appreciate
how it started at the
very beginning—the
why of AngularJS—and
slowly worked its way
up from there. The
complimentary code
repository was a huge
help as well!
”
—Marc Amos
frontend developer
Implement single-page applications, using ngRoute to select
views and navigation
Dive into basic and advanced directives for creating reusable
components
Write an end-to-end test on a live version of your entire
application
Use best practices, guidelines, and tools throughout the
development cycle
Shyam Seshadri, owner/CEO of Fundoo Solutions in Mumbai, splits his time
between working on innovative and exciting new products for the Indian markets, and consulting about and running workshops on AngularJS.
Brad Green, an engineering manager at Google, works on the AngularJS project
and directs Accessibility as well as Support Engineering. Brad also worked on the
early mobile web at AvantGo, and founded and sold startups.
PROGR AMMING/JAVA SCRIPT
US $39.99
Twitter: @oreillymedia
facebook.com/oreilly
CAN $41.99
ISBN: 978-1-491-90194-6
www.it-ebooks.info
AngularJS: Up And Running
Shyam Seshadri and Brad Green
www.it-ebooks.info
AngularJS: Up And Running
by Shyam Seshadri and Brad Green
Copyright © 2014 Shyam Seshadri and Brad Green. All rights reserved.
Printed in the United States of America.
Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.
O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are
also available for most titles (http://safaribooksonline.com). For more information, contact our corporate/
institutional sales department: 800-998-9938 or
[email protected].
Editors: Simon St. Laurent and Brian MacDonald
Production Editor: Kara Ebrahim
Copyeditor: Gillian McGarvey
Proofreader: Kim Cofer
September 2014:
Indexer: Judy McConville
Cover Designer: Ellie Volckhausen
Interior Designer: David Futato
Illustrator: Rebecca Demarest
First Edition
Revision History for the First Edition:
2014-09-05:
First release
See http://oreilly.com/catalog/errata.csp?isbn=9781491901946 for release details.
Nutshell Handbook, the Nutshell Handbook logo, and the O’Reilly logo are registered trademarks of O’Reilly
Media, Inc. AngularJS: Up and Running, the image of a thornback cowfish, and related trade dress are
trademarks of O’Reilly Media, Inc.
Many of the designations used by manufacturers and sellers to distinguish their products are claimed as
trademarks. Where those designations appear in this book, and O’Reilly Media, Inc. was aware of a trademark
claim, the designations have been printed in caps or initial caps.
While every precaution has been taken in the preparation of this book, the publisher and authors assume
no responsibility for errors or omissions, or for damages resulting from the use of the information contained
herein.
ISBN: 978-1-491-90194-6
[LSI]
www.it-ebooks.info
Table of Contents
Introduction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ix
1. Introducing AngularJS. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Introducing AngularJS
What Is MVC (Model-View-Controller)?
AngularJS Benefits
The AngularJS Philosophy
Starting Out with AngularJS
What Backend Do I Need?
Does My Entire Application Need to Be an AngularJS App?
A Basic AngularJS Application
AngularJS Hello World
Conclusion
2
2
3
4
10
10
11
11
12
13
2. Basic AngularJS Directives and Controllers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
AngularJS Modules
Creating Our First Controller
Working with and Displaying Arrays
More Directives
Working with ng-repeat
ng-repeat Over an Object
Helper Variables in ng-repeat
Track by ID
ng-repeat Across Multiple HTML Elements
Conclusion
15
17
22
26
27
28
29
30
32
34
3. Unit Testing in AngularJS. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
Unit Testing: What and Why?
Introduction to Karma
35
37
iii
www.it-ebooks.info
Karma Plugins
Explaining the Karma Config
Generating the Karma Config
Jasmine: Spec Style of Testing
Jasmine Syntax
Useful Jasmine Matchers
Writing a Unit Test for Our Controller
Running the Unit Test
Conclusion
38
39
41
42
42
43
44
47
48
4. Forms, Inputs, and Services. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Working with ng-model
Working with Forms
Leverage Data-Binding and Models
Form Validation and States
Error Handling with Forms
Displaying Error Messages
Styling Forms and States
Nested Forms with ng-form
Other Form Controls
Textareas
Checkboxes
Radio Buttons
Combo Boxes/Drop-Downs
Conclusion
49
51
52
54
55
56
58
60
62
62
63
64
66
68
5. All About AngularJS Services. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
AngularJS Services
Why Do We Need AngularJS Services?
Services Versus Controllers
Dependency Injection in AngularJS
Using Built-In AngularJS Services
Order of Injection
Common AngularJS Services
Creating Our Own AngularJS Service
Creating a Simple AngularJS Service
The Difference Between Factory, Service, and Provider
Conclusion
69
70
72
73
74
76
77
78
78
82
86
6. Server Communication Using $http. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Fetching Data with $http Using GET
A Deep Dive into Promises
iv | Table of Contents
www.it-ebooks.info
87
91
Propagating Success and Error
The $q Service
Making POST Requests with $http
$http API
Configuration
Advanced $http
Configuring $http Defaults
Interceptors
Best Practices
Conclusion
93
94
94
96
97
99
99
101
104
106
7. Unit Testing Services and XHRs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
Dependency Injection in Our Unit Tests
State Across Unit Tests
Mocking Out Services
Spies
Unit Testing Server Calls
Integration-Level Unit Tests
Conclusion
107
109
111
113
115
118
120
8. Working with Filters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
What Are AngularJS Filters?
Using AngularJS Filters
Common AngularJS Filters
Using Filters in Controllers and Services
Creating AngularJS Filters
Things to Remember About Filters
Conclusion
121
122
124
130
131
133
134
9. Unit Testing Filters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
The Filter Under Test
Testing the timeAgo Filter
Conclusion
135
136
138
10. Routing Using ngRoute. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
Routing in a Single-Page Application
Using ngRoute
Routing Options
Using Resolves for Pre-Route Checks
Using the $routeParams Service
Things to Watch Out For
A Full AngularJS Routing Example
140
141
143
146
148
149
150
Table of Contents
www.it-ebooks.info
| v
Additional Configuration
HTML5 Mode
SEO with AngularJS
Analytics with AngularJS
Alternatives: ui-router
Conclusion
160
160
162
163
165
166
11. Directives. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
What Are Directives?
Alternatives to Custom Directives
ng-include
Limitations of ng-include
ng-switch
Understanding the Basic Options
Creating a Directive
Template/Template URL
Restrict
The link Function
Scope
Replace
Conclusion
169
170
170
173
173
175
175
176
179
181
182
192
194
12. Unit Testing Directives. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Steps Involved in Testing a Directive
The Stock Widget Directive
Setting Up Our Directive Unit Test
Other Considerations
Conclusion
195
196
197
201
202
13. Advanced Directives. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
Life Cycles in AngularJS
AngularJS Life Cycle
The Digest Cycle
Directive Life Cycle
Transclusions
Basic Transclusion
Advanced Transclusion
Directive Controllers and require
require Options
Input Directives with ng-model
Custom Validators
Compile
vi | Table of Contents
www.it-ebooks.info
203
203
206
208
208
211
212
216
221
222
226
228
Priority and Terminal
Third-Party Integration
Best Practices
Scopes
Clean Up and Destroy
Watchers
$apply (and $digest)
Conclusion
234
234
239
240
240
241
242
242
14. End-to-End Testing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
The Need for Protractor
Initial Setup
Protractor Configuration
An End-to-End Test
Considerations
Conclusion
245
246
247
248
251
254
15. Guidelines and Best Practices. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
Testing
Test-Driven Development
Variety of Tests
When to Run Tests
Project Structure
Best Practices
Directory Structure
Third-Party Libraries
Starting Point
Build
Grunt
Serve a Single JavaScript File
Minification
ng-templates
Best Practices
General
Services
Controllers
Directives
Filters
Tools and Libraries
Batarang
WebStorm
Optional Modules
255
255
256
257
258
258
259
263
264
265
265
266
267
267
267
268
268
269
270
270
271
271
272
273
Table of Contents
www.it-ebooks.info
| vii
Conclusion
274
Index. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
viii
| Table of Contents
www.it-ebooks.info
Introduction
I remember the very first time I was introduced to AngularJS. It was called Angular, and
it was an open source library built as a hobby by one of my fellow engineers, Misko. At
that point, we had spent months struggling to develop Google Feedback (the project we
were developing) in an efficient and maintainable manner. We had written over 18,000
lines of code, a lot of which were untested, and were frustrated with our inability to
continue adding features quickly. Misko Hevery, the engineer I mentioned, made a bold
statement that he could reproduce everything we had developed in the past six months
within two weeks. I should mention that we were all Java engineers at that point, with
a complete lack of JavaScript knowledge.
After what we expected to be an entertaining two weeks of watching Misko struggle,
scramble, and fail, it wasn’t done. But one more week later, he had replicated what took
us six months. What had been an 18,000-line codebase had dropped to a mere 1,500
lines, and almost every single piece of functionality was modular, reusable, and testable.
Misko was on to something!
Brad Green, this book’s coauthor, saw the beginning of something amazing there, and
decided with Misko to build a team around the core idea of making it simple to build
web applications. Google Feedback, which I was leading, became the first project to ship
with AngularJS, and really helped us understand what was important from a web de‐
veloper’s perspective in a JavaScript framework.
What started as a side project quickly took off into one of the leading JavaScript frame‐
works (or meta-framework, as I call it) on the Web. There are a lot of reasons why
AngularJS is awesome, and a super community of helpful developers and contributors
is just one of them. The more recent releases have all incorporated features from the
open source community around AngularJS. Thousands of developers rely on AngularJS
daily, and thousands more start using it every month. And each developer makes An‐
gularJS better through his or her experience.
I am excited to present this book, and look forward to learning from your experiences.
ix
www.it-ebooks.info
Who Should Read This Book
This book is for anyone who is looking to get started with AngularJS, whether as a side
project, as an additional tool, or for their main work. It is expected that readers are
comfortable with JavaScript before starting this book, but a basic knowledge of Java‐
Script should be sufficient to learn AngularJS. The book will cover everything from
getting started with AngularJS, to advanced concepts like directives. We will take it step
by step, so relax and have fun learning with us.
Why We Wrote This Book
When we wrote the first book on AngularJS, there was no easy way to learn it. The
documentation was (and still is to some extent) confusing. With this book, the aim is
to present a step-by-step guide on getting started with AngularJS. AngularJS is layered,
with some very simple and powerful concepts, and some advanced and hard-to-get
features. This book aims to walk developers through each of these in an organized, stepwise fashion, adding complexity bit by bit.
At the end of the book, you should be able to quickly get started with an AngularJS
project, and really understand how to develop large, maintainable, and performant
applications.
A Word on Web Application Development Today
JavaScript has come a long way from being just a scripting language (or hack, as it was
affectionately called) that was only used to do minor validations to becoming a fullfledged programming language. jQuery did a lot of ground work in ensuring browser
compatibility and giving a solid, stable API to work across all browsers and interact with
the DOM. As applications grew in complexity and size, jQuery, which is a DOM ma‐
nipulation layer, became insufficient by itself to provide a solid, modular, testable, and
easily understandable framework for developing applications. Each jQuery project
would look completely different from another.
AngularJS (and quite a lot of other MVC frameworks for JavaScript) tackles this very
problem of providing a layer on top of jQuery, and on top of the DOM, to think in terms
of application structure and maintainability, while reducing the amount of boilerplate
code you would end up writing. The best part about using a framework in a consistent
manner is that a new developer coming in has a sense of the structure, the layout, and
how to develop right off the bat. We want a framework where we can spend time wor‐
rying about our look and feel, and our core functionality, without having to worry about
the boilerplate and cruft.
Some of the concepts that are currently at the center of web application development
and thus also at the core of AngularJS are:
x
| Introduction
www.it-ebooks.info
• Data-driven programming, where the aim is to manipulate the model, and let the
framework do the heavy lifting and UI rendering.
• Declarative programming, which entails declaring your intent when you are per‐
forming an action, instead of imperative programming, where the actual work is
performed in a separate file/function and not where the effect is needed.
• Modularity and separation of concerns, which is the ability to separate your appli‐
cation into smaller, reusable functional pieces, each responsible for one and only
one thing.
• Testability, so that we can ensure that what we as developers write actually does
what it is supposed to.
• And much more.
With the help of frameworks like AngularJS, we can focus on developing amazing New
Age web applications with immense complexity in a manageable and maintainable
fashion.
Navigating This Book
This book aims to walk a developer through each part of AngularJS, step by step. Each
chapter that introduces a new concept will be immediately followed by a chapter on
how we can unit test it. The book is roughly organized as follows:
• Chapter 1, Introducing AngularJS, is an introduction to AngularJS as well as the
concepts behind it. It also covers what it takes to start writing an AngularJS
application.
• Chapter 2, Basic AngularJS Directives and Controllers, starts introducing some
built-in AngularJS directives, and the concept of controllers.
• Chapter 3, Unit Testing in AngularJS, digs into unit testing AngularJS projects with
Karma and Jasmine.
• Chapter 4, Forms, Inputs, and Services, covers forms and how best to leverage An‐
gularJS when working with them.
• Chapter 5, All About AngularJS Services, introduces the concept of AngularJS serv‐
ices, some common built-in AngularJS services, and how to create your own.
• Chapter 6, Server Communication Using $http, involves server communication in
AngularJS using $http and advanced $http concepts like interceptors and
transformers.
• Chapter 7, Unit Testing Services and XHRs, then digs into unit testing of services
and mocking server requests using $httpBackend.
Introduction | xi
www.it-ebooks.info
• Chapter 8, Working with Filters, and Chapter 9, Unit Testing Filters, introduce An‐
gularJS filters as well as how to unit test them.
• Chapter 10, Routing Using ngRoute, covers routing in an SPA using the optional
ngRoute module.
• Chapter 11, Directives, introduces some basic concepts of directives and how to
create them.
• Chapter 12, Unit Testing Directives, covers unit testing of directives.
• Chapter 13, Advanced Directives, involves advanced directive creation concepts like
compile, transclusion, controllers, and require. It also provides some examples of
third-party widget integration as a directive.
• Chapter 14, End-to-End Testing, covers end-to-end testing of an AngularJS appli‐
cation using Protractor and WebDriver.
• Chapter 15, Guidelines and Best Practices, brings everything together into best
practices, guidelines, and useful tools.
The entire code repository is hosted on GitHub, so if you don’t want to type in the code
examples from this book, or want to ensure that you are looking at the latest and greatest
code examples, do visit the repository and grab the contents.
If you’re like us, you don’t read books from front to back. If you’re really like us, you
usually don’t read the Introduction at all. However, on the off chance that you will see
this in time, here are a few suggestions:
• You can skip Chapter 1 if you have already worked on AngularJS in the past.
• Chapter 2 digs into ng-repeat and all the various ways you can use and optimize it.
• Chapters 3, 7, 9, and 12 cover unit testing of controllers, services, filters, and di‐
rectives, so if you want to know more about those, jump to those chapters directly.
• Chapter 14 is where you want to jump to in case you are interested in end-to-end
testing using Protractor.
• Chapters 11 and 13 are essential if you really want to understand directives and
leverage the power that it provides.
• If you want to look at a full-fledged AngularJS application that uses routing, au‐
thorization, and more, check out the last example in Chapter 10.
This book uses AngularJS version 1.2.19 for all its code examples, and Karma version
0.12.16 for the unit tests.
xii | Introduction
www.it-ebooks.info
Online Resources
The following resources are a great starting point for any AngularJS developer, and
should be always available at your fingertips:
• The Official AngularJS API Documentation
• The Official AngularJS Developer Guide
• The AngularJS PhoneCat Tutorial App
• ngModules: A list of all known open source AngularJS modules
• Egghead.io: Great AngularJS video tutorials
Conventions Used in This Book
The following typographical conventions are used in this book:
Italic
Indicates new terms, URLs, email addresses, filenames, and file extensions.
Constant width
Used for program listings, as well as within paragraphs to refer to program elements
such as variable or function names, databases, data types, environment variables,
statements, and keywords.
Constant width bold
Shows commands or other text that should be typed literally by the user.
Constant width italic
Shows text that should be replaced with user-supplied values or by values deter‐
mined by context.
This element signifies a tip or suggestion.
This element signifies a general note.
Introduction | xiii
www.it-ebooks.info
This element indicates a warning or caution.
Using Code Examples
Supplemental material (code examples, exercises, etc.) is available for download at
https://github.com/shyamseshadri/angularjs-up-and-running.
This book is here to help you get your job done. In general, if example code is offered
with this book, you may use it in your programs and documentation. You do not need
to contact us for permission unless you’re reproducing a significant portion of the code.
For example, writing a program that uses several chunks of code from this book does
not require permission. Selling or distributing a CD-ROM of examples from O’Reilly
books does require permission. Answering a question by citing this book and quoting
example code does not require permission. Incorporating a significant amount of ex‐
ample code from this book into your product’s documentation does require permission.
We appreciate, but do not require, attribution. An attribution usually includes the title,
author, publisher, and ISBN. For example: “AngularJS: Up and Running by Shyam Se‐
shadri and Brad Green (O’Reilly). Copyright 2014 Shyam Seshadri and Brad Green,
978-1-491-90194-6.”
If you feel your use of code examples falls outside fair use or the permission given above,
feel free to contact us at
[email protected].
Safari® Books Online
Safari Books Online is an on-demand digital library that
delivers expert content in both book and video form from
the world’s leading authors in technology and business.
Technology professionals, software developers, web designers, and business and crea‐
tive professionals use Safari Books Online as their primary resource for research, prob‐
lem solving, learning, and certification training.
Safari Books Online offers a range of plans and pricing for enterprise, government,
education, and individuals.
Members have access to thousands of books, training videos, and prepublication manu‐
scripts in one fully searchable database from publishers like O’Reilly Media, Prentice
Hall Professional, Addison-Wesley Professional, Microsoft Press, Sams, Que, Peachpit
Press, Focal Press, Cisco Press, John Wiley & Sons, Syngress, Morgan Kaufmann, IBM
Redbooks, Packt, Adobe Press, FT Press, Apress, Manning, New Riders, McGraw-Hill,
xiv
| Introduction
www.it-ebooks.info
Jones & Bartlett, Course Technology, and hundreds more. For more information about
Safari Books Online, please visit us online.
How to Contact Us
Please address comments and questions concerning this book to the publisher:
O’Reilly Media, Inc.
1005 Gravenstein Highway North
Sebastopol, CA 95472
800-998-9938 (in the United States or Canada)
707-829-0515 (international or local)
707-829-0104 (fax)
We have a web page for this book, where we list errata, examples, and any additional
information. You can access this page at http://bit.ly/angularjs-up.
To comment or ask technical questions about this book, send email to bookques
[email protected].
For more information about our books, courses, conferences, and news, see our website
at http://www.oreilly.com.
Find us on Facebook: http://facebook.com/oreilly
Follow us on Twitter: http://twitter.com/oreillymedia
Watch us on YouTube: http://www.youtube.com/oreillymedia
Acknowledgments
I’d like to thank Misko Hevery, Igor Minar, and the entire AngularJS team for building
AngularJS, and for continuing to make it more awesome with every release (and think‐
ing of hilarious release names such as curdling-stare, insomnia-induction, and tofuanimation, to name a few). I’d also like to thank my untiring reviewers, Brad Green,
Brian Holt, Ross Dederer, and Jesse Palmer, who willingly waded through pages and
pages multiple times and never missed a single detail. You guys are amazing.
I’d also like to thank my team at Fundoo Solutions (Abhiroop Patel, Pavan Jartarghar,
Suryakant Sharma, and Amol Kedari) who helped me test all the code examples and
give me feedback on the order in which I introduced content.
Finally, I don’t think this book would have happened without my mom, dad, and grand‐
mom, who ensured that I was well-fed, caffeinated at the right times, and motivated to
sit and write for long stretches. And this book would definitely not have finished on
Introduction | xv
www.it-ebooks.info
time without the support of my loving wife, Sanchita, who was a great sport and didn’t
complain while I typed away at this book during our wedding and honeymoon!
And finally, thank you to the amazing AngularJS community for all their contributions,
feedback, and support, and for teaching us how to use and make it better.
xvi
| Introduction
www.it-ebooks.info
CHAPTER 1
Introducing AngularJS
The Internet has come a long way since its inception. Consumption-oriented, noninteractive websites started moving toward something users interacted with. Users
could respond, fill in details, and eventually access all their mail on websites. Concurrent
usage, offline support, and so many other things became basic features, and the size and
scope of client-side applications has kept on accelerating and increasing.
As applications have gotten bigger, better, and faster, so has the complexity a developer
has to manage. A pure JavaScript/jQuery solution would not always have the right
structure to ensure a rapid speed of development or long-term maintainability. Projects
became heavily dependent on having a great software engineer to set up the initial
framework. Even then, modularity, testability, and separation of concerns may not make
it into a project. Testing and reliability were often pushed to the backburner in such
cases.
AngularJS was started to fill this basic need. Could we provide a standard structure and
meta-framework within which web applications could be developed reliably and quick‐
ly? Could the same software engineering concepts like testable code, separation of con‐
cerns, MVC (Model-View-Controller) (or rather, MVVM), and so on be applied to
JavaScript applications? Could we have the best of both worlds—the succinctness of
JavaScript and the pleasure of rapid, maintainable development? We think so, but we’ll
let you be the final judge as we walk through AngularJS throughout the rest of this book.
By the end of this chapter, we will build a basic AngularJS “hello world” example to get
a sense of some common concepts and philosophies behind AngularJS. We will also see
how to bootstrap and convert any HTML into an AngularJS application, and see how
to use common data-binding techniques in AngularJS.
1
www.it-ebooks.info
Introducing AngularJS
AngularJS is a superheroic JavaScript MVC framework for the Web. We call it super‐
heroic because AngularJS does so much for us that we only have to focus on our core
application and let AngularJS take care of everything else. It allows us to apply standard,
tried-and-tested software engineering practices traditionally used on the server side in
client-side programming to accelerate frontend development. It provides a consistent
scalable structure that makes it a breeze to develop large, complex applications as part
of a team.
And the best part? It’s all done in pure JavaScript and HTML. No need to learn another
new programming or templating language (though you do have to understand the MVC
and MVVM paradigms of developing applications, which we briefly cover in this book).
And how does it fulfill all these crazy and wonderful, seemingly impossible-to-satisfy
promises?
The AngularJS philosophy is driven by a few key tenets that drive everything from how
to structure your application, to how your applications should be hooked together, to
how to test your application and integrate your code with other libraries. But before we
get into each of these, let’s take a look at why we should even care in the first place.
What Is MVC (Model-View-Controller)?
The core concept behind the AngularJS framework is the MVC architectural pattern.
The Model-View-Controller pattern (or MVVM, which stands for Model-ViewViewModel, which is quite similar) evolved as a way to separate logical units and con‐
cerns when developing large applications. It gives developers a starting point in deciding
how and where to split responsibilities. The MVC architectural pattern divides an ap‐
plication into three distinct, modular parts:
• The model is the driving force of the application. This is generally the data behind
the application, usually fetched from the server. Any UI with data that the user sees
is derived from the model, or a subset of the model.
• The view is the UI that the user sees and interacts with. It is dynamic, and generated
based on the current model of the application.
• The controller is the business logic and presentation layer, which peforms actions
such as fetching data, and makes decisions such as how to present the model, which
parts of it to display, etc.
Because the controller is responsible for basically deciding which parts of the model to
display in the view, depending on the implementation, it can also be thought of as a
viewmodel, or a presenter.
2 | Chapter 1: Introducing AngularJS
www.it-ebooks.info
At its core, though, each of these patterns splits responsibilities in the application into
separate subunits, which offers the following benefits:
• Each unit is responsible for one and only one thing. The model is the data, the view
is the UI, and the controller is the business logic. Figuring out where the new code
we are working on belongs, as well as finding prior code, is easy because of this
single responsibility principle.
• Each unit is as independent from the others as possible. This makes the code much
more modular and reusable, as well as easy to maintain.
AngularJS Benefits
We are going to make some claims in this section, which we will expand on in the
following section when we dive into how AngularJS makes all this possible:
• AngularJS is a Single Page Application (SPA) meta-framework. With client-side
templating and heavy use of JavaScript, creating and maintaining an application
can get tedious and onerous. AngularJS removes the cruft and does the heavy lifting,
so that we can focus solely on the application core.
• An AngularJS application will require fewer lines of code to complete a task than a
pure JavaScript solution using jQuery would. When compared to other frame‐
works, you will still find yourself writing less boilerplate, and cleaner code, as it
moves your logic into reusable components and out of your view.
• Most of the code you write in an AngularJS application is going to be focused on
business logic or your core application functionality, and not unnecessary routine
cruft code. This is a result of AngularJS taking care of all the boilerplate that you
would otherwise normally write, as well as the MVC architecture pattern.
• AngularJS’s declarative nature makes it easier to write and understand applications.
It is easy to understand an application’s intent just by looking at the HTML and the
controllers. In a sense, AngularJS allows you to create HTMLX (instead of relying
on HTML5 or waiting for HTML6, etc.), which is a subset of HTML that fits your
needs and requirements.
• AngularJS applications can be styled using CSS and HTML independent of their
business logic and functionality. That is, it is completely possible to change the
entire layout and design of an application without touching a single line of
JavaScript.
• AngularJS application templates are written in pure HTML, so designers will find
it easier to work with and style them.
• It is ridiculously simple to unit test AngularJS applications, which also makes the
application stable and easier to maintain over a longer period of time. Got new
Introducing AngularJS
www.it-ebooks.info
| 3
features? Need to make changes to existing logic? All of it is a breeze with that rocksolid bed of tests underneath.
• We don’t need to let go of those jQueryUI or BootStrap components that we love
and adore. AngularJS plays nicely with third-party component libraries and gives
us hooks to integrate them as we see fit.
The AngularJS Philosophy
There are five core beliefs to which AngularJS subscribes that enable developers to
rapidly create large, complex applications with ease:
Data-driven (via data-binding)
In a traditional server-side application, we create the user interface by merging
HTML with our local data. Of course, this means that whenever we need to change
part of the UI, the server has to send the entire HTML and data to the client yet
again, even if the client already has most of the HTML.
With client-side Single Page Applications (SPAs), we have an advantage. We only
have to send from the server to the client the data that has changed. But the clientside code still has to update the UI as per the new data. This results in boilerplate
that might look something like the following (if we were using jQuery). First, let’s
look at some very simple HTML:
Hello <span id="name"></span>
The JavaScript that makes this work might look something like this:
var updateNameInUI = function(name) {
$('#name').text(name);
};
// Lots of code here...
// On initial data load
updateNameInUI(user.name);
// Then when the data changes somehow
updateNameInUI(updatedName);
The preceding code defines a updateNameInUI function, which takes in the name
of the user, and then finds the UI element and updates its innerText. Of course,
we would have to be sure to call this function whenever the name value changes,
like the initial load, and maybe when the user logs out and logs back in, or if he
edits his name. And this is just one field. Now imagine dozens of such lines across
your entire codebase. These kinds of operations are very common in a CRUD
(Create-Retrieve-Update-Delete) model like this.
4 | Chapter 1: Introducing AngularJS
www.it-ebooks.info
Now, on the other hand, the AngularJS approach is driven by the model backing it.
AngularJS’s core feature—one that can save thousands of lines of boilerplate code
—is its data-binding (both one-way and two-way). We don’t have to waste time
funneling data back and forth between the UI and the JavaScript in an AngularJS
application. We just bind to the data in our HTML and AngularJS takes care of
getting its value into the UI. Not only that, but it also takes care of updating the UI
whenever the data changes.
The exact same functionality in an AngularJS application would look something
like this:
Hello <span>{{name}}</span>
Now, in the JavaScript, all that we need to do is set the value of the name variable.
AngularJS will take care of figuring out that it has changed and update the UI
automatically.
This is one-way data-binding, where we take data coming from the server (or any
other source), and update the Document Object Model (DOM). But what about
the reverse? The traditional way when working with forms—where we need to get
the data from the user, run some processing, and then send it to the server—would
look something like the following. The HTML first might look like this:
<form name="myForm" onsubmit="submitData()">
<input type="text" id="nameField"/>
<input type="text" id="emailField"/>
</form>
The JavaScript that makes this work might look like this:
// Set data in the form
function setUserDetails(userDetails) {
$('#nameField').value(userDetails.name);
$('#emailField').value(userDetails.email);
}
function getUserDetails() {
return {
name: $('#nameField').value(),
email: $('#emailField').value()
};
}
var submitData = function() {
// Assuming there is a function which makes XHR request
// Make POST request with JSON data
makeXhrRequest('http://my/url', getUserDetails());
};
In addition to the layout and templating, we have to manage the data flow between
our business logic and controller code to the UI and back. Any time the data
Introducing AngularJS
www.it-ebooks.info
| 5
changes, we need to update the UI, and whenever the user submits or we need to
run validation, we need to call the getUserDetails() function and then do our
actual core logic on the data.
AngularJS provides two-way data-binding, which allows us to skip writing this
boilerplate code as well. The two-way data-binding ensures that our controller and
the UI share the same model, so that updates to one (either from the UI or in our
code) update the other automatically. So, the same functionality as before in An‐
gularJS might have the HTML as follows:
<form name="myForm" ng-submit="ctrl.submitData()">
<input type="text" ng-model="user.name"/>
<input type="text" ng-model="user.email"/>
</form>
Each input tag in the HTML is bound to an AngularJS model declared by the ngmodel attribute (called directives in AngularJS). When the form is submitted, An‐
gularJS hooks on by triggering a function in the controller called submitData. The
JavaScript for this might look like:
// Inside my controller code
this.submitData = function() {
// Make Server POST request with JSON object
$http.post('http://my/url', this.user);
};
AngularJS takes care of the two-way data-binding, which entails getting the latest
values from the UI and updating the name and email in the user object automati‐
cally. It also ensures that any changes made to the name or email values in the user
object are reflected in the DOM automatically.
Because of data-binding, in an AngularJS application, you can focus on your core
business logic and functionality and let AngularJS do the heavy lifting of updating
the UI. It also means that it requires a shift in our mindset to develop an AngularJS
application. Need to update the UI? Change the model and let AngularJS update
the UI.
Declarative
A single-page web application (also known as an AJAX application) is made up of
multiple separate HTML snippets and data stitched together via JavaScript. But
more often than not, we end up having HTML templates that have no indication
of what they turn into. For example, consider HTML like the following:
<ul class="nav nav-tabs">
<li>Home</li>
<li class="selected">Profile</li>
</ul>
<div class="tab1">
6 | Chapter 1: Introducing AngularJS
www.it-ebooks.info
Some content here
</div>
<div class="tab2">
<input id="startDate" type="text"/>
</div>
Now, if you are used to certain HTML constructs or are familiar with jQuery or
similar frameworks, you might be able to divine that the preceding HTML reflects
a set of tabs, and that the second tab has an input field that needs to become a
datepicker. But none of that is actually mentioned in the HTML. It is only because
there is some JS and CSS in your codebase that has the task of converting these li
elements into tabs, and the input field into a datepicker.
This is essentially the imperative paradigm, where we tell the application exactly
how to do each and every action. We tell it to find the element with class navtabs and make it a tab component, then to select the first tab by default. We ac‐
complish this entirely in our JavaScript code and not where the actual HTML needs
to change. The HTML does not reflect any of this logic.
AngularJS instead promotes a declarative paradigm, where you declare right in your
HTML what it is you are trying to accomplish. This is done through something that
AngularJS calls directives. Directives basically extend the vocabulary of HTML to
teach it new tricks. We let AngularJS figure out how to accomplish what we want
it to do, whether it is creating tabs or datepickers. The ideal way to write the previous
code in AngularJS would be something like the following:
<tabs>
<tab title="Home">Some content here</tab>
<tab title="Profile">
<input type="text"
datepicker
ng-model="startDate"/>
</tab>
</tabs>
The AngularJS-based HTML uses <tab> tags, which tells AngularJS to figure out
how to render the tabs component, and declares that the <input> is a datepick
er that is bound to an AngularJS model variable called startDate.
There are a few advantages to this approach:
• It’s declarative, so just by looking at the HTML we can immediately figure out
that there are two tabs, one of which has a datepicker inside of it.
• The business logic of selecting the current tab, unselecting the other tabs, and
hiding and showing the correct content is all encapsulated inside the tab
directive.
Introducing AngularJS
www.it-ebooks.info
| 7
• Similarly, any developer who wants a datepicker does not have to know whether
we are using jQueryUI, BootStrap, or something else underneath. It separates
out the usage from the implementation so there is a clear separation of
concerns.
• Because the entire functionality is encapsulated and contained in one place, we
can make changes in one central place and have it affect all usages, instead of
finding and replacing each API call manually.
Separate your concerns
AngularJS adopts a Model-View-Controller (MVC)-like pattern for structuring any
application. If you think about it, there are three parts to your application.
There is the actual data that you want to display to the user, or get the user to enter
through your application. This is the model in an AngularJS project, which is mostly
pure data, and represented using JSON objects.
Then there is the user interface or the final rendered HTML that the user sees and
interacts with, which displays the data to the user. This is the view.
Finally, there is the actual business logic and the code that fetches the data, decides
which part of the model to show to the user, how to handle validation, and so on
—core logic specific to your application. This is the controller for an AngularJS
application.
We think MVC or an MVC-like approach is neat for a few solid reasons:
• There is a clear separation of concerns between the various parts of your ap‐
plication. Need some business logic? Use the controller. Need to render some‐
thing differently? Go to the view.
• Your team and collaborators will have an instant leg up on understanding your
codebase because there is a set structure and pattern.
• Need to redesign your UI for any reason? No need to change any JavaScript
code. Need to change how something is validated? No need to touch your
HTML. You can work on independent parts of the codebase without spilling
over into another.
• AngularJS is not completely MVC; the controller will never have a direct ref‐
erence to the view. This is great because it keeps the controller independent of
the view, and also allows us to easily test the controller without needing to
instantiate a DOM.
Because of all of these reasons, MVC allows you develop and scale your application
in a way that is easy to maintain, extend, and test.
8 | Chapter 1: Introducing AngularJS
www.it-ebooks.info
Dependency Injection
AngularJS is the one of the few JavaScript frameworks with a full-fledged Depend‐
ency Injection system built in. Dependency Injection (discussed in Chapter 5) is
the concept of asking for the dependencies of a particular controller or service,
instead of instantiating them inline via the new operator or calling a function ex‐
plicitly (for example, DatabaseFactory.getInstance()). Some other part of your
code becomes responsible (in this case, the injector) for figuring out how to create
those dependencies and provide them when asked for.
This is helpful because:
• The controller, service, or function asking for the dependency does not need
to know how to construct its dependencies, and traverse further up the chain,
however long it might be.
• It’s explicit, so we immediately know what we need before we can start working
with our piece of code.
• It makes for super easy testing because we can replace heavy dependencies with
nicer mocks for testing. So instead of passing an HttpService that talks to the
real server, we pass in a MockHttpService that talks to a server created in
memory.
Dependency Injection in AngularJS is used across all of its parts, from con‐
trollers and services to modules and tests. It allows you to easily write modular,
reusable code so that you can use it cleanly and simply as needed.
Extensible
We already mentioned directives in the previous section when we talked about
AngularJS’s declarative nature. Directives are AngularJS’s way of teaching the
browser and HTML new tricks, from handling clicks and conditionals to creating
new structure and styling.
But that is just the built-in set of directives. AngularJS exposes the same API that
it uses internally to create these directives so that anyone can extend existing di‐
rectives or create their own. We can develop robust and complex directives that
integrate with third-party libraries like jQueryUI and BootStrap, to name a few, to
create a language that is specific to our needs. We’ll see how to create our own
directives in Chapter 11.
The bottom line is that AngularJS has a great core set of directives for us to get
started, and an API that allows us to do everything AngularJS does and more. Our
imagination is really the only limit for creating declarative, reusable components.
Test first, test again, keep testing
A lot of the benefits that we mentioned previously actually stem from the singular
focus on testing and testability that AngularJS has. Every bit and piece of AngularJS
Introducing AngularJS
www.it-ebooks.info
| 9
is designed to be testable, from its controllers, services, and directives to its views
and routes.
Between Dependency Injection and the controller being independent of references
to the view, the JS code that we write in an AngularJS application can easily be tested.
Because we get the same Dependency Injection system in our tests as in our pro‐
duction code, we can easily instantiate any service without worrying about its de‐
pendencies. All of this is run through our beautiful, insanely fast test runner, Karma.
Of course, to ensure that our application actually works end to end, we also have
Protractor, which is a WebDriver-based end-to-end scenario runner designed from
the ground up to be AngularJS-aware. This means that we will not have to write
any random waits and watches in our end-to-end test, like waiting for an element
to show or waiting for five seconds after a click for the server to respond. Protractor
is able to hook into AngularJS and figure out when to proceed with the test, leaving
us with a suite of solid, deterministic end-to-end tests.
We will start using Karma, and talk about how to set it up and get started in Chap‐
ter 3, and Protractor in Chapter 14. So there really is no excuse for your AngularJS
application not to be completely tested. Go ahead, you and your teammates will
thank yourself for it.
Now that you have had a brief overview of what makes AngularJS great, let’s see how to
get started with writing your own AngularJS applications.
Starting Out with AngularJS
Starting an AngularJS application has never been easier, but even before we jump into
that, let’s take a moment to answer a few simple questions to help you decide whether
or not AngularJS is the right framework for you.
What Backend Do I Need?
One of the first questions we usually get is regarding the kind of a backend one would
need to be able to write an AngularJS application. The very short answer is: there are
no such requirements.
AngularJS has no set requirements on what kind of a backend it needs to work as a
Single-Page Application. You are free to use Java, Python, Ruby, C#, or any other lan‐
guage you feel comfortable with. The only thing you do need is a way of communicating
back and forth with your server. Ideally, this would be via XHR (XML HTTP requests)
or sockets.
10
| Chapter 1: Introducing AngularJS
www.it-ebooks.info
If your server has REST or API endpoints that provide JSON values, then it makes your
life as a frontend developer even easier. But even if your server doesn’t return JSON,
that doesn’t mean you should abandon AngularJS. It’s pretty easy to teach AngularJS to
talk with an XML server, or any other format for that matter.
Does My Entire Application Need to Be an AngularJS App?
In a word, no. AngularJS has a concept (technically, a directive, but we’ll get to that in
the next section) called ng-app. This allows you to annotate any existing HTML element
with the tag (and not just the main <html> or <body> tag). This tells AngularJS that it
is only allowed to work on, control, and modify that particular section of the HTML.
This makes it pretty simple to start with a small section of an existing application and
then grow the part that AngularJS controls over time gradually.
A Basic AngularJS Application
Finally, with all that out of the way, let’s get to some code. We’ll start with the most basic
of AngularJS applications, which just imports the AngularJS library and proves that
AngularJS is bootstrapped and working:
<!-- File: chapter1/basic-angularjs-app.html -->
<!DOCTYPE html>
<html ng-app>
<body>
<h1>Hello {{1 + 2}}</h1>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
</body>
</html>
Examples in This Book
All the examples in this book are hosted at its GitHub repository. The
latest updated and correct version will always be available there in
case you run into any issues when running the example from the book.
Each example will also give the filename so that you can find it in the
GitHub repository. Each chapter has its own folder to make it easier
to find examples from the book.
There are two parts to starting an AngularJS application:
Starting Out with AngularJS | 11
www.it-ebooks.info
Loading the AngularJS source code
We have included the unminified version directly from the Google Hosted Libra‐
ries, but you could also have your own local version that you serve. The Google
CDN hosts all the latest versions of AngularJS that you can directly reference it
from, or download it from the AngularJS website.
Bootstrapping AngularJS
This is done through the ng-app directive. This is the first and most important
directive that AngularJS has, which denotes the section of HTML that AngularJS
controls. Putting it on the <html> tag tells AngularJS to control the entire HTML
application. We could also put it on the <body> or any other element on the page.
Any element that is a child of that will be handled with AngularJS and be annotated
with directives, and anything outside would not be processed.
Finally, we have our first taste of AngularJS one-way data-binding. We have put the
expressions “1+2” within double curly braces. The double curly is an AngularJS syntax
to denote either one-way data-binding or AngularJS expressions. If it refers to a variable,
it keeps the UI up to date with changes in the value. If it is an expression, AngularJS
evaluates it and keeps the UI up to date if the value of the expression changes.
If for any reason AngularJS had not bootstrapped correctly, we would have seen {{1 +
2}} in the UI, but if there are no errors, we should see Figure 1-1 in our browser.
Figure 1-1. Screenshot of a basic AngularJS application
AngularJS Hello World
Now that we’ve seen how to create an AngularJS application, let’s build the traditional
“hello world” application. For this, we will have an input field that allows users to type
in their name. Then, as the user types, we will update the UI with the latest value from
the text box. Sound complicated? Let’s see how it would look:
<!-- File: chapter1/angularjs-hello-world.html -->
<!DOCTYPE html>
<html>
<body ng-app>
<input type="text"
ng-model="name"
placeholder="Enter your name">
<h1>Hello <span ng-bind="name"></span></h1>
12
| Chapter 1: Introducing AngularJS
www.it-ebooks.info
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
</body>
</html>
We have added two new things from the last example, and kept two things:
• The AngularJS source code is still the same. The ng-app directive has moved to the
<body> tag.
• We have an input tag, with a directive called ng-model on it. The ng-model directive
is used with input fields whenever we want the user to enter any data and get access
to the value in JavaScript. Here, we tell AngularJS to store the value that the user
types into this field in a variable called name.
• We also have another directive called ng-bind. ng-bind and the double-curly no‐
tation are interchangeable, so instead of <span ng-bind="name"></span>, we
could have written {{ name }}. Both accomplish the same thing, which is putting
the value of the variable name inside the tag, and keeping it up to date if it changes.
The end result is captured in Figure 1-2.
Figure 1-2. AngularJS “hello world” example screenshot
Conclusion
We wrote two very simple AngularJS applications. The first demonstrated how to create
a very simple AngularJS application, and the second showcased the power of AngularJS
two-way data-binding. The best part was that we were able to do that without writing
a single line of JavaScript. The same application in pure JavaScript would require us to
create listeners and jQuery DOM manipulators. We were able to do away with all of
that.
Conclusion | 13
www.it-ebooks.info
We also went over the basic philosophies of AngularJS and some of its benefits and how
it differs from existing solutions. In the next chapter, we become familiar with some of
the most common pieces of AngularJS, such as common directives, working with con‐
trollers, and using services.
14
| Chapter 1: Introducing AngularJS
www.it-ebooks.info
CHAPTER 2
Basic AngularJS Directives and Controllers
We saw in Chapter 1 how to create a very simple and trivial AngularJS application, which
was basically the “hello world” of the AngularJS world. In this chapter, we will expand
on that example.
We explore AngularJS modules and controllers, and create our very own controllers.
Then we use these controllers to load data or state into our application, and manipulate
the HTML to perform common tasks such as displaying an array of items in the UI,
hiding and showing elements conditionally, styling HTML elements based on certain
conditions, and more.
AngularJS Modules
The very first thing we want to introduce is the concept of modules. Modules are An‐
gularJS’s way of packaging relevant code under a single name. For someone coming
from a Java background, a simple analogy is to think of modules as packages.
An AngularJS module has two parts to it:
• A module can define its own controllers, services, factories, and directives. These
are functions and code that can be accessed throughout the module.
• The module can also depend on other modules as dependencies, which are defined
when the module is instantiated. What this means is that AngularJS will go and find
the module with that particular name, and ensure that any functions, controllers,
services, etc. defined in that module are made available to all the code defined in
this module.
In addition to being a container for related JavaScript, the module is also what AngularJS
uses to bootstrap an application. What that means is that we can tell AngularJS what
15
www.it-ebooks.info
module to load as the main entry point for the application by passing the module name
to the ng-app directive.
Let’s clear this up with the help of a few examples.
This is how we define a module named notesApp:
angular.module('notesApp', []);
The first argument to the module function in AngularJS is the name of the module. Here,
we define a module named notesApp. The second argument is an array of module
names that this module depends on. Do note the empty square brackets we pass as the
second argument to the function. This tells AngularJS to create a new module with the
name notesApp, with no dependencies.
This is how we define a module named notesApp, which depends on two other modules:
notesApp.ui, which defines our UI widgets, and thirdCompany.fusioncharts, which
is a third-party library for charts:
angular.module('notesApp',
['notesApp.ui', 'thirdCompany.fusioncharts']);
If we want to load an existing module that has already been defined in some other file,
we use the the angular.module function with just the first argument, as follows:
angular.module('notesApp');
This line of code tells AngularJS to find an existing module named notesApp, and to
make it available to use, add, or modify in the current file. This is how we refer to the
same module across multiple files and add code to it.
There are two common mistakes to watch out for:
• Trying to define a module, but forgetting to pass in the second argument. This
would cause AngularJS to try to look up a module instead of defining one, and we
would get an error (“No module found”).
• Trying to load a module from another file to modify, but the file that defines the
module has not been loaded first. Make sure the file that defines the module is
loaded first in your HTML before you try to use it.
Now that the module has been defined, how do we use it? We can of course add our
functionality to it, and modularize our codebase into distinct sections. But more im‐
portantly, we can tell AngularJS to use these modules to bootstrap our application. The
ng-app directive takes an optional argument, which is the name of the module to load
during bootstrapping.
Let’s take a look at a complete example to make sense of this:
16
| Chapter 2: Basic AngularJS Directives and Controllers
www.it-ebooks.info
<!-- File: chapter2/module-example.html -->
<html ng-app="notesApp">
<head><title>Hello AngularJS</title></head>
<body>
Hello {{1 + 1}}nd time AngularJS
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', []);
</script>
</body>
</html>
This example defines a module (note the empty array as the second argument), and then
lets AngularJS bootstrap the module through the ng-app directive.
Creating Our First Controller
We saw how to create modules, but what do we do with them? So far, they have just been
empty modules.
Let’s now take a look at controllers. Controllers in AngularJS are our workhorse, the
JavaScript functions that perform or trigger the majority of our UI-oriented work. Some
of the common responsibilities of a controller in an AngularJS application include:
• Fetching the right data from the server for the current UI
• Deciding which parts of that data to show to the user
• Presentation logic, such as how to display elements, which parts of the UI to show,
how to style them, etc.
• User interactions, such as what happens when a user clicks something or how a text
input should be validated
An AngularJS controller is almost always directly linked to a view or HTML. We will
never have a controller that is not used in the UI (that kind of business logic goes into
services). It acts as the gateway between our model, which is the data that drives our
application, and the view, which is what the user sees and interacts with.
So let’s take a look at how we could go about creating a controller for our notesApp
module:
<!-- File: chapter2/creating-controller.html -->
<html ng-app="notesApp">
<head><title>Hello AngularJS</title></head>
<body ng-controller="MainCtrl">
Hello {{1 + 1}}nd time AngularJS
Creating Our First Controller
www.it-ebooks.info
| 17
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
// Controller-specific code goes here
console.log('MainCtrl has been created');
}]);
</script>
</body>
</html>
We define a controller using the controller function that is exposed on an AngularJS
module. The controller function takes the name of the controller as the first argument,
which in the previous example is creatively named MainCtrl. The second argument is
the actual controller definition, of what it does and how it does it.
But there is a slight twist here, which is the array notation. Notice that we have defined
our controller definition function inside an array. That is, the first argument to the
controller function on the module is the name of the controller (MainCtrl), and the
second argument is an array. The array holds all the dependencies for the controller as
string variables, and the last argument in the array is the actual controller function. In
this case, because we have no dependencies, the function is the only argument in the
array. The function then houses all the controller-specific code.
We also introduce a new directive, ng-controller. This is used to tell AngularJS to go
instantiate an instance of the controller with the given name, and attach it to the DOM
element. In this case, it would load MainCtrl, which would end up printing the
console.log() statement.
Dependency Injection Syntax and AngularJS
The notation that we have used is one of the two ways in which we
can declare AngularJS controllers (or services, directives, or filters).
The style we have used (and will use for the remainder of the book),
which is also the recommended way, is safe-style of Dependency In‐
jection, or declaration. We could also use:
angular.module('notesApp', [])
.controller('MainCtrl', function() {
});
and it would work similarly, but it might cause problems when we
have a build step that minifies our code. We will delve into this more
when we introduce Dependency Injection in Chapter 5.
18
| Chapter 2: Basic AngularJS Directives and Controllers
www.it-ebooks.info
Now, for our first AngularJS application with a controller, we are going to move the
“hello world” message from the HTML to the controller, and get and display it from the
controller. Let’s see how this would look:
<!-- File: chapter2/hello-controller.html -->
<html ng-app="notesApp">
<head><title>Notes App</title></head>
<body ng-controller="MainCtrl as ctrl">
{{ctrl.helloMsg}} AngularJS.
<br/>
{{ctrl.goodbyeMsg}} AngularJS
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
this.helloMsg = 'Hello ';
var goodbyeMsg = 'Goodbye ';
}]);
</script>
</body>
</html>
If we run this application, our UI should look something like Figure 2-1.
Figure 2-1. “Hello world” controller example screenshot
Yes, we only see “Hello AngularJS.” The “Goodbye” message is not printed in the UI.
Let’s dig into the example to see if we can clarify what is happening:
• We defined our notesApp module as we saw before.
• We created a controller called MainCtrl using the controller function on the
module.
• We defined the variable helloMsg on the controller’s instance (using the this key‐
word), and the variable goodbyeMsg as a local inner variable in the controller’s in‐
stance (using the var keyword).
Creating Our First Controller
www.it-ebooks.info
| 19
• We used this controller in the UI through the use of another directive: ngcontroller. This directive allows us to associate an instance of a controller with a
UI element (in this case, the body tag).
• We also gave this particular instance of the MainCtrl a name when we used ngcontroller. Here, we called it ctrl. This is known as the controllerAs syntax in
AngularJS, where we can give each instance of the controller a name to recognize
its usage in the HTML.
• We then referred to the helloMsg and goodbyeMsg variables from the controller in
the HTML using the double-curly notation.
By now, it should be obvious that variables that were defined on the this keyword in
the controller are accessible from the HTML, but local, inner variables are not.
Furthermore, any variable defined on the controller instance (on this in the controller,
as opposed to declaring variables with the var keyword like goodbyeMsg) can be accessed
and displayed to the user via the HTML. This is basically how we funnel and expose
data from our controller and business logic to the UI.
Getting Data to the HTML
Changing ctrl.goodbyeMsg to goodbyeMsg in the HTML will not
help either. We will not get the value of the goodbyeMsg variable from
the controller to the UI without declaring it on the controller in‐
stance using the this keyword.
Anything that the user needs to see, or the HTML needs to use, needs to be defined on
this. Anything that the HTML does not directly access should not be put on this, but
should rather be saved as local variables in the controller’s scope, similar to goodbyeMsg.
$scope Versus controllerAs Syntax
If you used AngularJS prior to 1.2, you might have expected the $scope variable to be
injected into the controller, and the variables helloMsg and goodbyeMsg to be set on it.
In AngularJS 1.2 and later, there is a new syntax, the controllerAs syntax, which allows
us to define the variables on the controller instance using the this keyword, and refer
to them through the controller from the HTML.
The advantage of this over the earlier syntax is that it makes it explicit in the HTML
which variable or function is provided by which controller and which instance of the
controller. So with a complicated, nested UI, you don’t need to play a game of “Where’s
Waldo?” to find your variables in your codebase. It becomes immediately obvious be‐
cause the controller instance is present in the HTML.
20
| Chapter 2: Basic AngularJS Directives and Controllers
www.it-ebooks.info
Let’s take a look at one more example before we delve into how AngularJS works and
accomplishes this:
<!-- File: chapter2/controller-click-message.html -->
<html ng-app="notesApp">
<head><title>Notes App</title></head>
<body ng-controller="MainCtrl as ctrl">
{{ctrl.message}} AngularJS.
<button ng-click="ctrl.changeMessage()">
Change Message
</button>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.message= 'Hello ';
self.changeMessage = function() {
self.message = 'Goodbye';
};
}]);
</script>
</body>
</html>
What has changed here?
• We now have only one binding, which is in the ctrl.message variable.
• There is a button with the label “Change Message.” There is a built-in directive on
it, ng-click, to which we pass a function as an argument. The ng-click directive
evaluates any expression passed to it when the button is clicked. In this case, we tell
AngularJS to trigger the controller’s changeMessage() function.
• The changeMessage() function in the controller sets the value of message to
“Goodbye.”
• Also, as good practice, we avoid referring to the this keyword inside the controller,
preferring to use a proxy self variable, which points to this. The following note
has more information on why this is recommended.
What we will see in play is that the app starts with “Hello AngularJS,” but the moment
we click the button, the text changes to “Goodbye AngularJS.” This is the true power of
data-binding in AngularJS. Here are a few things of note from the example:
• The controller we wrote has no direct access to the view or any of the DOM elements
that it needs to update. It is pure JavaScript.
Creating Our First Controller
www.it-ebooks.info
| 21
• When the user clicked the button and changeMessage was triggered, we did not
have to tell the UI to update. It happened automatically.
• The HTML connects parts of the DOM to controllers, functions, and variables, and
not the other way around.
This is one of the core principles of AngularJS at play here. An AngularJS application
is a data-driven app. We routinely say “The model is the truth” in an AngularJS appli‐
cation. What this means is that our whole aim in an AngularJS application should be to
manipulate and modify the model (pure JavaScript), and let AngularJS do the heavy
lifting of updating the UI accordingly.
Before we talk about how AngularJS accomplishes this, let’s take a look at one more
example that will help clarify things.
this in JavaScript
People used to languages like Java have trouble getting their heads
around the this keyword in JavaScript. One of the insane and crazy
(and downright cool) things about JavaScript is that the this key‐
word inside a function can be overridden by whoever calls the func‐
tion. Thus, the this outside and inside a function can refer to two
completely different objects or scopes.
Thus, it is generally better to assign the this reference inside a con‐
troller to a proxy variable, and always refer to the instance through
this proxy (self, for example) to be assured that the instance we are
referring to is the correct one.
If you want to read more about this, as well as understand a bit more
about the craziness that can be JavaScript, do check out Kyle Simp‐
son’s You Don’t Know JS: this & Object Prototypes (O’Reilly, 2014).
Working with and Displaying Arrays
We have seen how to create a controller, and how to get data from the controller into
the HTML. But we worked with very simplistic string messages. Let’s now take a look
at how we would work with a collection of data; for example:
<!-- File: chapter2/ng-repeat-example-1.html -->
<html ng-app="notesApp">
<head><title>Notes App</title></head>
<body ng-controller="MainCtrl as ctrl">
<div ng-repeat="note in ctrl.notes">
<span class="label"> {{note.label}}</span>
<span class="status" ng-bind="note.done"></span>
</div>
22
| Chapter 2: Basic AngularJS Directives and Controllers
www.it-ebooks.info
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.notes = [
{id: 1, label: 'First Note', done: false},
{id: 2, label: 'Second Note', done: false},
{id: 3, label: 'Done Note', done: true},
{id: 4, label: 'Last Note', done: false}
];
}]);
</script>
</body>
</html>
We introduced a few new concepts in this example. Before we delve into those, Figure 2-2
shows how the HTML and JS would look when we run it.
Figure 2-2. ng-repeat example screenshot
First things first, we removed the message variable and introduced an array of JSON
objects in our MainCtrl. We exposed this on the controller instance with the name
notes.
In our HTML, we used a directive called ng-repeat. ng-repeat is one of the most
versatile and heavily used directives of AngularJS, because it allows us to iterate over an
array or over the keys and values of an object and display them in the HTML.
When we use the ng-repeat directive, the contents of the element on which the directive
is applied is considered the template of the ng-repeat. AngularJS picks up this template,
makes a copy of it, and then applies it for each instance of the ng-repeat. In the previous
case, the label and status span elements were repeated four times, once for each item
in the notes array.
Working with and Displaying Arrays
www.it-ebooks.info
| 23
The ng-repeat is basically the same as a for each loop in any programming language,
so the syntax is similar:
ng-repeat="eachVar in arrayVar"
We’ll cover more details about ng-repeat in the following section.
The next point of interest is the template that we used for the ng-repeat. Inside the
context of the ng-repeat, we now have a new variable, note, which is not present in
our controller. This is created by ng-repeat, and each instance of the ng-repeat has its
own version and value of note, obtained from each item of the array.
The final thing to note is that we used the double-curly notation to print the note’s label,
but used a directive called ng-bind for the note’s done field. There is no functional
difference between the two; both take the value from the controller and display it in the
UI. Both of them also keep it data-bound and up to date, so if the value underneath
changes, the UI will change automatically. We can use them interchangeably, because
the expression between the double curly braces will directly drop into the ng-bind.
Using ng-bind Versus Double Curlies
The advantage ng-bind has over the double-curly notation is that it
takes AngularJS time to bootstrap and execute before it can find and
replace all the double curly braces from the HTML. That means, for
a portion of a second while the browser starts, you might see flash‐
ing double curly braces in the UI before AngularJS has the chance to
kick in and replace them. This is only for the very first page load, and
not on views loaded after the first load. You will not have that issue
with ng-bind. You can also get around that issue with the ng-cloak
directive.
Waiting for AngularJS to Load
AngularJS has a directive called ng-cloak, which is a mechanism to hide sections of the
page while AngularJS bootstraps and finishes loading. ng-cloak uses the following CSS
rules, which are automatically included when you load angular.js or angular.min.js:
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak],
.ng-cloak, .x-ng-cloak {
display: none !important;
}
After this, any section or element that needs to be hidden in your HTML needs to have
class="ng-cloak" added to it. This applies the preceding CSS and hides the element
by default. After AngularJS finishes loading, it goes through your HTML and removes
ng-cloak from all these elements, thus ensuring that your UI is shown after AngularJS
has finished bootstrapping.
24
| Chapter 2: Basic AngularJS Directives and Controllers
www.it-ebooks.info
You can apply ng-cloak on the body tag, but it is often better to add it on smaller sections
so that your application can load progressively instead of all at once.
Do note that the ng-cloak styling is loaded as part of the angular.js source code. So if
you load your AngularJS library at the very end of your HTML (as you should), the style
will not be applied to the HTML until AngularJS has finished loading. Thus it is often
a good idea to include the preceding CSS as part of your own CSS to ensure it is loaded
upfront before your HTML starts displaying.
With this example in place, let’s dig in and understand how AngularJS is working behind
the scenes:
1. The HTML is loaded. This triggers requests for all the scripts that are a part of it.
2. After the entire document has been loaded, AngularJS kicks in and looks for the
ng-app directive.
3. When it finds the ng-app directive, it looks for and loads the module that is specified
and attaches it to the element.
4. AngularJS then traverses the children DOM elements of the root element with the
ng-app and starts looking for directives and bind statements.
5. Each time it hits an ng-controller or an ng-repeat directive, it creates what we
call a scope in AngularJS. A scope is the context for that element. The scope dictates
what each DOM element has access to in terms of functions, variables, and the like.
6. AngularJS then adds watchers and listeners on the variables that the HTML ac‐
cesses, and keeps track of the current value of each of them. When that value
changes, AngularJS updates the UI immediately.
7. Instead of polling or some other mechanism to check if the data has changed, An‐
gularJS optimizes and checks for updates to the UI only on certain events, which
can cause a change in the data or the model underneath. Examples of such events
include XHR or server calls, page loads, and user interactions like click or type.
In our previous example with the ng-repeat, we have a template that shows the note’s
label and status. That template accesses a variable called note, which is created in our
for each loop that is the ng-repeat. Now, if each template accessed the same context,
each instance would show the same text. But to ensure that each template instance of
the ng-repeat shows its own value, each ng-repeat also gets its own scope with a
variable called note defined in it, which is specific to it.
Also note that while the ng-repeat instances each get their own scope, they still have
access to the parent scope. If there were a function in our controller that we wanted to
access within the ng-repeat, we could still do that.
Working with and Displaying Arrays
www.it-ebooks.info
| 25
In summation, AngularJS creates scopes or context for various elements in the DOM
to ensure that there is no global state and each element accesses only what is relevant to
it. These scopes have a parent-child relation by default, which allows children scopes to
access functions and controllers from a parent scope.
More Directives
Let’s now build on our example, and add more functionality to our ongoing application:
<!-- File: chapter2/more-directives.html -->
<html ng-app="notesApp">
<head>
<title>Notes App</title>
<style>
.done {
background-color: green;
}
.pending {
background-color: red;
}
</style>
</head>
<body ng-controller="MainCtrl as ctrl">
<div ng-repeat="note in ctrl.notes"
ng-class="ctrl.getNoteClass(note.done)">
<span class="label"> {{note.label}}</span>
<span class="assignee"
ng-show="note.assignee"
ng-bind="note.assignee">
</span>
</div>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', []).controller('MainCtrl', [
function() {
var self = this;
self.notes = [
{label: 'First Note', done: false, assignee: 'Shyam'},
{label: 'Second Note', done: false},
{label: 'Done Note', done: true},
{label: 'Last Note', done: false, assignee: 'Brad'}
];
self.getNoteClass = function(status) {
return {
done: status,
26
| Chapter 2: Basic AngularJS Directives and Controllers
www.it-ebooks.info
pending: !status
};
};
}]);
</script>
</body>
</html>
We added two more directives in this example. Let’s take a look at what they are and
what they do:
ng-show
There are two directives in AngularJS that deal with hiding and showing HTML
elements: ng-show and ng-hide. They inspect a variable and, depending on the
truthiness of its value, show or hide elements in the UI, respectively. In this case,
we say show the assignee span if note.assignee is true. AngularJS treats true,
nonempty strings, nonzero numbers, and nonnull JS objects as truthy. So in this
case, we get to see the assignee span if the note has an assignee.
ng-class
The ng-class directive is used to selectively apply and remove CSS classes from
elements. There are multiple ways of using ng-class, and we will talk about what
we feel is the most declarative and cleanest option. The ng-class directive can take
strings or objects as values. If it is a string, it simply applies the CSS classes directly.
If it is an object (which we are returning from the function in the controller), An‐
gularJS takes a look at each key of the object, and depending on whether the value
for that key is true or false, applies or removes the CSS class.
In this case, the CSS class done gets added and pending gets removed if note.done is
true, and done gets removed and pending gets added if note.done is false.
Notice also that ng-bind, ng-show, and most of these directives can directly refer to a
variable on the controller or call a function to get the value, as we did with the
ng-class. We can pass variables and arguments to the function as normal by directly
referring to the variable. No need for the double-curly syntax.
Working with ng-repeat
The ng-repeat directive is one of the most versatile directives in AngularJS, and can be
used for a whole variety of situations and requirements. We saw how we can use it to
repeat an array in the previous examples. In this section, we will explore some of the
other options we have when using the ng-repeat directive.
Working with ng-repeat | 27
www.it-ebooks.info
ng-repeat Over an Object
Just like we used the ng-repeat directive to show an array of elements in the HTML,
we can also use it to show all the keys and values of an object:
<!-- File: chapter2/ng-repeat-object.html -->
<html ng-app="notesApp">
<head><title>Notes App</title></head>
<body ng-controller="MainCtrl as ctrl">
<div ng-repeat="(author, note) in ctrl.notes">
<span class="label"> {{note.label}}</span>
<span class="author" ng-bind="author"></span>
</div>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.notes = {
shyam: {
id: 1,
label: 'First Note',
done: false
},
Misko: {
id: 3,
label: 'Finished Third Note',
done: true
},
brad: {
id: 2,
label: 'Second Note',
done: false
}
};
}]);
</script>
</body>
</html>
In this example, we have intentionally capitalized Misko while leaving brad and shyam
lowercase. When we use the ng-repeat directive over an object instead of an array, the
keys of the object will be sorted in a case-sensitive, alphabetic order. That is, uppercase
first, and then sorted by alphabet. So in this case, the items would be shown in the HTML
in the following order: Misko, brad, shyam.
28
| Chapter 2: Basic AngularJS Directives and Controllers
www.it-ebooks.info
The ng-repeat directive takes an argument in the form variable in arrayExpres
sion or (key, value) in objectExpression. When used with an array, the items will
be in the order in which they exist in the array.
Helper Variables in ng-repeat
The ng-repeat directive also exposes some variables within the context of the HTML
template that gets repeated, which allows us to gain some insight into the current
element:
<!-- File: chapter2/ng-repeat-helper-variables.html -->
<html ng-app="notesApp">
<head><title>Notes App</title></head>
<body ng-controller="MainCtrl as ctrl">
<div ng-repeat="note in ctrl.notes">
<div>First Element: {{$first}}</div>
<div>Middle Element: {{$middle}}</div>
<div>Last Element: {{$last}}</div>
<div>Index of Element: {{$index}}</div>
<div>At Even Position: {{$even}}</div>
<div>At Odd Position: {{$odd}}</div>
<span class="label"> {{note.label}}</span>
<span class="status" ng-bind="note.done"></span>
<br/><br/>
</div>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.notes = [
{id: 1, label: 'First Note', done: false},
{id: 2, label: 'Second Note', done: false},
{id: 3, label: 'Done Note', done: true},
{id: 4, label: 'Last Note', done: false}
];
}]);
</script>
</body>
</html>
In this example, we use the same array that we did in the example with the ng-repeat
over the array of items. The only difference is that we now display more state about the
item being repeated in the HTML. For each item, we display which index the item is in,
and whether it is the first, middle, last, odd, or even item.
Working with ng-repeat | 29
www.it-ebooks.info
Each of the $ prefixed variables we use within the context of the ng-repeat are provided
by AngularJS, and refer to the state of the repeater for that particular element. They
include:
• $first, $middle, and $last are Boolean values that tell us whether that particular
element is the first, between the first and last, or the last element in the array or
object.
• $index gives us the index or position of the item in the array.
• $odd and $even tell us if the item is in an index that is odd or even (we could use
this for conditional styling of elements, or other conditions we might have in our
application).
Do note that in the case of an ng-repeat over an object, all of these list items exist and
are still applicable, but the index of the item may or may not correspond to the order
in which we declare the keys in the object. This is because of the way AngularJS ngrepeat sorts the keys of the object alphabetically, as we saw in the “ng-repeat Over an
Object” on page 28.
Track by ID
By default, ng-repeat creates a new DOM element for each value in the array or object
that we iterate over. But to optimize performance, it caches or reuses DOM elements if
the objects are exactly the same, according to the hash of the object (calculated by
AngularJS).
In some cases, we might want AngularJS to reuse the same DOM element, even if the
object instance does not hash to the same value. That is, if we have objects coming from
a database and we do not care about the exact object properties, we want AngularJS to
treat two objects with the same ID as identical for the purpose of the repeat. For this
purpose, AngularJS allows us to provide a tracking expression when specifying our
ng-repeat:
<!-- File: chapter2/ng-repeat-track-by-id.html -->
<html ng-app="notesApp">
<body ng-controller="MainCtrl as ctrl">
<button ng-click="ctrl.changeNotes()">Change Notes</button>
<br/>
DOM Elements change every time someone clicks
<div ng-repeat="note in ctrl.notes1">
{{note.$$hashKey}}
<span class="label"> {{note.label}}</span>
<span class="author" ng-bind="note.done"></span>
</div>
<br/>
30
| Chapter 2: Basic AngularJS Directives and Controllers
www.it-ebooks.info
DOM Elements are reused every time someone clicks
<div ng-repeat="note in ctrl.notes2 track by note.id">
{{note.$$hashKey}}
<span class="label"> {{note.label}}</span>
<span class="author" ng-bind="note.done"></span>
</div>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
var self = this;
var notes = [
{
id: 1,
label: 'First Note',
done: false,
someRandom: 31431
},
{
id: 2,
label: 'Second Note',
done: false
},
{
id: 3,
label: 'Finished Third Note',
done: true
}
];
self.notes1 = angular.copy(notes);
self.notes2 = angular.copy(notes);
self.changeNotes = function() {
notes = [
{
id: 1,
label: 'Changed Note',
done: false,
someRandom: 4242
},
{
id: 2,
label: 'Second Note',
done: false
},
{
id: 3,
label: 'Finished Third Note',
done: true
}
Working with ng-repeat | 31
www.it-ebooks.info
];
self.notes1 = angular.copy(notes);
self.notes2 = angular.copy(notes);
};
}]);
</script>
</body>
</html>
Here we have two arrays, notes1 and notes2, which are identical in all respects. Both
of them are shown in the UI using the ng-repeat directive. The difference is that one
uses the plain vanilla ng-repeat (ng-repeat="note in ctrl.notes1") and the other
uses the track by ID version (ng-repeat="note in ctrl.notes2 track by note.id").
We also included an ng-click, which we used before. This allows us to trigger a function
in our controller whenever someone clicks that element. In this case, we call change
Notes() on our controller. The function changes the notes arrays to a new array.
Now we can see that the hashKeys and the DOM elements in the first ng-repeat are
getting changed every time we click a button. In the second ng-repeat, there is no $
$hashKey that AngularJS needs to generate, because we tell it what the unique identifier
is for each element. So the DOM elements are reused based on the ID of the object.
Do not use any variables that start with $$ in your application. An‐
gularJS uses them to denote private variables that it uses for its own
purposes, and does not guarantee their presence or continued work‐
ing across different versions of AngularJS. If you find yourself reach‐
ing out to a $$ variable, stop! You need to rethink your approach.
We would use the track-by expression to optimize DOM manipulation in our appli‐
cation. This would generally be on the IDs of objects returned from our databases, to
ensure AngularJS reuses DOM elements even if we fetch the data multiple times from
the server.
ng-repeat Across Multiple HTML Elements
An uncommon requirement, but something that still pops up every now and then, is
the ability to repeat multiple sibling HTML elements that may not be in a single con‐
tainer element. For example, think of the case where we need to repeat two table rows
(<tr>) for each item in our array, maybe one as a header row and one as a child row.
For these kinds of situations, AngularJS provides the ability to mark where our ngrepeat starts and tell which HTML element is considered part of the ng-repeat. It does
so through the use of ng-repeat-start and ng-repeat-end directives:
32
| Chapter 2: Basic AngularJS Directives and Controllers
www.it-ebooks.info
<!-- File: chapter2/ng-repeat-across-elements.html -->
<html ng-app="notesApp">
<body ng-controller="MainCtrl as ctrl">
<table>
<tr ng-repeat-start="note in ctrl.notes">
<td>{{note.label}}</td>
</tr>
<tr ng-repeat-end>
<td>Done: {{note.done}}</td>
</tr>
</table>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.notes = [
{id: 1, label: 'First Note', done: false},
{id: 2, label: 'Second Note', done: false},
{id: 3, label: 'Finished Third Note', done: true}
];
}]);
</script>
</body>
</html>
In this example, we are creating a table to display the list of notes. For each note, we
want a row where we display the label of the note, followed by a second table row where
we display the status of the note. This could very well contain the author information,
when it was created, and so on. Because we can’t have a wrapper element around the tr
table row element, we can use the ng-repeat-start and ng-repeat-end directives.
We mark the first tr as where our ng-repeat starts, and use our traditional ng-repeat
expression as the argument. We then define our template, which contains the first ele‐
ment with the label, and then move on to the second table row element. We mark this
element as where our repeater ends by using the ng-repeat-end directive.
AngularJS will then ensure that it creates both tr elements for each element in the array
we are repeating.
Working with ng-repeat | 33
www.it-ebooks.info
Conclusion
We covered the very basic features of AngularJS, and introduced some commonly used
directives like ng-repeat and ng-click. We used these to show arrays and objects in
the UI, as well as to handle user interactions and style applications conditionally. We
also saw how to create controllers, and how to get data from our controllers into our
views. Finally, we did a deep dive into the ng-repeat to see what it is and how we could
use it in a variety of situations.
As we mentioned earlier, AngularJS has a huge focus on unit testing. To this end, each
and every part of AngularJS is easy to unit test. In the next chapter, we will see how we
might write some simple unit tests for the controllers we have written so far. We will
cover how unit testing is done in AngularJS, how to set up Karma (which is used to run
unit tests in AngularJS), and how we can instantiate controllers and set our expectations
on their behavior.
34
| Chapter 2: Basic AngularJS Directives and Controllers
www.it-ebooks.info
CHAPTER 3
Unit Testing in AngularJS
In Chapter 2, we looked at starting a very basic AngularJS application. We saw some
commonly used AngularJS directives, and then looked at using controllers to add be‐
havior and functionality to our application.
JavaScript does not have all the niceties of a strongly typed language like type safety and
a compiler. That puts the onus on us developers to ensure that what we write actually
works as intended, and continues to run as intended well into the future. Unit tests
provide a way to write expectations about how code should behave, and running them
in an automated way ensures that behaviors don’t change unexpectedly.
Because AngularJS and testing are so well integrated, we introduce a small chapter on
testing after each major concept to show how each concept can be unit tested inde‐
pendently, and in isolation. We also dive into Karma, the unit test runner for AngularJS,
and Jasmine, the test framework. Finally, we bring all these pieces together to write our
first AngularJS unit test.
Unit Testing: What and Why?
Unit testing is the concept of taking a single function or part of our code, and writing
assertions and tests to ensure that it works as intended. While very common on the
server side, we often find the habit of writing unit tests amiss when developing clientside applications. Here are five reasons why we should write unit tests when working
on a JavaScript-based client-side application:
Proof of correctness
Without unit tests, there is blind reliance on end-to-end manual tests to ensure that
our feature or part of the application actually works. Unit tests act as proof that
what we have developed actually does what it is supposed to, that we have handled
all the edge cases correctly, and that it delivers the correct result in all these cases.
35
www.it-ebooks.info
Lack of compiler
In JavaScript, there is nothing like the Java compiler to tell us that something doesn’t
work. It is up to the browser to ultimately tell us that something is broken, and even
then, different browsers produce different results. Unit tests can be run well before
our application makes it out to the browser to catch problems and warn us about
our assumptions.
Catch errors early
Without unit tests, we would only know about an error in our application after we
hit refresh in our browser and saw the live application. Unit tests can help us catch
errors much earlier, reducing turnaround time and thus increasing our develop‐
ment speed.
Prevent regressions
At the end of the day, we are unlikely to be the only person working on our codebase.
Other developers will inevitably rely on or actively change parts of the codebase
that we developed. You can ensure that they don’t change any fundamental as‐
sumptions by providing a set of unit tests that prevent regressions and bugs in the
future.
Specification
Comments have a bad habit of becoming outdated. Unit tests in AngularJS, espe‐
cially written using Jasmine, look and read like English. And because unit tests break
when the underlying code changes, we are forced to keep comments updated. So
unit tests can act as a living, breathing specification for our codebase.
An Introduction to TDD
Test-driven development, or TDD, is an AGILE methodology that flips the development
life cycle by ensuring that tests are written, before the code is implemented, and that
tests drive the development (and are not just used as a validation tool).
The tenets of TDD are simple:
• Code is written only when there is a failing test that requires the code to pass.
• The bare minimum amount of code is written to ensure that the test passes.
• Duplication is removed at every step.
• When all tests pass, the next failing test is added for the next required functionality.
These simple rules ensure that:
• Code develops organically, and that every line of code is purposeful.
• Code remains highly modular, cohesive, and reusable (because we need to be able
to test it).
36
| Chapter 3: Unit Testing in AngularJS
www.it-ebooks.info
• We provide a comprehensive array of tests to prevent future breakages and bugs.
• The tests also act as specification, and thus documentation, for future needs and
changes.
We at AngularJS have found these tenets and rules to be true, and the entire AngularJS
codebase has been developed using TDD. For an uncompiled, dynamic language like
JavaScript, we strongly believe that having a good set of unit tests reduces headaches in
the future.
Introduction to Karma
Karma is the test runner that makes running tests painless and amazingly fast. It uses
NodeJS and SocketIO to facilitate tests in multiple browsers at insanely fast speeds.
Test Runner Versus Testing Framework
We have often noticed that developers can sometimes get confused
between the test runner and the testing framework. This could be
because the same library often handles both responsibilities.
When working with JS (and AngularJS), we have two separate tools/
libraries for each purpose. Karma, which is the test runner, is solely
responsible for finding all the unit tests in our codebase, opening
browsers, running the tests in them, and capturing results. It does not
care what language or framework we use for writing the tests; it sim‐
ply runs them.
Jasmine is the testing framework we will use. Jasmine defines the
syntax with which we write our tests, the APIs, and the way we write
our assertions. It is possible to not use Jasmine, and instead use
something like mocha or some other framework to write tests for
AngularJS.
Karma is a great test runner, and its aim is to make testing as simple and fast as possible.
It makes it easy to set up tests and then gets out of the way, letting us developers get
instant feedback on our code and tests.
These are the steps to install Karma (as of this writing):
1. Install NodeJS. You can get the installers from here.
2. Install Karma CLI, which allows you to run Karma in an easier step. This is an npm
(Node Package Manager) package, so you can install it with the following
command:
sudo npm install -g karma-cli
Introduction to Karma
www.it-ebooks.info
| 37
Windows users can also run this from the command line after NodeJS has been
installed successfully.
3. Install Karma locally in the folder where you need to run these tests:
npm install karma
4. Karma has a concept called plugins, which allow you to choose only the components
you need for your project. These plugins allow you to choose which framework you
use for writing your unit tests (Karma is framework agnostic), which browsers to
launch, and so on. To start off, we will install the Jasmine plugin to write our tests
in Jasmine, and the Chrome launcher to start Google Chrome automatically. Install
these two with the following command:
npm install karma-jasmine karma-chrome-launcher
Be sure that you run the last two commands from inside the folder where you down‐
loaded the code repository. These are specific to each project, and will need to be run
for every new project.
The recommended way to run Karma on our projects has changed
with the newer releases of Karma, which recommend a local instal‐
lation of Karma for each project as opposed to a global installation of
the Karma package. Local installation is what we demonstrated in the
installation instructions earlier.
This also means that instead of directly running Karma commands
from the command line, we have to execute them with the path to
Karma from the local npm installation folder. That is:
karma start myconf.js
would have to be written as:
node_modules/karma/bin/karma start myconf.js
To stop doing this, Karma has an npm package called karma-cli,
which we installed. This allows us to execute Karma without the full
path, because it will underneath pick up the local Karma installation.
Karma Plugins
We installed two plugins for Karma in the previous section. Let’s explore the concept of
Karma plugins a bit more. Karma plugins can be broadly split into the following
categories:
Browser launchers
The first type of plugins for Karma are ones that help Karma launch browsers
automatically as part of a test run. We installed the Chrome browser launcher plu‐
gin, and there are similar ones for Firefox, IE, and many more.
38
| Chapter 3: Unit Testing in AngularJS
www.it-ebooks.info
Testing frameworks
We can also choose the type of framework we want to use when we write unit tests.
We will be using Jasmine, which we installed again in the previous section, but if
you prefer a different style of writing unit tests, like mocha or qunit, there are plugins
available for those as well.
Reporters
Karma can give us the results of the tests in various forms as well. The default
progress reporter comes built in, but you might decide that you need the test results
as junit.xml files. You can install a Karma plugin for that.
Integrations
One other major category of plugins allows us to integrate with existing JavaScript
libraries or tools, like Google’s Closure or RequireJS. Most of these have plugins for
Karma as well that you can install if you need them.
Explaining the Karma Config
To use Karma, we need a configuration file that tells Karma how to operate. We will see
how easy it is to generate this configuration file in the next section. But first, let’s take a
look at the Karma configuration and the options that we will use for our unit tests in
this chapter. The default name for this file is karma.conf.js, and unless you tell Karma
otherwise, it will automatically look for a file with this name in the directory you run
Karma from:
// File: chapter3/karma.conf.js
// Karma configuration
module.exports = function(config) {
config.set({
// base path that will be used to resolve files and exclude
basePath: '',
// testing framework to use (jasmine/mocha/qunit/...)
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
'angular.min.js',
'angular-mocks.js',
'controller.js',
'simpleSpec.js',
'controllerSpec.js'
],
// list of files / patterns to exclude
exclude: [],
// web server port
Introduction to Karma
www.it-ebooks.info
| 39
port: 8080,
// level of logging
// possible values: LOG_DISABLE || LOG_ERROR ||
//
LOG_WARN || LOG_INFO || LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests
// whenever any file changes
autoWatch: true,
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers: ['Chrome'],
// Continuous Integration mode
// if true, it captures browsers, runs tests, and exits
singleRun: false
});
};
Let’s take a look at each of the options in the preceding example, to see what effect they
have on Karma:
basePath
The base path from which all files for testing and the tests themselves need to be
loaded. This is set relative to the location of the Karma config file.
frameworks
Which frameworks to load, as an array. In our example, we loaded Jasmine (which
requires that the karma-jasmine plugin be installed). You can select mocha, qunit,
or something else as well here.
files
The list of files (or file paths) to load, listed as an array. In the case of AngularJS, we
first load the AngularJS library, and then the angular-mocks.js file, which AngularJS
provides as a helper for testing. Finally, we load the application source code followed
by the actual unit tests.
40
| Chapter 3: Unit Testing in AngularJS
www.it-ebooks.info
exclude
A list of files (or file paths) to exclude. Useful if you are using a lot of glob rules
(wildcard statements to include a set of files, like **.js) for the files, and want to
exclude certain files (like the karma.conf.js).
port
This specifies which port the Karma test runner server runs on. By default, it is 8080.
logLevel
Which levels of log (console.log, console.info) Karma needs to capture from
the browser.
autoWatch
This is by far the coolest and most useful feature of Karma. This tells Karma to keep
a watch on all the files included by the files config, and if any of them change, to
run the affected unit tests. If this is set to true, you don’t need to ever manually
trigger a run of your unit tests; Karma will take care of that for you.
browsers
The browsers Karma should open when it is initially started. Most of these require
a karma-launcher plugin (we installed the chrome-launcher, so we specify Chrome
in this).
singleRun
This is a Boolean value, and tells Karma to shut down the server after one single
run of the unit tests. This should be set to true for continuous integration envi‐
ronments, and can be ignored otherwise.
There are a lot more configurations that you can set and modify with Karma, but we
won’t get into that in this book. You can read about them at the Karma Configuration
File Overview page.
Generating the Karma Config
Now, you can of course copy and paste the contents of the config file from the previous
section to get started, but Karma offers a much nicer way to get started with your own
Karma config. Karma lets you autogenerate the config by running the following
command:
karma init
This triggers an interactive shell, which prompts us with a series of questions. Each
answer usually has a series of options that you can cycle through by using Tab on your
keyboard. After we select all our options, the karma.conf.js file is generated for us.
Introduction to Karma
www.it-ebooks.info
| 41
Jasmine: Spec Style of Testing
In the previous section, we saw the test runner that we will use to run the unit tests we
write for AngularJS. But the actual testing framework that we will use for the purpose
of this book is Jasmine. The Jasmine framework uses what we call a behavior-driven
style of writing tests.
That is, instead of writing a bunch of functions and asserts, we describe behaviors and
set expectations. How does this translate into actual tests? Let’s take a deeper look.
Jasmine Syntax
Before we jump down into Jasmine syntax and talk about the various concepts of a
Jasmine test, let’s take an example to make things clearer:
// A test suite in Jasmine
describe('My Function', function() {
var t;
// Similar to setup
beforeEach(function() {
t = true;
});
afterEach(function() {
t = null;
});
it('should perform action 1', function() {
expect(t).toBeTruthy();
});
it('should perform action 2', function() {
var expectedValue = true;
expect(t).toEqual(expectedValue);
});
});
Let’s go through the example one piece at a time:
describe
The very first line of our test creates a test suite of sorts. Think of it as a container
for multiple unit tests. You would write a describe for a controller, for a service,
and so on. You can also nest describes within a describe, in case you want a
describe for a complex function inside a controller.
beforeEach
A beforeEach is similar to a setup function in the xUnit testing pattern. That is, the
function you pass to beforeEach will be executed before each individual it block.
In this case, the t = true; line will execute once before each of the it blocks in the
42
| Chapter 3: Unit Testing in AngularJS
www.it-ebooks.info
describe. You can also have multiple beforeEach functions inside a describe, and
they will each be executed once in the order in which they are declared.
afterEach
Similar to the beforeEach, the afterEach block gets executed after the individual
it blocks are completed. If you use mocking libraries, this is the best place to check
whether or not any expectations set on the mocks are satisfied.
it
These are the unit tests. Each it block should be self-contained, and independent
of all the other it blocks. Inside the it, you would basically set up your state, execute
the function, check its return value, and ensure that all expectations are met.
expect
These are the Jasmine equivalents of assert statements. Each expect takes a value,
and then you can use one of the built-in matchers (or create your own) to check its
value. In the preceding example, we use the Jasmine matcher toBeTruthy, which
states that the value should match the JavaScript concept of truthy (nonnull, non‐
empty string, nonzero number, or Boolean true). In the second it block, we use
the toEqual matcher, which takes another value and compares it with the first value
for equality.
Useful Jasmine Matchers
Let’s quickly run through some basic built-in Jasmine matchers that we use in the tests.
All of these are used along with expect; that is, expect(value).myMatcherHere:
toEqual
The most basic of Jasmine matchers, the toEqual takes a second value and does a
deep equality check between the two objects. In the case of an object, all the fields
have to match. In the case of an array, all the array elements have to match.
toBe
The toBe matcher checks for reference, and expects both items passed to the expect
and the matcher to be the exact same object reference.
toBeTruthy
Checks the value passed to the matcher to pass the JavaScript concept of true.
Nonnull objects, nonempty strings, nonzero numbers, and the Boolean true all
evaluate to be truthy in JavaScript.
toBeFalsy
Checks the value passed to the matcher to pass the JavaScript concept of false.
Null values, undefined variables, empty strings, zero, and false all evaluate to be
falsy in JavaScript.
Jasmine: Spec Style of Testing
www.it-ebooks.info
| 43
toBeDefined
Ensures the reference passed to the expect is defined (a value is assigned to the
reference, that is).
toBeUndefined
Checks if the reference passed to the expect is undefined or not set.
toBeNull
Checks if the reference passed to the expect is null.
toContain
Checks if the array passed to the expect contains the element passed to the matcher,
toContain.
toMatch
Used for regular expression checks when the first argument to the expect is a string
that needs to match a specific regular expression pattern.
We will be using some or all of these throughout this book. Of course, Jasmine is ex‐
tensible, and allows you to write your own custom matchers as well. You can read all
about it at the Jasmine Matcher page.
Writing a Unit Test for Our Controller
Now, with both Karma (our test runner) and Jasmine (our test framework) in place, let’s
see how we can write tests for the controllers we create in AngularJS. Let’s take a very
simple controller, something similar to what we saw in Chapter 2:
// File: chapter3/controller.js
angular.module('notesApp', [])
.controller('ListCtrl', [function() {
var self = this;
self.items = [
{id: 1, label: 'First', done: true},
{id: 2, label: 'Second', done: false}
];
self.getDoneClass = function(item) {
return {
finished: item.done,
unfinished: !item.done
};
};
}]);
We have a very simplistic controller in the preceding example. All it does is assign an
array to its instance (to make it available to the HTML), and then has a function to figure
44
| Chapter 3: Unit Testing in AngularJS
www.it-ebooks.info
out the presentation logic, which returns the classes to apply based on the item’s done
state. Given this controller, let’s take a look at how a Jasmine spec for this might look:
// File: chapter3/controllerSpec.js
describe('Controller: ListCtrl', function() {
// Instantiate a new version of my module before each test
beforeEach(module('notesApp'));
var ctrl;
// Before each unit test, instantiate a new instance
// of the controller
beforeEach(inject(function($controller) {
ctrl = $controller('ListCtrl');
}));
it('should have items available on load', function() {
expect(ctrl.items).toEqual([
{id: 1, label: 'First', done: true},
{id: 2, label: 'Second', done: false}
]);
});
it('should have highlight items based on state', function() {
var item = {id: 1, label: 'First', done: true};
var actualClass = ctrl.getDoneClass(item);
expect(actualClass.finished).toBeTruthy();
expect(actualClass.unfinished).toBeFalsy();
item.done = false;
actualClass = ctrl.getDoneClass(item);
expect(actualClass.finished).toBeFalsy();
expect(actualClass.unfinished).toBeTruthy();
});
});
In this example, we look at two specific unit tests for the ListCtrl controller. Let’s take
a look at each interesting bit one by one:
Instantiating a module
The very first thing we do as a part of our describe block for the ListCtrl is
instantiate a new instance of the AngularJS module notesApp. This has the effect
of freshly loading all the controllers, services, directives, and filters associated with
that module before each specific unit test. The advantage of this is that the state you
set up and modify in one unit test cannot affect another. Each unit test essentially
becomes independent and contained. This module function is one of the helper
methods that the angular-mocks.js AngularJS library file provides, as well as many
others.
Writing a Unit Test for Our Controller | 45
www.it-ebooks.info
Injecting services
We then have a variable called ctrl, which will hold a controller instance across
each of the unit tests. The beforeEach block after that uses something called in
ject, which basically injects AngularJS services into the functions that befor
eEach and it in Jasmine take. We will look into AngularJS services in Chapter 5,
but just know that there is an AngularJS service called $controller that we can use
to instantiate new instances of our controller. The function passed to inject can
take multiple arguments, each of which is an AngularJS service that AngularJS then
creates and injects into the function.
Creating our controller instance
We use the $controller service to create an instance of our ListCtrl. We do this
by simply passing in the name of the controller as a string to the $controller
service, and get back a new instance of the controller. We then assign this to the
ctrl variable, which we will use in each individual test.
Writing a test for the constructor
Whatever we define in our controller function gets executed when the controller is
instantiated. The only things we do in our controller function is set up the items
array and the function. So the first it block tests the state of the items array to see
if it is instantiated correctly with the right values. It uses the Jasmine toEqual
matcher to check if the items array is exactly the same as what we specify.
Writing a test for the getDoneClass function
The last test we write is to check the getDoneClass function defined on the con‐
troller instance. We do this by instantiating some local state for the test (the item
object), and then pass that to the getDoneClass function on the controller and store
its return value. Next, we check for the truthiness and falsiness of the two classes
on it, finished and unfinished. We then change the state of item from done to not
done (by setting it to false), and then check if the function changes its return value
correspondingly.
The order of execution in the previous example is as follows:
1. The beforeEach that loads the AngularJS module executes.
2. The beforeEach that creates our controller executes.
3. The it for the “items on load” executes.
4. The beforeEach that loads the AngularJS module executes.
5. The beforeEach that creates our controller executes.
6. The it for the getDoneClass executes.
46
| Chapter 3: Unit Testing in AngularJS
www.it-ebooks.info
Thus each unit test gets a clean slate when it executes, and ensures that modifications
and state changes in one unit test don’t affect the other.
Running the Unit Test
How do we actually run these unit tests? That is the simplest part—just execute the
following command from the command line/terminal/console:
karma start
The command automatically looks for the karma.conf.js file in the directory you are
executing the command from, and picks up the config from it. In case your config file
is not named karma.conf.js, or if it is in a different folder, you can optionally pass it in
as an argument to the command. That is:
karma start my.conf.js
This is not needed in our example.
Both of these will read the config file, grab the files that are under test, start a server on
the port specified, and then try to open the browsers listed in the config (provided the
launcher plugins for them are installed).
The examples and tests in this book were run using version 0.12.16
of Karma, and version 1.2.19 of AngularJS (both the angular.js and
angular-mocks.js files). If you are having trouble running them for
any reason, make sure that you are using the same version.
The karma command starts a test server that is used for serving the test files. Each
browser that you open and connect to the server gets a green bar at the top to signal that
it is ready to run the tests. If you have not selected any browsers in the configuration,
or there are some problems capturing the browsers automatically, you can still capture
any browser manually (on any machine) by opening the URL specified in the browser.
The results of the test are printed in the console/command window from which the
karma start command was executed. The captured browsers themselves do not print
the results. This is because the browsers that are executing the test might be on a different
machine altogether. We should see something like Figure 3-1 in the console/command
window after a successful run.
Running the Unit Test | 47
www.it-ebooks.info
Figure 3-1. Test results for a Karma run
Now if you have autoWatch set to true in your configuration, Karma will automatically
trigger a test run in all the browsers you have captured every time any of the files under
test change. If you don’t have autoWatch set to true, you can manually trigger a test run
by executing the following command from the console/terminal:
karma run
This tells Karma to execute tests for the currently active server configuration, again
based on the configuration.
Conclusion
We generated a Karma configuration, and then wrote our very first AngularJS unit test.
In our unit test, we instantiated an instance of the controller using the $controller
service, and leveraged Jasmine to set expectations on our controller.
One thing to note and remember is that Dependency Injection, which is heavily used
throughout AngularJS, is something we leveraged in our unit tests as well. We did not
need to worry about what the controller is, what it depends on, or how to instantiate it.
AngularJS took care of that for us. This would be something that would continue for all
the other unit tests we will write for the rest of the book.
In the next chapter, we will see how to do two-way data-binding and work with forms
in AngularJS. We will also cover form validation and error handling with forms.
48
| Chapter 3: Unit Testing in AngularJS
www.it-ebooks.info
CHAPTER 4
Forms, Inputs, and Services
In the previous chapters, we first covered the most basic AngularJS directives and dealt
with creating controllers and getting our data from the controllers into the UI. We then
looked at how to write tests for the same, using Karma and Jasmine. In this chapter, we
will build on the work from Chapter 2 and work on getting the user’s data out of forms
in the UI into our controller so that we can then send it to the server, validate it, or do
whatever else we might need to.
We will then get into using AngularJS services, and see how we can leverage some of
the common existing services as well as create our own. We will also briefly cover when
and why you should create AngularJS services.
Working with ng-model
In the previous chapter, we saw the ng-bind directive, or its equivalent double-curly
{{ }} notation, which allowed us to take the data from our controllers and display it in
the UI. That gives us our one-way data-binding, which is powerful in its own regard.
But most applications we develop also have user interaction, and parts where the user
has to feed in data. From registration forms to profile information, forms are a staple
of web applications, and AngularJS provides the ng-model directive for us to deal with
inputs and two-way data-binding:
<!-- File: chapter4/simple-ng-model.html -->
<html ng-app="notesApp">
<head><title>Notes App</title></head>
<body ng-controller="MainCtrl as ctrl">
<input type="text" ng-model="ctrl.username"/>
You typed {{ctrl.username}}
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
49
www.it-ebooks.info
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
this.username = 'nothing';
}]);
</script>
</body>
</html>
In this example, we define a controller with a variable exposed on its instance called
username. Now, we get its value out into the HTML using the ng-controller and the
double-curly syntax for one-way data-binding. What we have introduced in addition is
an input element. It is a plain text box, but on it we have attached the ng-model directive.
We pointed the value for the ng-model at the same username variable on the
MainCtrl. This accomplishes the following things:
• When the HTML is instantiated and the controller is attached, it gets the current
value (in this case, nothing as a string) and displays it in our UI.
• When the user types, updates, or changes the value in the input box, it updates the
model in our controller.
• When the value of the variable changes in the controller (whether because it came
from the server, or due to some internal state change), the input field gets the value
updated automatically.
The beauty of this is twofold:
• If we need to update the form element in the UI, all we need to do is update the
value in the controller. No need to go looking for input fields by IDs or CSS class
selectors; just update the model.
• If we need to get the latest value that the user entered into the form or input to
validate or send to the server, we just need to grab it from our controller. It will have
the latest value in it.
Now let’s add some complexity, and actually deal with forms. Let’s see if we can bring
this concept together with an example:
<!-- File: chapter4/simple-ng-model-2.html -->
<html ng-app="notesApp">
<head><title>Notes App</title></head>
<body ng-controller="MainCtrl as ctrl">
<input type="text" ng-model="ctrl.username">
<input type="password" ng-model="ctrl.password">
<button ng-click="ctrl.change()">Change Values</button>
<button ng-click="ctrl.submit()">Submit</button>
50
| Chapter 4: Forms, Inputs, and Services
www.it-ebooks.info
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.change = function() {
self.username = 'changed';
self.password = 'password';
};
self.submit = function() {
console.log('User clicked submit with ',
self.username, self.password);
};
}]);
</script>
</body>
</html>
We introduced one more input field, which is bound to a field called password on the
controller’s instance. And we added two buttons:
• The first button, Change Values, is to simulate the server sending some data that
needs to be updated in the UI. All it does is reassign the values to the username and
password fields in the controller with the latest values.
• The second button, Submit, simulates submitting the form to the server. All it does
for now is log the value to the console.
The most important thing in both of these is that the controller never reached out into
the UI. There was no jQuery selector, no findElementById, or anything like that. When
we need to update the UI, we just update the model fields in the controller. When we
need to get the latest and greatest value, we just grab it from the controller. Again, this
is the AngularJS way.
Let’s now build on this, and see how we can integrate and work with forms in AngularJS.
Working with Forms
When we work with forms in AngularJS, we heavily leverage the ng-model directive to
get our data into and out of the form. In addition to the data-binding, it is also recom‐
mended to structure your model and bindings in such a way to reduce your own effort,
as well as the lines of code you write. Let’s take a look at an example:
<!-- File: chapter4/simple-form.html -->
<html ng-app="notesApp">
<head><title>Notes App</title></head>
<body ng-controller="MainCtrl as ctrl">
Working with Forms | 51
www.it-ebooks.info
<form ng-submit="ctrl.submit()">
<input type="text" ng-model="ctrl.user.username">
<input type="password" ng-model="ctrl.user.password">
<input type="submit" value="Submit">
</form>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.submit = function() {
console.log('User clicked submit with ', self.user);
};
}]);
</script>
</body>
</html>
We are still using the same two input fields as last time, but we made a few changes:
• We wrapped our text fields and button inside a form. And instead of an ng-click
on the button, we added an ng-submit directive on the form itself. The ng-submit
directive has a few advantages over having an ng-click on a button when it comes
to forms. A form submit event can be triggered in multiple ways: clicking the Submit
button, or hitting Enter on a text field. The ng-submit gets triggered on all those
events, whereas the ng-click will only be triggered when the user clicks the button.
• Instead of binding to ctrl.username and ctrl.password, we bind to
ctrl.user.username and ctrl.user.password. Notice that we did not declare a
user object in the controller (that is, self.user = {}). When you use ng-model,
AngularJS automatically creates the objects and keys necessary in the chain to in‐
stantiate a data-binding connection. In this case, until the user types something into
the username or password field, there is no user object. The first letter typed into
either the username or password field causes the user object to be created, and the
value to be assigned to the correct field in it.
Leverage Data-Binding and Models
When designing your forms and deciding which fields to bind the ng-model to, you
should always consider what format you need the data in. Let’s take the following ex‐
ample to demonstrate:
52
| Chapter 4: Forms, Inputs, and Services
www.it-ebooks.info
<!-- File: chapter4/two-forms-databinding.html -->
<html ng-app="notesApp">
<head><title>Notes App</title></head>
<body ng-controller="MainCtrl as ctrl">
<form ng-submit="ctrl.submit1()">
<input type="text" ng-model="ctrl.username">
<input type="password" ng-model="ctrl.password">
<input type="submit" value="Submit">
</form>
<form ng-submit="ctrl.submit2()">
<input type="text" ng-model="ctrl.user.username">
<input type="password" ng-model="ctrl.user.password">
<input type="submit" value="Submit">
</form>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.submit1 = function() {
// Create user object to send to the server
var user = {username: self.username, password: self.password};
console.log('First form submit with ', user);
};
self.submit2 = function() {
console.log('Second form submit with ', self.user);
};
}]);
</script>
</body>
</html>
There are two forms in this example, both with the same fields. The first form is bound
to a username and password directly on the controller, while the second form is bound
to a username and password key on a user object in the controller. Both of them trigger
an ng-submit function on submission of a function. Now in the case of the first form,
we have to take those fields from the controller and put them into an object, or something
similar, before we can send it to the server. In the second case, we can directly take the
user object from the controller and pass it around.
The second flow makes more sense, because we are directly modeling how we want to
represent the form as an object in the controller. This removes any additional work we
might have to do when we work with the values of the form.
Leverage Data-Binding and Models | 53
www.it-ebooks.info
Form Validation and States
We have seen how to create forms, and enable (and leverage) data-binding to get our
data in and out of the UI. Now let’s proceed to see how else AngularJS can benefit us
when working with forms, and especially with validation and various states of the forms
and inputs:
<!-- File: chapter4/form-validation.html -->
<html ng-app="notesApp">
<head><title>Notes App</title></head>
<body ng-controller="MainCtrl as ctrl">
<form ng-submit="ctrl.submit()" name="myForm">
<input type="text"
ng-model="ctrl.user.username"
required
ng-minlength="4">
<input type="password"
ng-model="ctrl.user.password"
required>
<input type="submit"
value="Submit"
ng-disabled="myForm.$invalid">
</form>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.submit = function() {
console.log('User clicked submit with ', self.user);
};
}]);
</script>
</body>
</html>
In this example, we reworked our old example to add some validation. In particular, we
want to disable the Submit button if the user has not filled out all the required fields.
How do we accomplish this?
1. We give the form a name, which we can refer to later. In this case, it is myForm.
2. We leverage HTML5 validation tags and add the required attribute on each input
field.
3. We add a validator, ng-minlength, which enforces that the minimum length of the
value in the input field for the username is four characters.
54
| Chapter 4: Forms, Inputs, and Services
www.it-ebooks.info
4. On the Submit button, we add an ng-disabled directive. This disables the element
if the condition is true.
5. For the disable condition, we leverage the form, which exposes a controller with
the current state of the form. In this case, we tell the button to disable itself if the
form with the name myForm is $invalid.
When you use forms (and give them names), AngularJS creates a FormController that
holds the current state of the form as well as some helper methods. You can access the
FormController for a form using the form’s name, as we did in the preceding example
using myForm. Things that are exposed as the state and kept up to date with data-binding
are shown in Table 4-1.
Table 4-1. Form states in AngularJS
Form state
Description
$invalid
AngularJS sets this state when any of the validations (required, ng-minlength, and others) mark any of
the fields within the form as invalid.
$valid
The inverse of the previous state, which states that all the validations in the form are currently evaluating to
correct.
$pristine All forms in AngularJS start with this state. This allows you to figure out if a user has started typing in and
modifying any of the form elements. Possible usage: disabling the reset button if a form is pristine.
$dirty
The inverse of $pristine, which states that the user made some changes (he can revert it, but the $dirty
bit is set).
$error
This field on the form houses all the individual fields and the errors on each form element. We will talk more
about this in the following section.
Each of the states mentioned in the table (except $error) are Booleans and can be used
to conditionally hide, show, disable, or enable HTML elements in the UI. As the user
types or modifies the form, the values are updated as long as you are leveraging ngmodel and the form name.
Error Handling with Forms
We looked at the types of validation you can do at a form level, but what about individual
fields? In our previous example, we ensured that both input fields were required fields,
and that the minimum length on the username was four. What else can we do? Table 4-2
contains some built-in validations that AngularJS offers.
Error Handling with Forms | 55
www.it-ebooks.info
Table 4-2. Built-in AngularJS validators
Validator
Description
required
As previously discussed, this ensures that the field is required, and the field is marked invalid until it is
filled out.
ng-required
Unlike required, which marks a field as always required, the ng-required directive allows us to
conditionally mark an input field as required based on a Boolean condition in the controller.
ng-minlength
We can set the minimum length of the value in the input field with this directive.
ng-maxlength
We can set the maximum length of the value in the input field with this directive.
ng-pattern
The validity of an input field can be checked against the regular expression pattern specified as part of this
directive.
type="email"
Text input with built-in email validation.
type="number" Text input with number validation. Can also have additional attributes for min and max values of the
number itself.
type="date"
If the browser supports it, shows an HTML datepicker. Otherwise, defaults to a text input. The ngmodel that this binds to will be a date object. This expects the date to be in yyyy-mm-dd format (e.g.,
2009-10-24).
type="url"
Text input with URL validation.
In addition to this, we can write our own validators, which we cover in Chapter 13.
Displaying Error Messages
What can we do with all these validators? We can of course check the validity of the
form, and disable the Save or Update button accordingly. But we also want to tell the
user what went wrong and how to fix it. AngularJS offers two things to solve this
problem:
• A model that reflects what exactly is wrong in the form, which we can use to display
nicer error messages
• CSS classes automatically added and removed from each of these fields allow us to
highlight problems in the form
Let’s first take a look at how to display specific error messages based on the problem
with the following example:
<!-- File: chapter4/form-error-messages.html -->
<html ng-app="notesApp">
<head><title>Notes App</title></head>
<body ng-controller="MainCtrl as ctrl">
<form ng-submit="ctrl.submit()" name="myForm">
<input type="text"
name="uname"
ng-model="ctrl.user.username"
56
| Chapter 4: Forms, Inputs, and Services
www.it-ebooks.info
required
ng-minlength="4">
<span ng-show="myForm.uname.$error.required">
This is a required field
</span>
<span ng-show="myForm.uname.$error.minlength">
Minimum length required is 4
</span>
<span ng-show="myForm.uname.$invalid">
This field is invalid
</span>
<input type="password"
name="pwd"
ng-model="ctrl.user.password"
required>
<span ng-show="myForm.pwd.$error.required">
This is a required field
</span>
<input type="submit"
value="Submit"
ng-disabled="myForm.$invalid">
</form>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function () {
var self = this;
self.submit = function () {
console.log('User clicked submit with ', self.user);
};
}]);
</script>
</body>
</html>
Nothing in the controller has changed in this example. Instead, we can just focus on the
form HTML. Let’s see what changed with the form:
1. First, we added the name attribute to both the input fields where we needed valida‐
tion: uname for the username box, and pwd for the password text field.
2. Then we leverage AngularJS’s form bindings to be able to pick out the errors for
each individual field. When we add a name to any input, it creates a model on the
form for that particular input, with the error state.
3. So for the username field, we can access it if the field was not entered by accessing
myForm.uname.$error.required. Similarly, for ng-minlength, the field would be
Error Handling with Forms | 57
www.it-ebooks.info
myForm.uname.$error.minlength. For the password, we look at myForm.pwd.$er
ror.required to see if the field was filled out or not.
4. We also accessed the state of the input, similar to the form, by accessing myForm.un
ame.$invalid. All the other form states ($valid, $pristine, $dirty) we saw earlier
are also available similarly on myForm.uname.
With this, we now have an error message that shows only when a certain type of error
is triggered. Each of the validators we saw in Table 4-2 exposes a key on the $error
object, so that we can pick it up and display the error message for that particular error
to the user. Need to show the user that a field is required? Then when the user starts
typing, show the minimum length, and then finally show a message when he exceeds
the maximum length. All these kinds of conditional messages can be shown with the
AngularJS validators.
Styling Forms and States
We saw the various states of the forms (and the inputs): $dirty, $valid, and so on. We
saw how to display specific error messages and disable buttons based on these condi‐
tions, but what if we want to highlight certain input fields or form states using UI and
CSS? One option would be to use the form and input states along with the ng-class
directive to, say, add a class dirty when myForm.$dirty is true. But AngularJS provides
an easier option.
For each of the states we described previously, AngularJS adds and removes the CSS
classes shown in Table 4-3 to and from the forms and input elements.
Table 4-3. Form state CSS classes
Form state
CSS class applied
$invalid
ng-invalid
$valid
ng-valid
$pristine ng-pristine
$dirty
ng-dirty
Similarly, for each of the validators that we add on the input fields, we also get a CSS
class in a similarly named fashion, as demonstrated in Table 4-4.
Table 4-4. Input state CSS classes
Input state
CSS class applied
$invalid
ng-invalid
$valid
ng-valid
$pristine ng-pristine
$dirty
58
ng-dirty
| Chapter 4: Forms, Inputs, and Services
www.it-ebooks.info
Input state
CSS class applied
required
ng-valid-required or ng-invalid-required
min
ng-valid-min or ng-invalid-min
max
ng-valid-max or ng-invalid-max
minlength ng-valid-minlength or ng-invalid-minlength
maxlength ng-valid-maxlength or ng-invalid-maxlength
pattern
ng-valid-pattern or ng-invalid-pattern
url
ng-valid-url or ng-invalid-url
email
ng-valid-email or ng-invalid-email
date
ng-valid-date or ng-invalid-date
number
ng-valid-number or ng-invalid-number
Other than the basic input states, AngularJS takes the name of the validator (number,
maxlength, pattern, etc.) and depending on whether or not that particular validator has
been satisfied, adds the ng-valid-validator_name or ng-invalid-validator_name
class, respectively.
Let’s take an example of how this might be used to highlight the input in different ways:
<!-- File: chapter4/form-styling.html -->
<html ng-app="notesApp">
<head>
<title>Notes App</title>
<style>
.username.ng-valid {
background-color: green;
}
.username.ng-dirty.ng-invalid-required {
background-color: red;
}
.username.ng-dirty.ng-invalid-minlength {
background-color: lightpink;
}
</style>
</head>
<body ng-controller="MainCtrl as ctrl">
<form ng-submit="ctrl.submit()" name="myForm">
<input type="text"
class="username"
name="uname"
ng-model="ctrl.user.username"
required
ng-minlength="4">
<input type="submit"
value="Submit"
ng-disabled="myForm.$invalid">
Error Handling with Forms | 59
www.it-ebooks.info
</form>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.submit = function() {
console.log('User clicked submit with ', self.user);
};
}]);
</script>
</body>
</html>
In this example, we kept the existing functionality of the validators, though we removed
the specific error messages. Instead, what we try to do is mark out the required field
using CSS classes. So here is what the example accomplishes:
• When the field is correctly filled out, it turns the input box green. This is done by
setting the background color when the CSS class ng-valid is applied to our input
field.
• We want to display the background as dark red if the user starts typing in, and then
undoes it. That is, we want to set the background as red, marking it as a required
field, but only after the user modifies the field. So we set the background color to
be red if the CSS classes ng-dirty (which marks that the user has modified it) and
ng-invalid-minlength (which marks that the user has not typed in the necessary
amount of characters) are applied.
Similarly, you could add a CSS class that shows a * mark in red if the field is required
but not dirty. Using a combination of these CSS classes (and the form and input states)
from before, you can easily style and display all the relevant and actionable things to
the user about your form.
Nested Forms with ng-form
By this point, we know how to create forms and get data into and out of our controllers
(by binding it to a model). We have also seen how to perform simple validation, and
style and display conditional error messages in AngularJS.
The next part that we want to cover is how to deal with more complicated form struc‐
tures, and grouping of elements. We sometimes run into cases where we need subsec‐
tions of our form to be valid as a group, and to check and ascertain its validity. This is
not possible with the HTML form tag because form tags are not meant to be nested.
60
| Chapter 4: Forms, Inputs, and Services
www.it-ebooks.info
AngularJS provides an ng-form directive, which acts similar to form but allows nesting,
so that we can accomplish the requirement of grouping related form fields under
sections:
<!-- File: chapter4/nested-forms.html -->
<html ng-app>
<head>
<title>Notes App</title>
</head>
<body>
<form novalidate name="myForm">
<input type="text"
class="username"
name="uname"
ng-model="ctrl.user.username"
required=""
placeholder="Username"
ng-minlength="4" />
<input type="password"
class="password"
name="pwd"
ng-model="ctrl.user.password"
placeholder="Password"
required="" />
<ng-form name="profile">
<input type="text"
name="firstName"
ng-model="ctrl.user.profile.firstName"
placeholder="First Name"
required>
<input type="text"
name="middleName"
placeholder="Middle Name"
ng-model="ctrl.user.profile.middleName">
<input type="text"
name="lastName"
placeholder="Last Name"
ng-model="ctrl.user.profile.lastName"
required>
<input type="date"
name="dob"
placeholder="Date Of Birth"
ng-model="ctrl.user.profile.dob">
</ng-form>
<span ng-show="myForm.profile.$invalid">
Please fill out the profile information
</span>
Nested Forms with ng-form | 61
www.it-ebooks.info
<input type="submit"
value="Submit"
ng-disabled="myForm.$invalid"/>
</form>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
</body>
</html>
In this example, we nest a subform inside our main form, but because the HTML form
element cannot be nested, we use the ng-form directive to do it. Now we can have
substate within our form, evaluate quickly if each section is valid, and leverage the same
binding and form states that we have looked at so far. A quick highlight of the features
in the example:
• A subform using the ng-form directive. We can give this a name to identify and
grab the state of the subform.
• The state of the subform can be accessed directly (profile.$invalid) or through
the parent form (myForm.profile.$invalid).
• Individual elements of the form can be accessed as normal (profile.firstName.
$error.required).
• Subforms and nested forms still affect the outer form (the myForm.$invalid is true
because of the use of the required tags).
You could have subforms and groupings that have their own way of checking and de‐
ciding validity, and ng-form allows you to model that grouping in your HTML.
Other Form Controls
We have dealt with forms, ng-models, and bindings, but mostly we have only looked at
regular text boxes. Let’s see how to interact and work with other form elements in
AngularJS.
Textareas
Textareas in AngularJS work exactly the same as text inputs. That is, to have two-way
data-binding with a textarea, and make it a required field, you would do something like:
<textarea ng-model="ctrl.user.address" required></textarea>
All the data-binding, error states, and CSS classes remain as we saw it with text inputs.
62
| Chapter 4: Forms, Inputs, and Services
www.it-ebooks.info
Checkboxes
Checkboxes are in some ways easier to deal with because they can only have one of two
values: true or false. So an ng-model two-way data-binding to the checkbox basically
takes a Boolean value and assigns the checked state based on it. After that, any changes
to the checkbox toggles the state of the model:
<input type="checkbox" ng-model="ctrl.user.agree">
But what if we didn’t have just Boolean values? What if we wanted to assign the string
YES or NO to our model, or have the checkbox checked when the value is YES? Angu‐
larJS gives two attribute arguments to the checkbox that allow us to specify our custom
values for the true and false values. We could accomplish this as follows:
<input type="checkbox"
ng-model="ctrl.user.agree"
ng-true-value="YES"
ng-false-value="NO">
This sets the value of the agree field to YES if the user checks the checkbox, and NO if
the user unchecks it.
But what if we didn’t want the two-way data-binding, and just want to use the checkbox
to display the current value of a Boolean? That is, one-way data-binding where the state
of the checkbox changes when the value behind it changes, but the value doesn’t change
on checking or unchecking the checkbox.
We can accomplish this using the ng-checked directive, which binds to an AngularJS
expression. Whenever the value is true, AngularJS will set the checked property for the
input element, and remove and unset it when the value is false. Let’s use the following
example to demonstrate all these together:
<!-- File: chapter4/checkbox-example.html -->
<html ng-app="notesApp">
<head><title>Notes App</title></head>
<body ng-controller="MainCtrl as ctrl">
<div>
<h2>What are your favorite sports?</h2>
<div ng-repeat="sport in ctrl.sports">
<label ng-bind="sport.label"></label>
<div>
With Binding:
<input type="checkbox"
ng-model="sport.selected"
ng-true-value="YES"
ng-false-value="NO">
</div>
<div>
Using ng-checked:
<input type="checkbox"
ng-checked="sport.selected === 'YES'">
Other Form Controls
www.it-ebooks.info
| 63
</div>
<div>
Current state: {{sport.selected}}
</div>
</div>
</div>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.sports = [
{label: 'Basketball', selected: 'YES'},
{label: 'Cricket', selected: 'NO'},
{label: 'Soccer', selected: 'NO'},
{label: 'Swimming', selected: 'YES'}
];
}]);
</script>
</body>
</html>
With this example, we have an ng-repeat, which has a checkbox with ng-model, a
checkbox with ng-checked, and a div with the current state bound to it. The first check‐
box uses the traditional two-way data-binding with the ng-model directive. The second
checkbox uses ng-checked. This means that:
• When the user checks the first checkbox, the value of selected becomes YES be‐
cause the true value, set using ng-true-value, is YES. This triggers the ng-checked
and sets the second box as checked (or unchecked).
• When the user unchecks the first box, the value of selected is set to NO because of
the ng-false-value.
• The second checkbox in each repeater element displays the state of the ng-model
using ng-checked. This updates the state of the checkbox whenever the model
backing ng-model changes. Checking or unchecking the second checkbox itself has
no effect on the value of the model.
So if you need two-way data-binding, use ng-model. If you need one-way data-binding
with checkboxes, use ng-checked.
Radio Buttons
Radio buttons behave similarly to checkboxes, but are slightly different. You can have
multiple radio buttons (and you normally do) that each assigns a different value to a
64
| Chapter 4: Forms, Inputs, and Services
www.it-ebooks.info
model depending on which one is selected. You can specify the value using the tradi‐
tional value attribute of the input element. Let’s see how that would look:
<div ng-init="user = {gender: 'female'}">
<input type="radio"
name="gender"
ng-model="user.gender"
value="male">
<input type="radio"
name="gender"
ng-model="user.gender"
value="female">
</div>
In this example, we have two radio buttons. We gave them both the same name so that
when one is selected, the other gets deselected. Both of them are bound to the same
ng-model (user.gender). Next, each of them has a value, which is the value that gets
stored in user.gender (male if it is the first radio button; female, otherwise). Finally,
we have an ng-init block surrounding it, which sets the value of user.gender to be
female by default. This has the effect of ensuring that the second checkbox is selected
when this snippet of HTML loads.
But what if our values are dynamic? What if the value we needed to assign was decided
in our controller, or some other place? For that, AngularJS gives you the ng-value
attribute, which you can use along with the radio buttons. ng-value takes an AngularJS
expression, and the return value of the expression becomes the value that is assigned to
the model:
<div ng-init="otherGender = 'other'">
<input type="radio"
name="gender"
ng-model="user.gender"
value="male">Male
<input type="radio"
name="gender"
ng-model="user.gender"
value="female">Female
<input type="radio"
name="gender"
ng-model="user.gender"
ng-value="otherGender">{{otherGender}}
</div>
In this example, the third option box takes a dynamic value. In this case, we assign it as
part of the initialization block (ng-init), but in a real application, the initialization
could be done from within a controller instead of in the HTML directly. When we say
ng-value="otherGender", it doesn’t assign otherGender as a string to user.gender,
but the value of the otherGender variable, which is other.
Other Form Controls
www.it-ebooks.info
| 65
Combo Boxes/Drop-Downs
The final HTML form element (which can be used outside forms as well) is the select
box, or the drop-down/combo box as it is commonly known. Let’s take a look at the
simplest way you can use select boxes in AngularJS:
<div ng-init="location = 'India'">
<select ng-model="location">
<option value="USA">USA</option>
<option value="India">India</option>
<option value="Other">None of the above</option>
</select>
</div>
In this example, we have a simple select box that is data-bound to the variable
location. We also initialize the value of location to India, so when the HTML loads,
India is the selected option. When the user selects any of the other options, the value of
the value attribute gets assigned to the ng-model. The standard validators and states also
apply to this field, so those can be applied (required, etc.).
This has a few restrictions, though:
• You need to know the values in the drop-down up front.
• They need to be hardcoded.
• The values can only be strings.
In a truly dynamic app, one or none of these might be true. In such a case, the select
HTML element also has a way of dynamically generating the list of options, and working
with objects instead of pure string ng-models. This is done through the ng-options
directive. Let’s take a look at how this might work:
<!-- File: chapter4/select-example.html -->
<html ng-app="notesApp">
<head><title>Notes App</title></head>
<body ng-controller="MainCtrl as ctrl">
<div>
<select ng-model="ctrl.selectedCountryId"
ng-options="c.id as c.label for c in ctrl.countries">
</select>
Selected Country ID : {{ctrl.selectedCountryId}}
</div>
<div>
<select ng-model="ctrl.selectedCountry"
ng-options="c.label for c in ctrl.countries">
</select>
Selected Country : {{ctrl.selectedCountry}}
66
| Chapter 4: Forms, Inputs, and Services
www.it-ebooks.info
</div>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
this.countries = [
{label: 'USA', id: 1},
{label: 'India', id: 2},
{label: 'Other', id: 3}
];
this.selectedCountryId = 2;
this.selectedCountry = this.countries[1];
}]);
</script>
</body>
</html>
In this example, we have two select boxes, both bound to different models in our con‐
troller. The first select element is bound to ctrl.selectedCountryId and the second
one is bound to ctrl.selectedCountry. Note that one is a number, while the other is
an actual object. How do we achieve this?
• We use the ng-options attribute on the select dialog, which allows us to repeat an
array (or object, similar to ng-repeat from “Working with and Displaying Ar‐
rays” on page 22) and display dynamic options.
• The syntax is similar to ng-repeat as well, with some additional ability to select
what is displayed as the label, and what is bound to the model.
• In the first select box, we have ng-options="c.id as c.label for c in
ctrl.countries". This tells AngularJS to create one option for each country in the
array of countries. The syntax is as follows: modelValue as labelValue for item
in array. In this case, we tell AngularJS that our modelValue is the ID of each
element, the label value is the label key of each array item, and then our typical for
each loop.
• In the second select box, we have ng-options="c.label for c in ctrl.coun
tries". Here, when we omit the modelValue, AngularJS assumes that each item in
the repeat is the actual model value, so when we select an item from the second
select box, the country object (c) of that option box gets assigned to ctrl.selec
tedCountry.
• Because the backing model for the two select boxes are different, changing one does
not affect the value or the selection in the other drop-down.
Other Form Controls
www.it-ebooks.info
| 67
• You can also optionally give a grouping clause, for which the syntax would be ngoptions="modelValue as labelValue group by groupValue for item in ar
ray". Similar to how we specified the model and label values, we can point the
groupValue at another key in the object (say, continent).
• When you use objects, the clause changes as follows: modelValue as labelValue
group by groupValue for (key, value) in object.
AngularJS compares the ng-options individual values with the ngmodel by reference. Thus, even if the two are objects that have the
same keys and values, AngularJS will not show that item as selected
in the drop-down unless and until they are the same object. We ac‐
complished this in our example by using an item from the array
countries to assign the initial value of the model.
There is a better way to accomplish this, which is through the use of
the track by syntax with ng-options. We could have written the ngoptions as:
ng-options="c.label for c in ctrl.countries track by c.id"
This would ensure that the object c is compared using the ID field,
instead of by reference, which is the default.
Conclusion
We started with the most common requirements, which is getting data in and out of UI
forms. We played around with ng-model, which gives us two-way data-binding to re‐
move most of the boilerplate code we would write when working with forms. We then
saw how we could leverage form validation, and show and style error messages. Finally,
we saw how to deal with other types of form elements, and the kinds of options Angu‐
larJS gives to work with them.
In the next chapter, we start dealing with AngularJS services and then jump into server
communication using the $http service in AngularJS.
68
| Chapter 4: Forms, Inputs, and Services
www.it-ebooks.info
CHAPTER 5
All About AngularJS Services
Until now, we have dealt with data-binding in AngularJS. We have seen how to take data
from our controllers and get it into the UI, and ensure that whenever the user interacts
with or types in any data, we get it back into our controllers. We used and worked with
some common directives, and dealt with forms and error handling.
In this chapter, we dive into AngularJS services. By the end of the chapter, we will have
a thorough understanding of AngularJS services and get some hands-on experience in
using core built-in AngularJS services. After that, we will learn why and when we should
create AngularJS services, and actually create a simple service ourselves.
AngularJS Services
AngularJS services are functions or objects that can hold behavior or state across our
application. Each AngularJS service is instantiated only once, so each part of our ap‐
plication gets access to the same instance of the AngularJS service. Repeated behavior,
shared state, caches, factories, etc. are all functionality that can be implemented using
AngularJS services.
Service Versus Service
In AngularJS, when we say service, we are actually referring to the
conceptual service that is a reusable API or substitutable objects,
which can be shared across our applications. A service in AngularJS
can be implemented as a factory, service, or provider.
This is one of the badly named concepts in AngularJS and thus can
lead to confusion. We end up calling all of the above services. We will
see the difference between them in a bit.
Let’s first take a look at why we need them.
69
www.it-ebooks.info
Why Do We Need AngularJS Services?
So far we have only created AngularJS controllers, which create state and functions that
our HTML then uses for a variety of tasks. AngularJS controllers are great for tasks that
relate to the following:
• Which model and data fields to fetch and show in the HTML
• User interaction, as in what needs to happen when a user clicks something
• Presentation logic, such as how a particular UI element should be styled, or whether
it should be hidden
Controllers are stateful, but ephemeral. That is, they can be destroyed and re-created
multiple times throughout the course of navigating across a Single Page Application.
Let’s take a look at an example to clarify this:
<!-- File: chapter5/need-for-service/index.html -->
<html ng-app="notesApp">
<head>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl as mainCtrl">
<h1>Hello Controllers!</h1>
<button ng-click="mainCtrl.open('first')">Open First</button>
<button ng-click="mainCtrl.open('second')">Open Second</button>
<div ng-switch on="mainCtrl.tab">
<div ng-switch-when="first">
<div ng-controller="SubCtrl as ctrl">
<h3>First tab</h3>
<ul>
<li ng-repeat="item in ctrl.list">
<span ng-bind="item.label"></span>
</li>
</ul>
<button ng-click="ctrl.add()">Add More Items</button>
</div>
</div>
<div ng-switch-when="second">
<div ng-controller="SubCtrl as ctrl">
<h3>Second tab</h3>
<ul>
<li ng-repeat="item in ctrl.list">
<span ng-bind="item.label"></span>
70
| Chapter 5: All About AngularJS Services
www.it-ebooks.info
</li>
</ul>
<button ng-click="ctrl.add()">Add More Items</button>
</div>
</div>
</div>
</body>
</html>
// File: chapter5/need-for-service/app.js
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.tab = 'first';
self.open = function(tab) {
self.tab = tab;
};
}])
.controller('SubCtrl', [function() {
var self = this;
self.list = [
{id: 1, label: 'Item 0'},
{id: 2, label: 'Item 1'}
];
self.add = function() {
self.list.push({
id: self.list.length + 1,
label: 'Item ' + self.list.length
});
};
}]);
In this example, we introduced two controllers for the first time: a MainCtrl and a
SubCtrl. The MainCtrl controls the overall page, and the SubCtrl controls a subsection
of the page and holds the data we want to display.
We also have two tabs, which are shown and hidden depending on which button the
user clicks. This is accomplished using a new directive, ng-switch. ng-switch acts like
a switch statement in the HTML. It takes a variable (using the on attribute, which in
this case is MainCtrl’s tab), and then, depending on the state, hides and shows elements
(using the ng-switch-when attribute, used as children of the ng-switch). The ngswitch-when takes the value that the variable should take.
Finally, the SubCtrl has a function to add more items to the array, which is triggered
by a button in the UI.
Now, moving on to our HTML, the body is controlled by the MainCtrl, and holds state
on which tab in the HTML is shown. We then have two buttons that allow us to change
AngularJS Services
www.it-ebooks.info
| 71
which tab is currently shown. Notice again that this is done by changing the model and
letting AngularJS update the UI automatically.
Finally, we have a div element on which we have the ng-switch. Both tabs (each one
has the ng-switch-when) are exactly the same except for the header. They show the list
of items, and add items when the button in that tab is clicked.
With this out of the way, let’s take a look at some key behaviors:
• Both of the tabs, First and Second, are using the same controller, SubCtrl. But each
one has its own instance of the list variable. Adding items in one tab does not add
them to the other, and vice versa.
• If we add items to the first tab and then switch to the second tab, we will see the
items the controller starts with. But then if we navigate back to the first tab, we will
see that those items disappear from the first controller as well.
We could still achieve the functionality we were aiming for by hav‐
ing a parent-level controller, and moving our list variable into the
parent controller (such as MainCtrl). Each SubCtrl would then have
to access the variable through the top controller explicitly. This sol‐
ves our problem but adds global, implicit state, which is never ideal
for a large, maintainable application.
When we use controllers, they are instances that get created and destroyed as we navigate
across our application. This is especially true when we start working on routing and
multiple URLs in a Single Page Application in Chapter 6. Also, one controller cannot
directly communicate with another controller to share state or behavior.
Services Versus Controllers
In the applications we develop, we will end up using both controllers as well as services.
Now, when we say “services” in AngularJS, we include factories, services, and providers.
We’ll see the difference between the three in “The Difference Between Factory, Service,
and Provider” on page 82.
That said, both controllers and services fill a certain need in our application, and at‐
tempting to do too much or do in one what ideally belongs in the other can lead to
bloated, unmaintainable, and untestable code. Table 5-1 gives a quick overview of the
types of responsibilites and needs for which we would use controllers versus services.
72
| Chapter 5: All About AngularJS Services
www.it-ebooks.info
Table 5-1. Controllers versus services
Controllers
Services
Presentation logic
Business logic
Directly linked to a view
Independent of views
Drives the UI
Drives the application
One-off, specific
Reusable
Responsible for decisions like what data to fetch, what data to show, how Responsible for making server calls, common
to handle user interactions, and styling and display of UI
validation logic, application-level stores, and reusable
business logic
We’ll dive into AngularJS services next, including how to use existing services and create
our own. After we finish that, we’ll come back to some examples of what belongs in
services.
Dependency Injection in AngularJS
The entire service concept in AngularJS is heavily dependent on and driven by its De‐
pendency Injection system. Any service known to AngularJS (internal or our own) can
be simply injected into any other service, directive, or controller by stating it as a de‐
pendency. AngularJS will automatically figure out what the service is, what it further
depends on, and create the entire chain before injecting a fully instantiated service.
Dependency Injection is a concept that started more on the server side, to basically
propagate reuse, modularity, and testability of code. Dependency Injection states that
instead of creating an instance of a dependent service when we need it, the class or
function should ask for it instead. Something else (usually known as an injector) would
then be responsible for figuring out how to create it and pass it in.
Consider a case where we had a service called $http that could make server calls. Now
let’s take two cases, with and without Dependency Injection:
// Without Dependency Injection
function fetchDashboardData() {
var $http = new HttpService();
return $http.get('my/url');
}
// With Dependency Injection
function fetchDashboardData($http) {
return $http.get('my/url');
}
In the first function, a new instance of $http is created whenever a server call needs to
happen. In the second, the $http service instance itself gets passed in.
What are the disadvantages of the former?
AngularJS Services
www.it-ebooks.info
| 73
• Because it creates a new instance using the new keyword, any test we write for this
function is dependent on HttpService implicitly.
• If we need to extend HttpService to provide for offline functionality, or change it
to sockets, we will be forced to change each implementation where new is called.
• It is inherently tied to HttpService, making it hard to reuse for other cases, such
as with sockets or offline as mentioned previously.
Dependency Injection allows us to:
• Change the underlying implemention of a dependency without manually changing
each dependent function
• Change the underlying implementation just for the test, to prevent it from making
server calls
• Explicitly state what needs to be included and present before this function or con‐
structor can execute
AngularJS guarantees that the function we provide to the service declaration will be
executed only once (lazily, the first time something that needs the dependency is load‐
ed), and future dependents will get that very same instance. That is, AngularJS services
are singletons for the scope of our application. Two controllers or services that ask for
ServiceA will get the very same instance, instead of two different instances.
Using Built-In AngularJS Services
Before we go off and try to create our own services, let’s take a look at some existing
core AngularJS services and how we might use them in our own applications. The sim‐
plest one we can start working with is the $log service.
The $ Prefix in AngularJS
AngularJS prefixes all the services that are provided by the Angu‐
larJS library with the $ sign. So you will see services like $log, $http,
$window, and so on. This is used as a namespacing technique so that
when you see a service, you can immediately figure out whether a
service is coming from AngularJS or somewhere else. Conversely,
when you create your own services, do not prefix them with a $ sign.
It will just end up confusing you and your team at some point in time.
Before we can use any AngularJS service (in a controller, service, or otherwise), we need
to inject it in. Let’s see how we can write a very simple controller that pulls in the $log
service:
74
| Chapter 5: All About AngularJS Services
www.it-ebooks.info
<!-- File: chapter5/log-example.html -->
<html ng-app="notesApp">
<body ng-controller="MainCtrl as mainCtrl">
<h1>Hello Services!</h1>
<button ng-click="mainCtrl.logStuff()">Log something</button>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('notesApp', [])
.controller('MainCtrl', ['$log', function($log) {
var self = this;
self.logStuff = function() {
$log.log('The button was pressed');
};
}])
</script>
</body>
</html>
In this example, the HTML has been simplified down to a single button, which triggers
MainCtrl.logStuff(). It uses the ng-click directive that we’ve seen before.
Our first major change is in the controller definition. So far, we have had the name of
the controller as the first argument to the controller() function, and an array with the
controller definition inside it. Now when we depend on a service, we first add the de‐
pendency as a string in the array (this is what we call the safe style of Dependency
Injection). After we declare it as a string, we then inject it as a variable (the name of our
choosing) into the function that is passed as the last argument in the array.
AngularJS will pick up the individual strings in the array, look up the services internally,
and inject them into the function in the order in which we have defined the strings.
As soon as we have the service, we can use it as the API permits within our controller,
so our logStuff function just logs a string to the console.
Safe Style of Dependency Injection
In the example, we defined our dependencies as follows:
myModule.controller("MainCtrl", ["$log", function($log) {}]);
We could have also defined them as:
myModule.controller("MainCtrl", function($log) {});
That is, ditch the array syntax and directly provide our controller function. Why then
would we go to this extra effort of typing in boilerplate if it doesn’t have any effect?
AngularJS Services
www.it-ebooks.info
| 75
The reason for preferring the first syntax over the latter is that when we build our ap‐
plication for deployment, we often run our JavaScript through a step known as minifi‐
cation or uglification. In this step, our JavaScript is globbed into one single file, com‐
ments are dropped, spaces are removed, and finally, variables are renamed to make them
shorter. So the $log variable might get renamed to xz (or some other random, shorter
name).
When we normally run our application and use the latter syntax (without the arrays),
AngularJS is able to look at the name of the variable and figure out what service we need.
When the uglification has finished, AngularJS has no clue what the variable xz previ‐
ously referred to.
The uglification and minification processes do not touch string constants. Therefore,
the first example would get translated to something like:
myModule.controller("MainCtrl", ["$log", function(xz) {}]);
while the latter example would translate to something like:
myModule.controller("MainCtrl", function(xz) {});
In the former, AngularJS still has the string "$log" to tell it what the service originally
was, while it doesn’t have that in the latter.
Recent developments like the ng-min library allow us to write code in the latter way and
have it automatically convert to the former, but it can have edge cases. So it might be
preferable to always use the safer style of Dependency Injection in case you don’t want
to risk it.
In this book, we will always use the safer style of Dependency Injection.
Order of Injection
We define our dependencies as strings. AngularJS inspects the strings, and injects the
dependencies in the order in which they are listed:
myModule.controller("MainCtrl",
["$log", "$window", function($l, $w) {}]);
In this line of code, the $log service would be injected into the $l variable in the function,
and the $window service would get injected into the $w variable:
myModule.controller("MainCtrl",
["$log", "$window", function($w, $l) {}]);
In this line of code, it is almost the exact same thing, except the $w and $l variables have
been switched inside the function. AngularJS will ignore this and take its cue from the
strings. So the $w variable will actually hold the $log service, and the $l variable would
in fact hold the $window service.
76
| Chapter 5: All About AngularJS Services
www.it-ebooks.info
So just be careful to keep the strings and the variables in sync and in the same order, or
expect some craziness with your code.
Common AngularJS Services
Some other AngularJS services that we will see or use on a common basis are:
$window
The $window service in AngularJS is nothing but a wrapper around the global win‐
dow object. The sole reason for its existence is to avoid global state, especially in
tests. Instead of directly working with the window object, we can ask for and work
with $window. In the unit tests, the $window service can be easily mocked out (avail‐
able for free with the AngularJS mocking library).
$location
The $location service in AngularJS allows us to interact with the URL in the
browser bar, and get and manipulate its value. Any changes made to the $loca
tion service get reflected in the browser, and any changes in the browser are im‐
mediately captured in the $location service. The $location service has the fol‐
lowing functions, which allow us to work with the URL:
absUrl
A getter that gives us the absolute URL in the browser (called $location.
absUrl()).
url
A getter and setter that gets or sets the URL. If we give it an argument, it will
set the URL; otherwise, it will return the URL as a string.
path
Again, a getter and setter that sets the path of the URL. Automatically adds the
forward slash at the beginning. So $location.path() would give us the current
path of the application, and $location.path("/new") would set the path
to /new.
search
Sets or gets the search or query string of the current URL. Calling $loca
tion.search() without any arguments returns the search parameter as an ob‐
ject. Calling $location.search("test") removes the search parameter from
the URL, and calling $location.search("test", "abc"); sets the search pa‐
rameter test to abc.
$http
We will deal with $http extensively in Chapter 6, but it is the core AngularJS service
used to make XHR requests to the server from the application. Using the $http
AngularJS Services
www.it-ebooks.info
| 77
service, we can make GET and POST requests, set the headers and caching, and
deal with server responses and failures.
Creating Our Own AngularJS Service
We saw how to use AngularJS services through the use of some built-in AngularJS
services. We will be using the ones previously mentioned extensively throughout the
book going forward, so don’t worry that we didn’t get to see them all in action yet.
The core AngularJS services only touch the tip of the iceberg in terms of the functionality
we will need when we start creating our own AngularJS applications. But how do we
decide between embedding our functionality right in the controller or putting it in a
service? We should consider creating an AngularJS service if what we are implementing
falls into one of the following broad criteria:
It needs to be reusable
More than one controller or service will need to access the particular function that
is being implemented.
Application-level state
Controllers get created and destroyed. If we need state stored across our application,
it belongs in a service.
It is independent of the view
If what we are implementing is not directly linked to a view, it probably belongs in
a service.
It integrates with a third-party service
We need to integrate a third-party service (think SocketIO, BreezeJS, etc.), but we
want to be able to mock or replace it in our unit tests. A service makes that easy.
Caching/factories
Do we need an object cache? Or something that creates model objects? Services are
our best bet.
Services themselves can depend on other built-in services or our own services. So tra‐
ditional software engineering concepts like modularity, composite services, and even
hierarchy of services are still applicable.
Creating a Simple AngularJS Service
Let’s take an example of how to create a simple service. We will take the very first example
from this chapter, which demonstrated the problem with using just controllers, and use
a service to share the state between the two views:
<!-- File: chapter5/simple-angularjs-service/index.html -->
<html ng-app="notesApp">
78
| Chapter 5: All About AngularJS Services
www.it-ebooks.info
<body ng-controller="MainCtrl as mainCtrl">
<h1>Hello Controllers!</h1>
<button ng-click="mainCtrl.open('first')">
Open First
</button>
<button ng-click="mainCtrl.open('second')">
Open Second
</button>
<div ng-switch on="mainCtrl.tab">
<div ng-switch-when="first">
<div ng-controller="SubCtrl as ctrl">
<h3>First tab</h3>
<ul>
<li ng-repeat="item in ctrl.list()">
<span ng-bind="item.label"></span>
</li>
</ul>
<button ng-click="ctrl.add()">
Add More Items
</button>
</div>
</div>
<div ng-switch-when="second">
<div ng-controller="SubCtrl as ctrl">
<h3>Second tab</h3>
<ul>
<li ng-repeat="item in ctrl.list()">
<span ng-bind="item.label"></span>
</li>
</ul>
<button ng-click="ctrl.add()">
Add More Items
</button>
</div>
</div>
</div>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script src="app.js"></script>
</body>
</html>
The app.js file, which houses the controllers and services, looks something like this:
// File: chapter5/simple-angularjs-service/app.js
angular.module('notesApp', [])
Creating Our Own AngularJS Service
www.it-ebooks.info
| 79
.controller('MainCtrl', [function() {
var self = this;
self.tab = 'first';
self.open = function(tab) {
self.tab = tab;
};
}])
.controller('SubCtrl', ['ItemService',
function(ItemService) {
var self = this;
self.list = function() {
return ItemService.list();
};
self.add = function() {
ItemService.add({
id: self.list().length + 1,
label: 'Item ' + self.list().length
});
};
}])
.factory('ItemService', [function() {
var items = [
{id: 1, label: 'Item 0'},
{id: 2, label: 'Item 1'}
];
return {
list: function() {
return items;
},
add: function(item) {
items.push(item);
}
};
}]);
We changed the following things in the previous example:
• Instead of the list being instantiated and stored in the SubCtrl (and getting de‐
stroyed and re-created), we are storing the list in a service called ItemService.
• The SubCtrl has a function called list(), which just delegates and returns the
value of ItemService.list() function.
• The SubCtrl has a function called add() that delegates and adds an item to the
ItemService.
• The HTML now binds the ng-repeat to ctrl.list() instead of ctrl.list. So it
calls the function and uses its return value to display the array in the UI.
We created the ItemService using an AngularJS module function called factory:
80
| Chapter 5: All About AngularJS Services
www.it-ebooks.info
• The factory function follows a similar declaration style like the controller. So we
declare the name of the service, ItemService, in the first argument and then the
array syntax for Dependency Injection with our actual service function as the sec‐
ond argument.
• In the service definition function, we return an object, which becomes the API for
the service. In this case, the ItemService defines two functions, list and add, which
all users of the service can access.
• In the service definition function, we also declare some local variables (in this case,
the items array). These are private to the service, and cannot be accessed directly
(though they are accessible through the list function here) by any users of the
service. Therefore, no controller can access ItemService.items directly.
The ItemService gets instantiated once when the application loads and the SubCtrl is
loaded, at which point AngularJS decides it needs an instance of the ItemService. After
it is created, all other controllers that ask for the ItemService will get the exact same
instance that was returned the very first time.
This is why both the tabs in our example show the exact same list, and if we click Add
in one tab and then move to the other tab, the items still show up.
To summarize, when we create our own AngularJS service:
• Use the angular.module().factory function to declare the service’s name and
dependencies.
• Return an object, or a function from within the service definition, which becomes
the public API for our service.
• Hold internal state as local variables inside the service. This is important because
in a Single Page Application where controllers can get created and destroyed, the
service can act as an application-level store.
AngularJS guarantees the following:
• The service will be lazily instantiated. The very first time a controller, service, or
directive asks for the service, it will be created.
• The service definition function will be called once, and the instance stored. Every
caller of this service will get this same, singleton instance handed to them.
This is important because in a Single Page Application, the HTML and controllers can
get destroyed and created multiple times in an application.
Creating Our Own AngularJS Service
www.it-ebooks.info
| 81
In this way, we can create our own AngularJS service, and define the API of how someone
interacts with our own service. Notice that we call ItemService a service, even though
we defined it using a function called factory. We will touch upon this in the next section.
The Difference Between Factory, Service, and Provider
AngularJS provides a few different ways in which we can to create and register services
(and constants and values), depending on our preference and style of programming. In
the previous section, we used the factory function to define our services.
You should use module.factory() to define your services if:
• You follow a functional style of programming
• You prefer to return functions and objects
JavaScript (and AngularJS) also allow us to follow a Class/OO style of programming,
where we define classes and types instead of functions and objects. When we use a
service, AngularJS assumes that the function definition passed in as part of the array of
dependencies is actually a JavaScript type/class. So instead of just invoking the function
and storing its return value, AngularJS will call new on the function to create an instance
of the type/class.
Let’s see how the service we defined in the previous example changes if we use the
service() function:
// File: chapter5/item-service-using-service/app.js
function ItemService() {
var items = [
{id: 1, label: 'Item 0'},
{id: 2, label: 'Item 1'}
];
this.list = function() {
return items;
};
this.add = function(item) {
items.push(item);
};
}
angular.module('notesApp', [])
.service('ItemService', [ItemService])
.controller('MainCtrl', [function() {
var self = this;
self.tab = 'first';
self.open = function(tab) {
self.tab = tab;
};
}])
82
| Chapter 5: All About AngularJS Services
www.it-ebooks.info
.controller('SubCtrl',
['ItemService', function(ItemService) {
var self = this;
self.list = function() {
return ItemService.list();
};
self.add = function() {
ItemService.add({
id: self.list().length + 1,
label: 'Item ' + self.list().length
});
};
}]);
In this example, we can use the exact same controllers and HTML from before. We can
use this app.js with the index.html from the previous example. We only display the part
that is changed, which is the service definition:
• The first thing of note is that we now use service instead of factory for defining
our AngularJS service.
• Our service definition function is now a JavaScript class function. It doesn’t return
anything.
• Our service defines the public API by defining methods (add, list) on its instance
(using the this keyword).
• Private state for the service is still defined as local variables inside the function
definition.
• AngularJS will perform new ItemService() (with possible dependencies injected
in) and then return that instance to all functions that depend on ItemService.
The third and final way of defining services is using the provider function. This is not
a very common approach, but can be useful when we need to set up some configuration
for our service before our application loads. We will deal with application-level config‐
uration in Chapter 6, but with the provider, we can have functions that can be called to
set up how our service works based on the language, environment, or other things that
are applicable to our service. Let’s see how that might look:
// File: chapter5/item-service-using-provider/app.js
function ItemService(opt_items) {
var items = opt_items || [];
this.list = function() {
return items;
};
this.add = function(item) {
items.push(item);
Creating Our Own AngularJS Service
www.it-ebooks.info
| 83
};
}
angular.module('notesApp', [])
.provider('ItemService', function() {
var haveDefaultItems = true;
this.disableDefaultItems = function() {
haveDefaultItems = false;
};
// This function gets our dependencies, not the
// provider above
this.$get = [function() {
var optItems = [];
if (haveDefaultItems) {
optItems = [
{id: 1, label: 'Item 0'},
{id: 2, label: 'Item 1'}
];
}
return new ItemService(optItems);
}];
})
.config(['ItemServiceProvider',
function(ItemServiceProvider) {
// To see how the provider can change
// configuration, change the value of
// shouldHaveDefaults to true and try
// running the example
var shouldHaveDefaults = false;
//
//
//
if
Get configuration from server
Set shouldHaveDefaults somehow
Assume it magically changes for now
(!shouldHaveDefaults) {
ItemServiceProvider.disableDefaultItems();
}
}])
.controller('MainCtrl', [function() {
var self = this;
self.tab = 'first';
self.open = function(tab) {
self.tab = tab;
};
}])
.controller('SubCtrl',
['ItemService', function(ItemService) {
var self = this;
self.list = function() {
return ItemService.list();
84
| Chapter 5: All About AngularJS Services
www.it-ebooks.info
};
self.add = function() {
ItemService.add({
id: self.list().length + 1,
label: 'Item ' + self.list().length
});
};
}]);
We introduced two new concepts in this example. The rest of the controllers and HTML
remain the same as before, so use the same index.html from the factory example:
• ItemService now takes in the list of default items as an argument to the constructor.
• ItemService is declared using a provider. We define a function in the provider
called disableDefaultItems. This can be called in the configuration phase of an
AngularJS application. That is, this can be called before the AngularJS app has
loaded and the service has been initialized.
• Note that the provider does not use the same notation as factory and service. It
doesn’t take an array as the second argument because providers cannot have de‐
pendencies on other services.
• The provider also declares a $get function on its instance, which is what gets called
when the service needs to be initialized. At this point, it can use the state that has
been set up in the configuration to instantiate the service as needed.
Let’s take a look at the config function that we have defined:
• The config function executes before the AngularJS app executes. So we can be
assured that this executes before our controllers, services, and other functions.
• The config function follows the same Dependency Injection pattern, but gets pro‐
viders injected in. In this case, we ask for the ItemServiceProvider.
• At this point, we can now call functions and set values that the provider exposes.
We are able to the call the disableDefaultItems function that we defined in the
provider.
• The config function could also set up URL endpoints, locale information, routing
configuration for our application, and so on: things that need to be executed and
initialized before our application starts.
• We can try changing the value of shouldHaveDefaults to true (this would come
from the server or URL, or some other way usually) to see the effect it has on our
application.
Creating Our Own AngularJS Service
www.it-ebooks.info
| 85
Conclusion
We saw the limitations of just using controllers for our entire application, and saw how
and when to use AngularJS services. We saw the Dependency Injection syntax in An‐
gularJS when we used the built-in $log service. We then covered some of other builtin services before diving into creating our own AngularJS services.
We then created the same AngularJS service (a data store for an array of items) in three
different ways, depending on the need and preference. We created the functional form
using the factory method, the OO style using the service method, and the configurable
version using the provider method.
In the next chapter, we will build on these concepts and start working with server com‐
munication in the context of AngularJS.
86
| Chapter 5: All About AngularJS Services
www.it-ebooks.info
CHAPTER 6
Server Communication Using $http
In Chapter 5, we looked at AngularJS services and how they differ from controllers. We
also explored some basic core AngularJS built-in services, and saw how to create our
own AngularJS service as well.
In this chapter, we explore how to start creating applications that can communicate with
a server to fetch and store data. In particular, we will work with the $http service and
save and update information. By the end of the chapter, we as developers should be
extremely comfortable working with asynchronous tasks in AngularJS and with server
communication, because we have built the infrastructure we might need for a fullfledged application.
Fetching Data with $http Using GET
The traditional way of making a request to the server from AJAX applications (using
XMLHttpRequests) involves getting a handle on the XMLHttpRequest object, making the
request, reading the response, checking the error codes, and finally processing the server
response. It goes something like this:
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readystate == 4 && xmlhttp.status == 200) {
var response = xmlhttp.responseText;
} else if (xmlhttp.status == 400) { // or really anything in the 4 series
// Handle error gracefully
}
};
// Set up connection
xmlhttp.open("GET", "http://myserver/api", true);
87
www.it-ebooks.info
// Make the request
xmlhttp.send();
This is a lot of work for such a simple, common, and often repeated task. More often
than not, we will likely end up creating wrappers or using a library.
$http is a core AngularJS service that allows us to communicate with server endpoints
using XHR. The AngularJS XHR API follows what is commonly known as the Promise
interface. Because XHRs are asynchronous method calls, the response from the server
will come back at an unknown future date and time (hopefully almost immediately).
The Promise interface guarantees how such responses will be dealt with, and allows
consumers of the Promise to use them in a predictable manner.
Reducing Code with ngResource
In case you have a RESTFUL API on your server, you can further reduce the amount of
code you write by using AngularJS’s optional module, ngResource. ngResource allows
us to take an API endpoint and create an AngularJS service around it. For example,
consider an API for projects on the server side that behaves like the following:
• GET request to /api/project/ returned an array of projects
• GET request to /api/project/17 returned the project with ID 17
• POST request to /api/project/ with a project object as JSON created a new project
• POST request to /api/project/19 with a project object as JSON updated the project
with ID 19
• DELETE request to /api/project/ deleted all the projects
• DELETE request to /api/project/23 deleted the project with ID 23
If we have such an API, then instead of manually creating a project resource, and wrap‐
ping up $http requests individually, we could just create a service as follows:
angular.module('resourceApp', ['ngResource'])
.factory('ProjectService', ['$resource', function($resource) {
return $resource('/api/project/:id');
}]);
This would automatically give us methods on ProjectService like:
• ProjectService.query() to get a list of projects
• ProjectService.save({id: 15}, projectObj) to update a project with ID 15
• ProjectService.get({id: 19}) to get an individual project with ID 19
88
| Chapter 6: Server Communication Using $http
www.it-ebooks.info
and so on. You can read more about the configuration and options you get with ngRe
source at the official AngularJS docs for ngResource.
We have a simple server that we can use to create and list some notes that we have made
available at our GitHub repository. To set up the server, clone the repository, open the
chapter6 folder, and execute the following commands in succession:
• npm install
• node server.js
Then navigate to http://localhost:8000 and click the first link to see this in action.
We need to serve the HTML and the JavaScript for this chapter from this server so that
it can make GET and POST requests successfully. The browser prevents us from making
XHR requests to any other domain for security reasons.
Our server exposes the following endpoints:
/api/note
GET request gives back an array of notes present on the server.
/api/note
POST request creates a note.
/api/note/:id
GET request with the :id replaced with a numeric ID returns the note with given
ID.
/api/note/:id
POST request with the id updates the note with the given ID.
The entire data is stored in memory so if we kill and restart the server, any data we might
have added will be destroyed.
Given this, let’s see how our app would fetch the list of notes from the server and display
them:
<!-- File: chapter6/public/http-get-example.html -->
<html ng-app="notesApp">
<head>
<title>$http get example</title>
<style>
.item {
padding: 10px;
}
</style>
</head>
Fetching Data with $http Using GET
www.it-ebooks.info
| 89
<body ng-controller="MainCtrl as mainCtrl">
<h1>Hello Servers!</h1>
<div ng-repeat="todo in mainCtrl.items" class="item">
<div><span ng-bind="todo.label"></span></div>
<div>- by <span ng-bind="todo.author"></span></div>
</div>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script>
angular.module('notesApp', [])
.controller('MainCtrl', ['$http', function($http) {
var self = this;
self.items = [];
$http.get('/api/note').then(function(response) {
self.items = response.data;
}, function(errResponse) {
console.error('Error while fetching notes');
});
}]);
</script>
</body>
</html>
In this example, our HTML is quite simple. We have a div that has the MainCtrl attached
to it. Inside the div, we have an ng-repeat over the items array in our controller in
which we ng-bind to the label and author fields.
Our controller has a dependency on $http as a service. Then, when the controller loads,
we make a GET request to the /api/notes server endpoint. $http.get() returns what
we call a Promise object (more on this in just a bit), which allows us to chain functions
as if they were synchronous. Our server call might execute in a jiffy, or might take a few
seconds to execute. With a Promise object, we can say, when the server returns a re‐
sponse (whether it is a success or failure), then execute the following function.
This is important because promises (just like callbacks) allow us to deal with scalability
issues. Both of these concepts keep JavaScript nonblocking and event-driven, which
allows us to let the browser continue to do its work while the server request is in flight.
Now, regardless of whether the server has only one request, ten requests, or even a
million requests, the client is not stuck waiting for the response. It can continue to do
other stuff without making the UI hang.
The then function takes two arguments, a success handler and an error handler. If the
server returns a non-200 response, the error handler is called. Otherwise, the success
handler is triggered. Both these handlers get passed in a response object, which has the
following keys:
90
| Chapter 6: Server Communication Using $http
www.it-ebooks.info
headers
The headers for the call
status
The status code for the response
config
The configuration with which the call was made
data
The body of the response from the server
In the case of success, we just assign the data from the server to the items array and let
AngularJS take care of updating the UI through data-binding. In the case of an error,
we just log it to the console.
A Deep Dive into Promises
At this point, let’s quickly dive into the powerful concept that are promises. Promises in
AngularJS are based on Kris Kowal’s Q proposal, which is a standardized, convenient
way of dealing with asynchronous calls in JavaScript.
The traditional way to deal with asynchronous calls in JavaScript has been with call‐
backs. Say we had to make three calls to the server, one after the other, to set up our
application. With callbacks, the code might look something like the following (assuming
a xhrGET function to make the server call):
// Fetch some server configuration
xhrGET('/api/server-config', function(config) {
// Fetch the user information, if he's logged in
xhrGET('/api/' + config.USER_END_POINT, function(user) {
// Fetch the items for the user
xhrGET('/api/' + user.id + '/items', function(items) {
// Actually display the items here
});
});
});
In this example, we first fetch the server configuration. Then based on that, we fetch
information about the current user, and then finally get the list of items for the current
user. Each xhrGET call takes a callback function that is executed when the server
responds.
Now of course the more levels of nesting we have, the harder the code is to read, debug,
maintain, upgrade, and basically work with. This is generally known as callback hell.
Also, if we needed to handle errors, we need to possibly pass in another function to each
xhrGET call to tell it what it needs to do in case of an error. If we wanted to have just one
common error handler, that is not possible.
Fetching Data with $http Using GET
www.it-ebooks.info
| 91
The Promise API was designed to solve this nesting problem and the problem of error
handling. The Promise API proposes the following:
1. Each asynchronous task will return a promise object.
2. Each promise object will have a then function that can take two arguments, a success
handler and an error handler.
3. The success or the error handler in the then function will be called only once, after
the asynchronous task finishes.
4. The then function will also return a promise, to allow chaining multiple calls.
5. Each handler (success or error) can return a value, which will be passed to the next
function in the chain of promises.
6. If a handler returns a promise (makes another asynchronous request), then the next
handler (success or error) will be called only after that request is finished.
So the previous example code might translate to something like the following, using
promises and the $http service:
$http.get('/api/server-config').then(function(configResponse) {
return $http.get('/api/' + configResponse.data.USER_END_POINT);
}).then(function(userResponse) {
return $http.get('/api/' + userResponse.data.id + '/items');
}).then(function(itemResponse) {
// Display items here
}, function(error) {
// Common error handling
});
In this example, we use the $http service to make a series of server calls. Each server
call using $http.get returns a promise, and we use the then function to add a success
handler. In the first two success handlers, we use the response from the server to make
another server call.
Each success handler in the promise returns another promise using $http.get. Angu‐
larJS then waits for that server call to return before proceeding to the next function in
the promise chain. Also, the server response value for that promise will be passed as an
argument to the next success handler in the chain. So, the first then will get the config
Response. The second then will get the return value of the configResponse success
handler, which is the userResponse, and so on.
Also, we have one error handler, which we pass as the second argument to the very last
function in the promise chain. Because of this, if any error happens in any of the func‐
tions in the promise chain, AngularJS will find the next closest error handler and trigger
it. So regardless of whether the error happens in the config request or the user request,
the common error handling function will get called.
92
| Chapter 6: Server Communication Using $http
www.it-ebooks.info
Propagating Success and Error
Chaining promises is a very powerful technique that allows us to accomplish a lot of
functionality, like having a service make a server call, do some postprocessing of the
data, and then return the processed data to the controller. But when we work with
promise chains, there are a few things we need to keep in mind.
Consider the following hypothetical promise chain with three promises, P1, P2, and P3.
Each promise has a success handler and an error handler, so S1 and E1 for P1, S2 and
E2 for P2, and S3 and E3 for P3:
xhrCall()
.then(S1, E1) //P1
.then(S2, E2) //P2
.then(S3, E3) //P3
In the normal flow of things, where there are no errors, the application would flow
through S1, S2, and finally, S3. But in real life, things are never that smooth. P1 might
encounter an error, or P2 might encounter an error, triggering E1 or E2.
Now, depending on the return value of any of these handlers, AngularJS will decide
which function in the chain to execute next. At each of these handlers, we as developers
have control. We can decide, given the current handler, which function in the chain to
execute next. Consider the following cases:
• We receive a successful response from the server in P1, but the data returned is not
correct, or there is no data available on the server (think empty array). In such a
case, for the next promise P2, it should trigger the error handler E2.
• We receive an error for promise P2, triggering E2. But inside the handler, we have
data from the cache, ensuring that the application can load as normal. In that case,
we might want to ensure that after E2, S3 is called.
So each time we write a success or an error handler, we need to make a call—given our
current function, is this promise a success or a failure for the next handler in the promise
chain?
If we want to trigger the success handler for the next promise in the chain, we can just
return a value from the success or the error handler, and AngularJS will treat it as us
successfully resolving any errors.
If, on the other hand, we want to trigger the error handler for the next promise in the
chain, we can leverage the $q service in AngularJS. Just ask for $q as a dependency in
our controller and service, and return $q.reject(data) from the handler. This will
ensure that the next promise in the chain goes into the error condition, and will get the
data passed to it as an argument.
Fetching Data with $http Using GET
www.it-ebooks.info
| 93
The $q Service
The $q service in AngularJS has the following APIs for us to use in our application:
$q.defer()
Creates a deferred object when we need to create a promise for our own asynchro‐
nous task. Most asynchronous tasks in AngularJS (server calls, timeouts, and in‐
tervals) return a promise, but if we are integrating with a third-party library, then
we might need our own promise. The $q.defer() is useful in those times because
the deferred object has a promise attribute that can be returned from a function.
deferredObject.resolve
The deferred object created by the previous function can be resolved successfully
at any point by calling the resolve() function on it with the argument being the
data passed to the success handler in the promise chain.
deferredObject.reject
The deferred object can also be rejected, thus denoting that the promise was a failure
and triggering the failure handler in the promise. Again, the argument passed to it
will be passed to the error handler as is.
$q.reject
The $q.reject() can be called from within any of the promise handlers (success
or error) with an optional argument that denotes the value to be passed along in
the promise chain. The return value of this should be returned to ensure that the
promise continues to the next error handler instead of the success handler in the
promise chain.
Making POST Requests with $http
Given this background, let’s now build out the rest of the UI for this server communi‐
cation example. We will now add a section that allows users to add notes to display in
this list:
<!-- File: chapter6/public/http-post-example.html -->
<html ng-app="notesApp">
<head>
<title>HTTP Post Example</title>
<style>
.item {
padding: 10px;
}
</style>
</head>
<body ng-controller="MainCtrl as mainCtrl">
<h1>Hello Servers!</h1>
94
| Chapter 6: Server Communication Using $http
www.it-ebooks.info
<div ng-repeat="todo in mainCtrl.items"
class="item">
<div><span ng-bind="todo.label"></span></div>
<div>- by <span ng-bind="todo.author"></span></div>
</div>
<div>
<form name="addForm"
ng-submit="mainCtrl.add()">
<input type="text"
placeholder="Label"
ng-model="mainCtrl.newTodo.label"
required>
<input type="text"
placeholder="Author"
ng-model="mainCtrl.newTodo.author"
required>
<input type="submit"
value="Add"
ng-disabled="addForm.$invalid">
</form>
</div>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script>
angular.module('notesApp', [])
.controller('MainCtrl', ['$http', function($http) {
var self = this;
self.items = [];
self.newTodo = {};
var fetchTodos = function() {
return $http.get('/api/note').then(
function(response) {
self.items = response.data;
}, function(errResponse) {
console.error('Error while fetching notes');
});
};
fetchTodos();
self.add = function() {
$http.post('/api/note', self.newTodo)
.then(fetchTodos)
.then(function(response) {
self.newTodo = {};
});
};
}]);
Fetching Data with $http Using GET
www.it-ebooks.info
| 95
</script>
</body>
</html>
In this example, we added a new section to the HTML that has a standard form with
two input fields. We bind these two input fields to our model, newTodo, in the controller.
On submit of the form, we trigger the add() function in our controller.
Our controller has slightly changed from the previous example as well. The fetching of
notes from the server is now wrapped inside the fetchTodos() function, which in ad‐
dition to making the $http.get server call also returns the promise for the async call.
This function is triggered once when the controller loads.
The add function also uses the $http service, and calls $http.post. Unlike the GET,
which takes one argument, the URL of the server, the POST request takes two argu‐
ments: the URL and the post data. We chain this server call to call fetchTodos on a
successful creation of the todo. We then finally add another promise to the chain, which
will clear the newTodo object. This last promise handler will only get triggered after the
server call to create the todo, and the server call to get the list of todos (because of the
promise returned by the fetchTodos function) both finish.
$http API
We have been using $http to get and save data, so let’s take a look at the actual API that
the $http service in AngularJS provides.
$http provides the following convenience methods to make certain types of requests:
• GET
• HEAD
• POST
• DELETE
• PUT
• JSONP
So just like $http.get, we can use $http.put or $http.delete. Each of these method
signatures are in one of two patterns:
• For requests without any post data (think GET), the function takes two arguments:
the URL as the first argument, and a configuration object as the second.
• For requests with post data (POST, PUT), the function takes three arguments: the
URL as the first argument, the post data as the second, and a configuration object
as the third and final argument.
96
| Chapter 6: Server Communication Using $http
www.it-ebooks.info
Each of these is a convenience method, and can actually be directly called through $http
itself. That is:
$http.get(url, config)
can be replaced with:
$http(config)
where the url and the method (GET, in this case) become part of the configuration object
itself. In each of the convenience methods ($http.get, $http.post, etc.), the config
object, which is the last parameter, is optional. So we can call $http.get with only the
URL like we did in these examples.
Configuration
We have been mentioning this configuration object for the past few paragraphs, so let’s
take a look at some acceptable parameters and values for it. The following is a basic
pseudocode template for the configuration object, which details the keys that are ac‐
ceptable and the type of value that it expects:
{
method: string,
url: string,
params: object,
data: string or object,
headers: object,
xsrfHeaderName: string,
xsrfCookieName: string,
transformRequest: function transform(data, headersGetter) or
an array of functions,
transformResponse: function transform(data, headersGetter) or
an array of functions,
cache: boolean or Cache object,
timeout: number,
withCredentials: boolean
}
The GET, POST, and other convenience methods set the method parameter, so we don’t
need to. Similarly, if we give the GET or POST requests a URL, it gets set in the config
automatically.
We can change the request or how it behaves by passing the config object set with the
following keys:
method
A string representing the HTTP request type, like GET or POST.
url
A URL string representing the absolute or relative URL of the resource being
requested.
Fetching Data with $http Using GET
www.it-ebooks.info
| 97
params
A JavaScript object with keys and values translating to URL query parameter keys
and values. For example:
[{key1: 'value1', key2: 'value2'}]
would be converted to:
?key1=value1&key2=value2
after the URL. If we use an object instead of a string or a number for the value, the
object will be converted to a JSON string.
data
A string or an object that will be sent as the request message data. This basically
becomes the POST data for the server.
headers
An object (or map) with each key being the name of the header, and the value being
the value of that particular header. So passing {'Content-Type': 'text/csv'}
would set the Content-Type header to be text/csv.
xsrfHeaderName
We can set the XSRF header that the server will be setting to prevent XSRF attacks
on our website. This will then be used in the request to ensure the XSRF handshake
happens with our server.
xsrfCookieName
The name of the cookie that has the xsrf token to be used for the XSRF handshake.
transformRequest and transformResponse
These provide a way for us to change the data for the request going out or the
response coming in. These take a single function (which gets passed the data and
a way to get the headers) or an array of these functions. Each of these functions can
take the data (which is either the post data being sent, or the data of the response),
and then return the converted, transformed data from it. A simple transformRe
quest that takes the JSON post data and converts it into jQuery like a post data
string is as follows:
transformRequest: function(data, headers) {
var requestStr;
for (var key in data) {
if (requestStr) {
requestStr += '&' + key + '=' + data[key];
} else {
requestStr = key + '=' + data[key];
}
}
98
| Chapter 6: Server Communication Using $http
www.it-ebooks.info
return requestStr;
}
cache
A Boolean or a cache object to use for an application-level caching mechanism.
This would be over and above the browser-level caching. If set to true, AngularJS
will automatically cache server responses and return them for subsequent requests
to the same URL.
timeout
The time in milliseconds to wait before the request is treated as timed out. This can
also be a promise object, which when rejected tells AngularJS to abandon the server
call.
Advanced $http
Until now, we have seen how to make simple GET and POST requests using the $http
service and have looked into some of the configuration we can do at a request level.
Until this point though, we have been dealing with the $http service on a request level.
The $http service also allows us to configure defaults, or intercept each and every re‐
quest going out and response coming in to have some common handling. We will deal
with these in this section.
Configuring $http Defaults
The first thing we want to look at is how to configure $http defaults. We saw how to
add transformations and headers as part of the config of a single $http request in the
previous section. If we needed to add a caching header as part of each and every request,
it could quickly become annoying if we did it each time we call $http. For these kinds
of requirements, we can use the config section of our module, and use the $httpPro
vider to configure these defaults. Let’s see how we might configure some headers and
a default transformRequest using the $httpProvider:
// File: chapter6/public/http-defaults.js
angular.module('notesApp', [])
.controller('LoginCtrl', ['$http', function($http) {
var self = this;
self.user = {};
self.message = 'Please login';
self.login = function() {
$http.post('/api/login', self.user).then(
function(resp) {
self.message = resp.data.msg;
});
};
}])
Advanced $http | 99
www.it-ebooks.info
.config(['$httpProvider', function($httpProvider) {
// Every POST data becomes jQuery style
$httpProvider.defaults.transformRequest.push(
function(data) {
var requestStr;
if (data) {
data = JSON.parse(data);
for (var key in data) {
if (requestStr) {
requestStr += '&' + key + '=' + data[key];
} else {
requestStr = key + '=' + data[key];
}
}
}
return requestStr;
});
// Set the content type to be FORM type for all post requests
// This does not add it for GET requests.
$httpProvider.defaults.headers.post['Content-Type'] =
'application/x-www-form-urlencoded';
}]);
In this example, we set up some application-level configuration for the $http service.
We do this by creating a config section for our module and getting the $httpProvid
er injected into it. As part of the configuration, we first add a global request transformer,
which changes the post data for any outgoing request from a JSON object into a jQuery
post string format. Notice that we push this transformation function into the default
transformers.
We would do something like the preceding example when we are dealing with a backend
that is configured to accept Content-Type text/www-form-urlencoded, which is what
jQuery defaults to. AngularJS defaults to application/json, which is recommended for
web applications. If you can’t change your backend to accept application/json, then you
can add a transformer and header to get your AngularJS application talking to your
backend.
We can have multiple transformers for requests and responses, both at an individual
request level as well as a global level, so the transformRequest and transformRes
ponse are arrays by default. We just push (and unshift) as necessary the functions we
want to add.
Then we also add a default header to all outgoing GET requests. The $httpProvid
er.defaults.headers object allows us to set default headers for common, get, post, and
put requests. Each one ($httpProvider.defaults.headers.post, for example) is an
object, where the key is the header name and the value is the value of the header. In this
example, we set the Content-Type for all outgoing POST requests.
100
| Chapter 6: Server Communication Using $http
www.it-ebooks.info
The following is the list of keys and values that can have defaults set using $httpPro
vider (using $httpProvider.defaults):
• headers.common
• headers.get
• headers.put
• headers.post
• transformRequest
• transformResponse
• xsrfHeaderName
• xsrfCookieName
transformRequest and transformResponse are arrays of functions. The XSRF-related
keys take pure string values. The headers are all object maps with keys being the header
names and the values being the value of the header.
Interceptors
Handling request-level actions (such as logging, authentication check, and handling
certain types of responses) globally has always been challenging. It usually required
planning to create a layer through which all requests would be channeled so that we
could add global hooks.
AngularJS immensely simplifies this using $httpProvider to set up interceptors. An‐
gularJS interceptors allow us to hook and check each request and response and handle
certain events (like the server returning 403s for authorization issues) in a common way.
Do note that the old style of creating response interceptors has been
deprecated and is not expected to be available in future versions of
AngularJS. Therefore, do not use $httpProvider.responseInter
ceptors in your code anymore.
When we create an interceptor (and we can create multiple), AngularJS makes sure that
it is called before any request is made to the server. Similarly, AngularJS makes sure that
it is called first before the controller or service that makes the $http call. So it gives us
a common pipe to work with. Let’s take a quick look at how we implement one:
// File: chapter6/public/logging-interceptor.js
angular.module('notesApp', [])
.controller('MainCtrl', ['$http', function($http) {
var self = this;
self.items = [];
Advanced $http | 101
www.it-ebooks.info
self.newTodo = {};
var fetchTodos = function() {
return $http.get('/api/note').then(function(response) {
self.items = response.data;
}, function(errResponse) {
console.log('Error while fetching notes');
});
};
fetchTodos();
self.add = function() {
$http.post('/api/note', self.newTodo)
.then(fetchTodos)
.then(function(response) {
self.newTodo = {};
});
};
}]).factory('MyLoggingInterceptor', ['$q', function($q) {
return {
request: function(config) {
console.log('Request made with ', config);
return config;
// If an error, not allowed, or my custom condition,
// return $q.reject('Not allowed');
},
requestError: function(rejection) {
console.log('Request error due to ', rejection);
// Continue to ensure that the next promise chain
// sees an error
return $q.reject(rejection);
// Or handled successfully?
// return someValue
},
response: function(response) {
console.log('Response from server', response);
// Return a promise
return response || $q.when(response);
},
responseError: function(rejection) {
console.log('Error in response ', rejection);
// Continue to ensure that the next promise chain
// sees an error
// Can check auth status code here if need to
// if (rejection.status === 403) {
//
Show a login dialog
//
return a value to tell controllers it has
//
been handled
// }
// Or return a rejection to continue the
// promise failure chain
102
| Chapter 6: Server Communication Using $http
www.it-ebooks.info
return $q.reject(rejection);
}
};
}])
.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('MyLoggingInterceptor');
}]);
In this example, we implement an interceptor that simply logs every single outgoing
request and incoming response from the server. We implement interceptors in Angu‐
larJS as factories, which return an object with any or all of the following four methods:
request
Any outgoing request passes through the request function, which is also passed the
configuration with which the request is being made. At this point, we can take a
look at the URL, the post data, the method (whether it is a GET or POST), etc., and
then decide to continue with the request (in which case we return the config), or
we can decide to reject it to prevent the request from being made (using return
$q.reject, which rejects the promise).
requestError
This is triggered if there are multiple interceptors and one of them rejected the
request going out. In that case, the reason for the rejection (the argument to $q.re
ject) is passed to this function.
response
When the server eventually returns, this function is called with the response object
(which holds the configuration, status code, headers, and data). If we need to check
the validity of the data or a particular header, or log the response, this is the place
to do it.
responseError
If the server returns with a non-200 series status code, AngularJS treats it as a
response error. We get the same response configuration handled here, where we
can check the status, do additional work (like show a login dialog if the status is a
403), and then finally continue returning a rejection (to tell future promises to treat
it as a failure), or return a value to say all errors have been handled successfully.
This factory basically dictates how our interceptor works, and how it handles each of
these four cases. In any interceptor, we might decide that we only care about respon
seErrors, so we can implement a factory that returns an object with only that function.
Advanced $http | 103
www.it-ebooks.info
We finally hook it up with the $http service through the $httpProvider in the con
fig function. The $httpProvider has an interceptors array on which we can push in‐
terceptors by name. So we simply push the MyLoggingInterceptor onto it, and that
automatically adds the interceptor after the AngularJS app has finished loading.
Best Practices
We have dived through the depths of $http, seen how to configure requests, intercept
all responses, and much more. With all that behind us, here are a few things we should
keep in mind when we work with $http:
Wrap $http in services
In the previous examples, we directly called $http.get or post in our controllers.
In a real application, we should do this: instead of calling $http.get(/api/
notes) directly from our controller, we should wrap that call in a service so that we
can do something like NoteService.query(), which in turn would do the
$http.get call. This service call can then return the promise so that the controller
can chain and handle the response correctly:
angular.module('notesApp', [])
.factory('NoteService', ['$http', function($http) {
return {
query: function() {
return $http.get('/api/notes');
}
};
}]);
This example code shows how such a NoteService might look. All it does is wrap
the $http call inside a service method, and return the promise from it to which
controllers and other services can add their functionality on the chain.
Use interceptors
There are some common tasks that we might want to do every time a request goes
out from the client, such as logging the request or adding some authorization head‐
ers to the request. Or tasks we might need accomplished or conditions we might
want to check on every response. In such a case, interceptors are our best bet. A
simple interceptor that handles 403s, as well as adds the authorization header on
every request, might look something like the following:
angular.module('notesApp', [])
.factory('AuthInterceptor',
['AuthInfoService', '$q', function(AuthInfoService, $q) {
return {
request: function(config) {
if (AuthInfoService.hasAuthHeader()) {
config.headers['Authorization'] =
AuthInfoService.getAuthHeader();
104
| Chapter 6: Server Communication Using $http
www.it-ebooks.info
}
return config;
},
responseError: function(responseRejection) {
if (responseError.status === 403) {
// Authorization issue, access forbidden
AuthInfoService.redirectToLogin();
}
return $q.reject(responseRejection);
}
};
}])
.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('AuthInterceptor');
}]);
In this example, we added an interceptor that only intercepts outgoing requests and
incoming responses with a non-200 status code. In the case of an outgoing request,
we add the authorization header if it is present in a service called AuthInfoSer
vice. In the case of the responses, we check if the status is a 403 and if so, redirect
the user to the login page. We ensure that the promise is rejected so that the con‐
troller or service still sees a failure. The implementation of AuthInfoService can
be as per the project’s needs.
Chain interceptors
Instead of creating one giant interceptor to do all our intercepting work, we create
multiple tiny interceptors, each with individual responsibility. We have a separate
interceptor for authorization, a separate one for logging, and so on. The interceptors
will be called in the order we add them to the provider, so we can also control the
order in which they are called.
Leverage defaults
If we find ourselves setting the same headers again and again, or adding the same
request or response transformation, then we should heavily consider using defaults.
If all our endpoints return XML instead of JSON, add a default transformRes
ponse to the $httpProvider that takes the XML and converts it into JSON (or vice
versa if our server only accepts XML). We can also set defaults for only GET or
POST requests, so we can be as specific or generic with our defaults as needed.
Advanced $http | 105
www.it-ebooks.info
Conclusion
We saw how to do the very simple task of making GET and POST requests to the server
using the $http service in AngularJS. We used the $http service to fetch a list of notes
from the server as well as add notes to be persisted on the server. We then dove into
some of the configuration parts of AngularJS, to see how to change request options such
as headers and transformers. We then looked at how to set defaults for HTTP requests,
as well how and when to use interceptors and transformers. We demonstrated a few
examples of interceptors, incuding logging interceptors and even authorization inter‐
ceptors. At this point, any task using $http should be straightforward.
In the next chapter, we will leverage $http and start building out a full-fledged appli‐
cation with multiple URLs and routes, and show views depending on the context. We
will use the core AngularJS ngRoute module to accomplish this.
106
| Chapter 6: Server Communication Using $http
www.it-ebooks.info
CHAPTER 7
Unit Testing Services and XHRs
In Chapters 5 and 6, we learned how to leverage existing AngularJS services, as well as
create our very own AngularJS services. We created simple AngularJS services that we
used to store state and communicate across different parts of the application, and serv‐
ices to allow for HTTP communication with our servers.
In Chapter 3, we saw how we might unit test our controllers. Now that we have started
creating our own AngularJS services, we will look at how to unit test them. In particular,
we will test controllers that use built-in AngularJS services, as well as our very own
services. Finally, we will write unit tests for services and controllers that make HTTP
requests, and see how we can mock out and leverage the AngularJS Dependency
Injection.
Dependency Injection in Our Unit Tests
In Chapter 3, we saw how to leverage AngularJS Dependency Injection in our unit tests
to test controllers. We asked for the $controller service, and then created controller
instances as and when we needed them. $controller is actually an AngularJS service
that we ask for in the unit test.
Similary, we can ask for any service that AngularJS knows about in our unit test, whether
it comes from core AngularJS or is one of our own creations. AngularJS will figure out
how to create it, what its dependencies are, and give us a fully instantiated service for
testing.
107
www.it-ebooks.info
Don’t forget that to run these tests, you need to:
1. Switch to the chapter7 folder.
2. Run npm install karma karma-jasmine karma-chromelauncher.
3. Run karma start.
This will ensure that all the dependencies are installed correctly for
you to run the Karma unit tests.
The examples and tests in this book were run using Karma version
0.12.16 and AngularJs version 1.2.19 (both the angular.js and angularmocks.js files). If you are having trouble running them for any rea‐
son, ensure that you are using the same versions.
We will use the following Karma configuration for this chapter:
// File: chapter7/karma.conf.js
// Karma configuration
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['jasmine'],
files: [
'angular.min.js',
'angular-mocks.js',
'*.js'
],
exclude: [],
port: 8080,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};
Let’s see how we might test whether the controller redirects us to a new URL when a
function is called in the controller:
// File: chapter7/simpleCtrl1.js
angular.module('notesApp', [])
.controller('SimpleCtrl', ['$location', function($location) {
var self = this;
self.navigate = function() {
$location.path('/some/where/else');
};
}]);
108
| Chapter 7: Unit Testing Services and XHRs
www.it-ebooks.info
This controller is as simplistic as it can get while depending on a core AngularJS service.
All it does is provide a function called navigate, which changes the current location in
the browser to /some/where/else. Now let’s see how its unit test might look:
// File: chapter7/simpleCtrl1Spec.js
describe('SimpleCtrl', function() {
beforeEach(module('notesApp'));
var ctrl, $loc;
beforeEach(inject(function($controller, $location) {
ctrl = $controller('SimpleCtrl');
$loc = $location;
}));
it('should navigate away from the current page', function() {
$loc.path('/here');
ctrl.navigate();
expect($loc.path()).toEqual('/some/where/else');
});
});
This code snippet is a simple unit test for the SimpleCtrl that we defined earlier. All we
attempt to do is to test the navigate function in the controller. All the navigate function
does is redirect the user to /some/where/else. Now, if this were a real live browser, the
URL would change in the browser bar and an actual page navigation would happen. We
don’t want this happening in our unit test, so the angular-mocks.js file that we included
as part of the Karma configuration provides mocked-out versions of services like $lo
cation and $window. This allows us to unit test without worrying about affecting the
browser or things like global state that might affect our unit tests.
The mocked-out version of the $location (for which we did not have to change a single
line of production code, thanks to Dependency Injection!) allows us to set an initial
state of the browser’s location (/here in this case). After executing the ctrl.navi
gate() function, we can then set an expectation that the $location.path be set to /
some/where/else. Neither of these will change the browser’s actual URL, so the unit tests
will complete as normal.
State Across Unit Tests
Let’s modify the previous code and unit tests to add two functions and two tests to
demonstrate how state is shared (or not shared) between tests:
// File: chapter7/simpleCtrl2.js
angular.module('simpleCtrl2App', [])
.controller('SimpleCtrl2', ['$location', '$window',
function($location, $window) {
var self = this;
Dependency Injection in Our Unit Tests | 109
www.it-ebooks.info
self.navigate1 = function() {
$location.path('/some/where');
};
self.navigate2 = function() {
$location.path('/some/where/else');
};
}]);
// File: chapter7/simpleCtrl2Spec.js
describe('SimpleCtrl2', function() {
beforeEach(module('simpleCtrl2App'));
var ctrl, $loc;
beforeEach(inject(function($controller, $location) {
ctrl = $controller('SimpleCtrl2');
$loc = $location;
}));
it('should navigate away from the current page', function() {
expect($loc.path()).toEqual('');
$loc.path('/here');
ctrl.navigate1();
expect($loc.path()).toEqual('/some/where');
});
it('should navigate away from the current page', function() {
expect($loc.path()).toEqual('');
$loc.path('/there');
ctrl.navigate2();
expect($loc.path()).toEqual('/some/where/else');
});
});
We added two functions to our controller. Both the navigate1 and navigate2 functions
in the controller navigate to a URL. If we call navigate1 first, it navigates to /some/
where. If we call navigate2 next, the URL changes to /some/where/else. The change
from the first navigate1 function (the redirected URL) is visible at the beginning of the
navigate2 function call.
With this context, let’s now look at our unit tests. There are two unit tests, one for each
of the two functions. And each one checks that the $location path changes to the correct
URL after the function is called. What is noteworthy is the pre-function call check we
do. In both cases, we can expect that the browser URL is an empty string.
The reason we can do that is because there is no global state in our unit test. If we execute
the first test in a real live application, that changes the value of the browser URL. The
second test would then see that. This makes the order of the tests important, because
something that sets a variable that the other test uses could cause the tests to fail if they
are in a certain order, but pass in another.
110
| Chapter 7: Unit Testing Services and XHRs
www.it-ebooks.info
AngularJS avoids that by getting rid of global state in the unit tests. The $location
service is destroyed and created between our unit tests. All of this happens because we
instantiate our module before each unit test. This is responsible for creating a fresh
version of each of the service that our test uses.
Mocking Out Services
What if we had a service that was really heavy, or we did not want to test the service?
We saw mocked-out versions of $location and $window that the AngularJS mock file
provides. In this section, we see how to create our own mocks. Let’s consider the very
simple ItemService from Chapter 5:
// File: chapter7/notesApp1.js
angular.module('notesApp1', [])
.factory('ItemService', [function() {
var items = [
{id: 1, label: 'Item 0'},
{id: 2, label: 'Item 1'}
];
return {
list: function() {
return items;
},
add: function(item) {
items.push(item);
}
};
}])
.controller('ItemCtrl', ['ItemService', function(ItemService) {
var self = this;
self.items = ItemService.list();
}]);
This code snippet uses the ItemService we defined in Chapter 5, and has a simple
controller that fetches the list of items when it loads. Now for the purpose of our unit
test, we want to mock out ItemService so that we can override the default implemen‐
tation for our unit test. There are two ways to accomplish this.
The first way is to override the service during the unit test, as an inline mock:
// File: chapter7/notesApp1Spec.js
describe('ItemCtrl with inline mock', function() {
beforeEach(module('notesApp1'));
var ctrl, mockService;
beforeEach(module(function($provide) {
mockService = {
list: function() {
Dependency Injection in Our Unit Tests | 111
www.it-ebooks.info
return [{id: 1, label: 'Mock'}];
}
};
$provide.value('ItemService', mockService);
}));
beforeEach(inject(function($controller) {
ctrl = $controller('ItemCtrl');
}));
it('should load mocked out items', function() {
expect(ctrl.items).toEqual([{id: 1, label: 'Mock'}]);
});
});
In this unit test, the start of the test is similar, where we instantiate our module, the
notesApp1. After that, we have another beforeEach, which is where we override the
ItemService with our own mock. We use the module function, but instead of giving it
the name of the module, we give it a function that gets injected with a $provide. This
provider shares its namespace with the modules loaded before. So now we create our
mockService and tell the provider that when any controller or service asks for ItemSer
vice, give it our value. Because we do this after the notesApp1 module is loaded, it
overwrites the original ItemService definition.
The rest of the unit test proceeds the same as before, except we now check that the value
of items in the controller is returned by our mock instead of the original service.
The second option to override services would be at a global level instead of a unit test
level. To decide whether to create the mocks as we did in the previous example using a
local variable and the $provide.value function, or whether to do it globally like An‐
gularJS does it, the question we need to answer is whether or not other tests could reuse
the mock.
The mock we created before would only be usable within this particular describe block.
To change the preceding to be a more reusable, general-purpose mock of the ItemSer
vice, we could do the following:
// File: chapter7/notesApp1-mocks.js
angular.module('notesApp1Mocks', [])
.factory('ItemService', [function() {
return {
list: function() {
return [{id: 1, label: 'Mock'}];
}
};
}]);
112
| Chapter 7: Unit Testing Services and XHRs
www.it-ebooks.info
What we had hardcoded in the mockService has been extracted out into a service with
the same name, but in a different module named notesApp1Mocks. This file will reside
in the test folder, and be included by karma.conf.js, but not in our live application. Our
tests would now change as follows:
// File: chapter7/notesApp1SpecWithMock.js
describe('ItemCtrl With global mock', function() {
var ctrl;
beforeEach(module('notesApp1'));
beforeEach(module('notesApp1Mocks'));
beforeEach(inject(function($controller) {
ctrl = $controller('ItemCtrl');
}));
it('should load mocked out items', function() {
expect(ctrl.items).toEqual([{id: 1, label: 'Mock'}]);
});
});
This ensures that after notesApp1 is loaded, we load the notesApp1Mocks module, which
overrides the ItemService. After that, when our test loads the controller, which then
calls the service, it defers to the mocked-out ItemService that we created.
We can use this approach when we need a global reusable mock, and defer to the
describe-level mock when we need to mock just one particular test.
Spies
But what if we didn’t want to implement an entire mocked-out service? What if we just
wanted to know in the case of ItemService whether or not the list function was called,
and not worry about the actual value from it? For those kinds of cases, we have Jasmine
spies. Spies allow us to hook into certain functions, and check whether they were called,
how many times they were called, what arguments they were called with, and so on.
So let’s see how to change our mock to use spies instead:
// File: chapter7/notesApp1SpecWithSpies.js
describe('ItemCtrl with spies', function() {
beforeEach(module('notesApp1'));
var ctrl, itemService;
beforeEach(inject(function($controller, ItemService) {
spyOn(ItemService, 'list').andCallThrough();
itemService = ItemService;
Dependency Injection in Our Unit Tests | 113
www.it-ebooks.info
ctrl = $controller('ItemCtrl');
}));
it('should load mocked out items', function() {
expect(itemService.list).toHaveBeenCalled();
expect(itemService.list.callCount).toEqual(1);
expect(ctrl.items).toEqual([
{id: 1, label: 'Item 0'},
{id: 2, label: 'Item 1'}
]);
});
});
We call the spyOn function with an object as the first argument, and a string with the
function name that we want to hook on to as the second argument. In this example, we
tell Jasmine to spy on the list function of the ItemService. We also tell it to continue
calling the actual service underneath by calling andCallThrough on the spy. This means
we can use Jasmine to check whether or not the function was called, and have the func‐
tion work as it used to underneath.
This adds a wrapper around the existing ItemService.list function. Jasmine lets the
existing code continue as is while giving us a window into what is happening, and letting
us know whether the right functions were called. The data that is returned is still from
the original service, as we can see in the expectation on the controllers items. Note that
it is recommended that you set up all your mocks and spies before instantiating your
controllers.
What if we wanted to not have the existing method execute as normal? Let’s see how we
might override the method using spies:
// File: chapter7/notesApp1SpecWithSpyReturn.js
describe('ItemCtrl with SpyReturn', function() {
beforeEach(module('notesApp1'));
var ctrl, itemService;
beforeEach(inject(function($controller, ItemService) {
spyOn(ItemService, 'list')
.andReturn([{id: 1, label: 'Mock'}]);
itemService = ItemService;
ctrl = $controller('ItemCtrl');
}));
it('should load mocked out items', function() {
expect(itemService.list).toHaveBeenCalled();
expect(itemService.list.callCount).toEqual(1);
expect(ctrl.items).toEqual([{id: 1, label: 'Mock'}]);
114
| Chapter 7: Unit Testing Services and XHRs
www.it-ebooks.info
});
});
In this example, we override the list method in the ItemService, and replace it with
our Jasmine spy. The spyOn function returns a spy that’s called with the andReturn
function on the spy created by createSpy, and gives it the value to return. Note that we
do this before creating our controller, which is recommended. Then, in our unit test, we
can check if ItemService.list was called, and if it was called once. Also, we specify
that our spy return the value in the controller’s items array (specified with the andRe
turn on the createSpy function).
Unit Testing Server Calls
We covered how to unit test simple services, as well as mock services and functions
depending on our need. With this in our toolbelt, now let’s explore how we might test
controllers and services using the $http service to make server calls.
In unit tests, we focus on testing a single unit of code and checking whether it behaves
correctly under all conditions. In a unit test, we want to mock out the larger system at
play, whether that be a server, other third-party dependencies, the DOM and browser,
or whatever.
With AngularJS, as long as we include the angular-mocks.js file as part of the Karma
configuration, AngularJS takes care of ensuring that when we use the $http service, it
doesn’t actually make server calls. All server calls are intercepted, and we can test them
all within the context of a unit test. Because they are intercepted and mocked out, our
unit tests remain fast and stable.
Let’s take a sample controller that makes server calls using $http, and see how we might
unit test it:
// File: chapter7/serverApp.js
angular.module('serverApp', [])
.controller('MainCtrl', ['$http', function($http) {
var self = this;
self.items = [];
self.errorMessage = '';
$http.get('/api/note').then(function(response) {
self.items = response.data;
}, function(errResponse) {
self.errorMessage = errResponse.data.msg;
});
}]);
In this code snippet, we have a very simple controller, which makes a GET request
to /api/note when it loads, and saves the response into the items array on the controller.
Unit Testing Server Calls | 115
www.it-ebooks.info
In case of an error, it saves the error message on the controller’s instance. Now, let’s see
how we might test this:
// File: chapter7/serverAppSpec.js
describe('MainCtrl Server Calls', function() {
beforeEach(module('serverApp'));
var ctrl, mockBackend;
beforeEach(inject(function($controller, $httpBackend) {
mockBackend = $httpBackend;
mockBackend.expectGET('/api/note')
.respond([{id: 1, label: 'Mock'}]);
ctrl = $controller('MainCtrl');
// At this point, a server request will have been made
}));
it('should load items from server', function() {
// Initially, before the server responds,
// the items should be empty
expect(ctrl.items).toEqual([]);
// Simulate a server response
mockBackend.flush();
expect(ctrl.items).toEqual([{id: 1, label: 'Mock'}]);
});
afterEach(function() {
// Ensure that all expects set on the $httpBackend
// were actually called
mockBackend.verifyNoOutstandingExpectation();
// Ensure that all requests to the server
// have actually responded (using flush())
mockBackend.verifyNoOutstandingRequest();
});
});
To unit test our controller that makes XHR calls, we leverage a service called $httpBack
end. The $http service internally uses the $httpBackend to make the actual XHR re‐
quests. The angular-mocks.js file provides a mock $httpBackend service that prevents
server calls, and gives us hooks to set expectations and trigger responses.
As part of beforeEach, we ask for the $httpBackend service to be injected into the test.
Because our controller makes a server call as part of the loading behavior, it is important
for us to set our expectations on what server calls will be made before the controller is
instantiated.
116
| Chapter 7: Unit Testing Services and XHRs
www.it-ebooks.info
There are two ways to set expectations on what server calls will be made on the
$httpBackend:
expect
The expect function is used when we want to control exactly how many requests
will be made and to what URLs, and then control the response. The expect function
has a series of functions, one for each method of HTTP, such as expectGET or
expectPOST. The first argument to the function is the URL and the second argu‐
ment, if provided, acts as the POST data. So expectGET('/api/notes') in the pre‐
vious example says that there will be a GET request to the given URL. Similarly,
expectPOST('/api/notes', {label: 'Hi'}) tells the service to expect a POST
request, and that the POST data should exactly match what is passed as the second
argument.
when
Similar to expect, when also takes a URL and potential POST data. The syntax is
also exactly the same. The difference is that the when does not care about the order
of requests or how many times the call was made. It simply sees a request and sends
a response. With the expect, a test can fail if the expectation was not satisfied. With
when, even if the test never makes the call, the test will pass.
The difference really comes down to the fact that expect is more fine-grained and sets
expectations. when stubs out the backend (a stub is something that returns the same
response, regardless of the request), allowing it to respond in a consistent manner
without any expectations for any and all requests.
After we use either expect (like expectGET) or when, we can define the response for that
particular server call by chaining the respond function on it. If respond is given one
argument, it is treated as the server response. You can optionally give it two arguments,
in which case the first argument will be the status code, and the second argument will
be the body of the response (like respond(404, {msg: 'Invalid'})). In our example,
we respond with a list of items from the server.
Now, on to our actual unit test. When the controller loads, the items array is initialized
to an empty array. Our first expectation in the test is whether the items array is empty.
If we consider a server request in a real live application, a request is made first, and then
the response comes back at some later point in time. The server requests are asynchro‐
nous in nature. To simulate this, AngularJS gives a flush method on the backend service.
So by default, when a server request happens, AngularJS tracks it against the expecta‐
tions and holds on to the request without returning the response. Then, when you as a
developer finally call flush() on the $httpBackend, AngularJS sends back the responses
for all the requests that the client has received so far.
Unit Testing Server Calls | 117
www.it-ebooks.info
flush allows us to test asynchronous behavior without actually writing asynchronous
tests. $httpBackend.flush() also takes an integer argument, which can tell the mock
backend how many server requests it needs to return. This is useful if we want to check
that the controller makes four server calls, but does some work only after at least three
of them return. In such a case, we can flush the requests one at a time (using $httpBack
end.flush(1)), or flush three of them ($httpBackend.flush(3)) at once.
At this point, now we can check whether the data that the server responded with has
been stored in the right variable in the controller (the items array).
As a good practice, it is recommended that when you write tests using the $httpBack
end service, you add an afterEach block with the two function calls in the previous
code snippet:
• The first function, verifyNoOutstandingExpectations(), checks whether you
specified any expects on the $httpBackend that were not satisfied as part of your
test. So if you added another expectation but the controller never made that server
call, your test fails. This adds a good check to ensure that everything that you ex‐
pected actually happened
• The second function, verifyNoOutstandingRequests(), is to ensure that you fully
tested all the cases. As mentioned earlier, AngularJS splits all server requests into a
request and a response. And we trigger the responses using the flush() function.
verifyNoOutstandingRequests ensures that for each server call made, the re‐
sponse has also been triggered using flush(). If not, the test fails.
Integration-Level Unit Tests
What if we followed the best practices, and didn’t have our $http calls right in our
controller? Instead, we had our $http calls in a NoteService, and our controller dele‐
gated the NoteService to fetch the list of notes.
In such a case, we have two options:
• Option 1 is to write a focused unit test and mock out (or spy on) the NoteSer
vice, and ensure that our controller delegates to the correct APIs on the NoteSer
vice and that the flow and arguments are correct.
• Option 2 is to write an integration-level unit test that only focuses on mocking out
the backend (using $httpBackend) and checking the entire flow.
118
| Chapter 7: Unit Testing Services and XHRs
www.it-ebooks.info
Let’s try our hand at Option 2 with the following code snippet:
// File: chapter7/serverAppWithService.js
angular.module('serverApp2', [])
.controller('MainCtrl', ['NoteService', function(NoteService) {
var self = this;
self.items = [];
self.errorMessage = '';
NoteService.query().then(function(response) {
self.items = response.data;
}, function(errResponse) {
self.errorMessage = errResponse.data.msg;
});
}])
.factory('NoteService', ['$http', function($http) {
return {
query: function() {
return $http.get('/api/note');
}
};
}]);
This example is almost the same as the one in the previous section, except that the $http
call has been extracted into the NoteService service. Functionally, it behaves exactly
the same way.
Now let’s look at how we might test this, while also getting an idea for the error condition
test:
// File: chapter7/serverAppWithServiceSpec.js
describe('Server App Integration', function() {
beforeEach(module('serverApp2'));
var ctrl, mockBackend;
beforeEach(inject(function($controller, $httpBackend) {
mockBackend = $httpBackend;
mockBackend.expectGET('/api/note')
.respond(404, {msg: 'Not Found'});
ctrl = $controller('MainCtrl');
// At this point, a server request will have been made
}));
it('should handle error while loading items', function() {
// Initially, before the server responds,
// the items should be empty
expect(ctrl.items).toEqual([]);
// Simulate a server response
Unit Testing Server Calls | 119
www.it-ebooks.info
mockBackend.flush();
// No items from server, only an error
// So items should still be empty
expect(ctrl.items).toEqual([]);
// and check the error message
expect(ctrl.errorMessage).toEqual('Not Found');
});
afterEach(function() {
// Ensure that all expects set on the $httpBackend
// were actually called
mockBackend.verifyNoOutstandingExpectation();
// Ensure that all requests to the server
// have actually responded (using flush())
mockBackend.verifyNoOutstandingRequest();
});
});
In this test, very little has changed even though our code has added a new service and
extracted it out. For the unit test, we are only ensuring that when the controller loads,
it makes a server call to /api/notes. We don’t care whether it is through NoteService or
directly. This makes it much more of an integration test, where it is independent of the
underlying implementation.
Also, we are now changing the server to respond with a 404. Under such a condition,
we expect the items array to still be empty, but now the errorMessage variable should
be updated in the controller with the server’s response. We added an expect to make
sure this happens. The afterEach blocks remains as it was.
Conclusion
We expanded our understanding of unit testing controllers, and walked through how
to unit test controllers that depend on built-in AngularJS services (like $location and
$window). After that, we created our own services and learned how to unit test those as
well. In both cases, it was as simple as asking for the service to be injected into our test,
and then interacting with that as need be.
We then covered unit testing XHRs, using the mocked-out $httpBackend service that
the angular-mocks.js file provides. We saw how to set expectations, and handle the
asynchronous behavior of the server calls using the flush() method. We also covered
how to handle and test both the error and the success cases.
In the next chapter, we will look at AngularJS filters. We will see how to apply common
built-in AngularJS filters, as well as create new filters to perform our own formatting
tasks.
120
| Chapter 7: Unit Testing Services and XHRs
www.it-ebooks.info
CHAPTER 8
Working with Filters
In the previous few chapters, we have explored two of the four cornerstones of AngularJS
applications: controllers and services. With controllers, we looked at how to get the data
we want out into the UI, and how to handle simple styling and presentation logic. We
used services to create common business logic, and a layer that would be common across
all our controllers.
In this chapter, we work with AngularJS filters. By the end of the chapter, we will get a
sense of how and when to use AngularJS filters, as well as how to create a very simple
but useful custom AngularJS filter. We end the chapter with a section on best practices
and how to get the most out of AngularJS filters.
What Are AngularJS Filters?
AngularJS filters are used to process data and format values to present to the user. They
are applied on expressions in our HTML, or directly on data in our controllers and
services. Mostly, they are used as that final level of formatting to convert data from the
way it is stored to a user-readable format. Some common examples where we would use
filters are to take a timestamp and make it human-readable, or to add the currency
symbol to a number.
Another feature of AngularJS filters, when they are used in the view, is that they give us
dynamic, on-the-fly data that doesn’t need to be stored. When we apply filters in the
HTML, the filtered values are shown to the user but do not modify the original value
on which they are applied.
Let’s look at some common AngularJS filters that come with the core AngularJS codebase
and how we might use them under various scenarios.
121
www.it-ebooks.info
Using AngularJS Filters
AngularJS has some built-in filters to work with dates, numbers, strings, and arrays. A
common use case for filters is to use them directly in the view as a last level for formatting
of the data that the user sees. Let’s look at the example to see some common filters in
action:
<!-- File: chapter8/filter-example-1.html -->
<html>
<head>
<title>Filters in Action</title>
</head>
<body ng-app="filtersApp">
<div ng-controller="FilterCtrl as ctrl">
<div>
Amount as a number: {{ctrl.amount | number}}
</div>
<div>
Total Cost as a currency: {{ctrl.totalCost | currency}}
</div>
<div>
Total Cost in INR: {{ctrl.totalCost | currency:'INR '}}
</div>
<div>
Shouting the name: {{ctrl.name | uppercase}}
</div>
<div>
Whispering the name: {{ctrl.name | lowercase}}
</div>
<div>
Start Time: {{ctrl.startTime | date:'medium'}}
</div>
</div>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('filtersApp', [])
.controller('FilterCtrl', [function() {
this.amount = 1024;
this.totalCost = 4906;
this.name = 'Shyam Seshadri';
this.startTime = new Date().getTime();
}]);
</script>
</body>
</html>
This example code translates into Figure 8-1.
122
| Chapter 8: Working with Filters
www.it-ebooks.info
Figure 8-1. AngularJS filter output
We will go over the AngularJS filters one by one, but before that, let’s talk about their
usage and syntax. The general syntax to use filters is to use the Unix syntax of piping
the result of one expression to another. That is:
{{expression | filter}}
The filter will take the value of the expression (a string, number, or array) and convert
it into some other form. For example, the currency filter used in the previous code
snippet takes the totalCost number and converts it into a string, with commas, deci‐
mals, and the currency symbol added. The uppercase and lowercase filters take the
string name and convert it into uppercase and lowercase, respectively.
Do note that when we say something like:
{{ctrl.name | lowercase}}
we are formatting data on the fly. This means that the value of
ctrl.name does not change, whereas the user still sees the final low‐
ercase result.
We can also chain multiple filters together by piping one filter after another. The syntax
would be:
{{expression | filter1 | filter2}}
For example, say we want to take our name variable from the previous controller, convert
it to lowercase, and display only the first five letters. We could accomplish that like this:
{{ctrl.name | lowercase | limitTo:5}}
Each filter takes the value from the previous expression and applies its logic on it. In
this example, the name would first be lowercased, and then the lowercase name would
be provided to the limitTo filter. The limitTo filter would just return the first five
characters from the string, thus returning “shyam” to the HTML. As you can see, we
can pass arguments to filters as well, which we use to tell the limitTo filter how many
characters to limit the string to.
What Are AngularJS Filters?
www.it-ebooks.info
| 123
Common AngularJS Filters
Let’s go over each of the filters we mentioned in passing, as well as some additional ones
with some examples to see how and when to use each one. We’ll talk about the available
filters before giving a comprehensive example that demos all of them in a single app:
currency
The currency filter formats a given number as currency with the commas, decimals,
and currency symbol added as needed. The filter takes an optional currency symbol
as the second argument; if none exists, it takes the default symbol for the current
browser.
number
The number filter takes a number and converts it to a human-readable string with
comma separation. The number filter also takes an optional decimal size that tells
it how many digits to keep after the decimal point.
lowercase
A very simple string filter that takes any string and converts all the characters to
lowercase.
uppercase
A very simple string filter that takes any string and converts all the characters to
uppercase.
json
The json filter is a great tool for debugging, or for any time we need to display the
contents of a JSON object or an array in the UI. It takes a JSON object or array (or
even primitives) and displays it as a string in the UI.
date
The date filter is a customizable and powerful filter that takes a date object or a
long timestamp and displays it as a human-readable string in the UI. It can take a
user-defined format or one of the built-in short, medium, or long formats. The
detailed documentation for various formatting options is available in the Date Filter
API page.
The following example demonstrates these filters used in combination with strings and
numbers:
<!-- File: chapter8/filter-number-string.html -->
<html>
<head>
<title>Filters in Action</title>
</head>
<body ng-app="filtersApp">
<ul ng-controller="FilterCtrl as ctrl">
124
| Chapter 8: Working with Filters
www.it-ebooks.info
<li>
Amount - {{ctrl.amount}}
</li>
<li>
Amount - Default Currency: {{ctrl.amount | currency}}
</li>
<li>
<!-- Using the English pound sign -->
Amount - INR Currency: {{ctrl.amount | currency:'£ '}}
</li>
<li>
Amount - Number: {{ctrl.amount | number}}
</li>
<li>
Amount - No. with 4 decimals: {{ctrl.amount | number:4}}
</li>
<li>
Name with no filters: {{ctrl.name}}
</li>
<li>
Name - lowercase filter: {{ctrl.name | lowercase}}
</li>
<li>
Name - uppercase filter: {{ctrl.name | uppercase}}
</li>
<li>
The JSON Filter: {{ctrl.obj | json}}
</li>
<li>
Timestamp: {{ctrl.startTime}}
</li>
<li>
Default Date filter: {{ctrl.startTime | date}}
</li>
<li>
Medium Date filter: {{ctrl.startTime | date:'medium'}}
</li>
<li>
Custom Date filter: {{ctrl.startTime | date:'M/dd, yyyy'}}
</li>
</ul>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('filtersApp', [])
What Are AngularJS Filters?
www.it-ebooks.info
| 125
.controller('FilterCtrl', [function() {
this.amount = 1024;
this.name = 'Shyam Seshadri';
this.obj = {test: 'value', num: 123};
this.startTime = new Date().getTime();
}]);
</script>
</body>
</html>
In this code snippet, four values are defined in the controller:
• A number, amount
• A string, name
• A JSON object, obj
• A timestamp, startTime
Then, in the HTML (shown in Figure 8-2) we have the following bindings and filters,
in order:
1. The amount variable itself.
2. The amount variable, using the default currency filter. This uses the current browser
to grab the currency symbol.
3. The amount variable using the currency filter with a defined symbol (in this case,
the English pound sign).
4. The amount variable using the default number filter, which is like the currency filter
but without decimals and the currency symbol by default.
5. The amount variable using the number filter, and forcing four digits after the decimal
point.
6. The name variable itself.
7. The name variable using the lowercase filter, which ensures that the output becomes
"shyam seshadri".
8. The name variable using the uppercase filter, which ensures that the output becomes
"SHYAM SESHADRI".
9. The obj variable, printed as a string {"test": "value", "num": 123}
10. The timestamp used for the date filters.
11. The timestamp used with the default date filter (which prints something like “Jan
3, 2007”: the medium date format).
12. The timestamp with the medium date filter, which prints the medium date in the
filter, along with the time (something like “Jan 3, 2007 12:04:45 pm”).
126
| Chapter 8: Working with Filters
www.it-ebooks.info
13. The timestamp with a custom date filter specified using a date format (something
like 1/23, 2014).
Figure 8-2. Screenshot of number and string filters
Next, let’s look at the filters that work mostly with arrays and give ways to slice and dice
and change the order as per our needs:
limitTo
The limitTo is a simple AngularJS filter that takes either a string (as we saw in the
example on chaining) or an array and returns a subset from the beginning or the
end of the array, depending on the argument passed to it. If the limitTo is given
only a number (say, 3), it returns only that many elements from the array or char‐
acters from the string (in this case, 3 again). If it is a negative number, it picks up
those elements or characters from the end of the array.
orderBy
One of the two more complicated filters (the other being filter, which we cover
next), orderBy allows us to take an array and order it by a predicate expression (or
a series of predicate expressons). It also takes a second optional Boolean argument,
which decides whether or not the sorted array is reversed. The simplest form of a
predicate expression is a string, which is the name of the field (the key of each object)
to order the array by, with an optional + or – sign before the field name to decide
whether to sort ascending or descending by the field. The predicate expression can
also be passed a function, in which case the return value of the function will be used
(with simple <, >, = comparisons) to decide the order. Finally, the predicate ex‐
pression can be an array, in which case each element of the array is either a string
What Are AngularJS Filters?
www.it-ebooks.info
| 127
or a function. AngularJS will then sort it by the first element of the array, and keep
cascading to the next element if it is equal.
filter
By far one of the most flexible filters in AngularJS is filter (confused yet?). While
named slightly confusingly, the filter filter in AngularJS is used to filter an array
based on predicates or functions, and decide which elements of an array are in‐
cluded. This filter is most commonly used along with the ng-repeat to do dynamic
filtering of an array. The expression to filter the array can be one of the following:
string
If provided a string expression, AngularJS will look for the string in the keys
of each object of the array, and if it is found, the element is included. The string
can optionally be prefixed with an ! to negate the match.
object
A pattern object can also be provided, in which case AngularJS takes each key
of the object and makes sure that its value is present in the corresponding key
of each object of the array. For example, an object expression like {size:
"M"} would check each item of the array and ensure that the objects have a key
called size and that they contain the letter “M” (not necessarily an exact
match).
function
The most flexible and powerful of the options, the filter can take a function to
implement arbitrary and custom filters. The function gets called with each item
of the array, and uses the return value of the function to decide whether to
include the item in the end result. Any item that returns a false gets dropped
from the result.
Let’s use an example to demonstrate how these might work:
<!-- File: chapter8/filter-arrays.html -->
<html>
<head>
<title>Filters in Action</title>
</head>
<body ng-app="filtersApp">
<div ng-controller="FilterCtrl as ctrl">
<button ng-click="ctrl.currentFilter = 'string'">
Filter with String
</button>
<button ng-click="ctrl.currentFilter = 'object'">
Filter with Object
</button>
<button ng-click="ctrl.currentFilter = 'function'">
128
| Chapter 8: Working with Filters
www.it-ebooks.info
Filter with Function
</button>
Filter Text
<input type="text"
ng-model="ctrl.filterOptions['string']">
Show Done Only
<input type="checkbox"
ng-model="ctrl.filterOptions['object'].done">
<ul>
<li ng-repeat="note in ctrl.notes |
filter:ctrl.filterOptions[ctrl.currentFilter] |
orderBy:ctrl.sortOrder |
limitTo:5">
{{note.label}} - {{note.type}} - {{note.done}}
</li>
</ul>
</div>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('filtersApp', [])
.controller('FilterCtrl', [function() {
this.notes = [
{label: 'FC Todo', type: 'chore', done: false},
{label: 'FT Todo', type: 'task', done: false},
{label: 'FF Todo', type: 'fun', done: true},
{label: 'SC Todo', type: 'chore', done: false},
{label: 'ST Todo', type: 'task', done: true},
{label: 'SF Todo', type: 'fun', done: true},
{label: 'TC Todo', type: 'chore', done: false},
{label: 'TT Todo', type: 'task', done: false},
{label: 'TF Todo', type: 'fun', done: false}
];
this.sortOrder = ['+type', '-label'];
this.filterOptions = {
"string": '',
"object": {done: false, label: 'C'},
"function": function(note) {
return note.type === 'task' && note.done === false;
}
};
this.currentFilter = 'string';
}]);
</script>
</body>
</html>
What Are AngularJS Filters?
www.it-ebooks.info
| 129
In this example, we are using all the array-related filters in one example. We have an
array of notes, with three keys (label, type, and done). We also define an array of
sorting predicates, sortOrder, to tell AngularJS to first sort by the type, and if they are
equal, to sort in reverse by label.
In the HTML, we are sorting by sortOrder and limiting the results to five elements.
Before either of these, we are filtering the array by our filterOptions. By default, we
are using the string filter, which is bound to the text box. If we type in anything, it will
match against all the fields in each note and display only the items that match. Clicking
the buttons will switch our filtering mode from string to object or function.
The object filter shows all notes that are not done, and which have the character C in
the label. The checkbox in the UI allows us to toggle the done filter to show only the
done notes, or only the notes that are not done.
The function filter only shows notes that are tasks and not done.
In a real application, we could bind checkboxes, select boxes, and text boxes to various
fields of an object (using ng-model) and use the object to filter the list dynamically. The
function filter could be expanded and made more complex based on the business logic.
The beauty of the filter filter is that it is dynamic when used in the HTML directly, so
the minute the underlying model changes, the entire list gets filtered automatically.
Using Filters in Controllers and Services
Filters are well and great in the HTML and UI, but what if we need to apply these
transformations to our controller or service? Thankfully, AngularJS allows us to use the
filters wherever we want or need through the power of Dependency Injection. So
without ever needing to access the DOM or the UI, we can use the business logic of the
filter right in our JavaScript code.
Any filter, whether built-in or our own, can be injected into any service or controller by
affixing the word “Filter” at the end of the name of the filter, and asking it to be injected.
For example, if we need the currency filter in our controller, we can do something like
this:
angular.module('myModule', [])
.controller('MyCtrl', ['currencyFilter',
function(currencyFilter) {
}]);
Similarly, the number filter becomes numberFilter and filter of course becomes the
convoluted filterFilter. Attaching the word “Filter” after any AngularJS filter allows
us to inject it into our controllers or services.
Now, in the HTML, we use the pipe syntax to give it some input to work with, followed
by optional arguments. When we get a handle on it in our controller or service, we get
130
| Chapter 8: Working with Filters
www.it-ebooks.info
a function. The first argument to the function is the value the filter needs to act upon:
a string, number, or array. All additional parameters are the arguments we mentioned
earlier, in order as needed.
So if we wanted to filter our array self.notes using the filterFilter with just a string,
we could do something like:
self.filteredArray = filterFilter(self.notes, 'ch');
There are three main things to note:
• The first argument to the filter is the value it needs to act upon.
• Further arguments are the arguments that the filter needs (optional for some), in
the order mentioned in the documentation.
• The return value of the filter is the final output that we need.
Creating AngularJS Filters
We saw how we can use some of the existing built-in AngularJS filters, but what if they
are not enough? The date filter might be good, but we want our own formatting and
behavior. We might want a filter for localization. In this section, we will go about creating
our own filter.
We are going to write a very simple filter called timeAgo. In production, we might use
something like MomentJS, but here, we are going to craft a very simplistic one ourselves.
All we want to do is take a timestamp and display in the UI messages like “seconds ago,”
“minutes ago,” “days ago,” and “months ago.” We are displaying only the message, without
any numbers. So regardless of whether it was 5 or 15 seconds ago, the message would
read “seconds ago.” How might we go about doing this?
<!-- File: chapter8/custom-filters.html -->
<html>
<head>
<title>Custom Filters in Action</title>
</head>
<body ng-app="filtersApp">
<div ng-controller="FilterCtrl as ctrl">
<div>
Start Time (Timestamp): {{ctrl.startTime}}
</div>
<div>
Start Time (DateTime): {{ctrl.startTime | date:'medium'}}
</div>
<div>
Start Time (Our filter): {{ctrl.startTime | timeAgo}}
</div>
Creating AngularJS Filters | 131
www.it-ebooks.info
<div>
someTimeAgo : {{ctrl.someTimeAgo | date:'short'}}
</div>
<div>
someTimeAgo (Our filter): {{ctrl.someTimeAgo | timeAgo}}
</div>
</div>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script type="text/javascript">
angular.module('filtersApp', [])
.controller('FilterCtrl', [function() {
this.startTime = new Date().getTime();
this.someTimeAgo = new Date().getTime() (1000 * 60 * 60 * 4);
}])
.filter('timeAgo', [function() {
var ONE_MINUTE = 1000 * 60;
var ONE_HOUR = ONE_MINUTE * 60;
var ONE_DAY = ONE_HOUR * 24;
var ONE_MONTH = ONE_DAY * 30;
return function(ts) {
var currentTime = new Date().getTime();
var diff = currentTime - ts;
if (diff < ONE_MINUTE) {
return 'seconds ago';
} else if (diff < ONE_HOUR) {
return 'minutes ago';
} else if (diff < ONE_DAY) {
return 'hours ago';
} else if (diff < ONE_MONTH) {
return 'days ago';
} else {
return 'months ago';
}
};
}]);
</script>
</body>
</html>
In this example, we defined our own custom filter called timeAgo. We define a filter in
a very similar manner to controllers and services, and we can also inject any services
that our filter might depend on into it.
Every filter returns a function, which is what gets called for every usage of the filter. This
function gets called with the value that the filter is being applied on. In this case, it is
132
| Chapter 8: Working with Filters
www.it-ebooks.info
the timestamp as a number. The filter can then act upon this value, and slice and dice
it in whichever way it wants. We just take the difference of the timestamp and the current
time, and then return a string based on the difference.
If we want to take optional arguments, like the currency filter or the number filter, we
just have to add them as additional parameters to the function we return. Suppose we
want to take a Boolean to say only show minutes, or ignoreSeconds, then we could
change the return to something like this:
return function(ts, ignoreSeconds) {
This would then be passed in from the HTML as follows, with the optional argument
set to true:
{{ctrl.startTime | timeAgo:true}}
If you need multiple arguments, keep adding them and pass them in the same order in
the HTML:
return function(ts, arg1, arg2, arg3) {
and:
{{ctrl.startTime | timeAgo:arg1:arg2:arg3}}
We will see how to test these filters in Chapter 9.
Things to Remember About Filters
We saw how to use existing filters in AngularJS, as well as how to customize them and
pass them arguments. We then saw how we could use them in controllers and services,
before finally creating our own custom filter. Now in this section, we cover some best
practices and things to remember about working with filters in AngularJS:
View filters are executed every digest cycle
The first and foremost thing we should know and remember about AngularJS filters
is that if we are using them directly in the view (which is quite often), they are reevaluated every time a digest cycle happens (we cover this in depth in “The Digest
Cycle” on page 206). Therefore, as the data we are working on grows, we need to be
aware of the extra computation we might be performing when we extensively use
filters across our UI.
Filters should be blazingly fast
Because of the previous point, whenever we write our own filters, we should write
them in such a way that makes their execution blazingly fast. Ideally, our filter
functions should be capable of executing multiple times in milliseconds. So things
like DOM manipulation, asynchronous calls, and other slow activities should be
avoided when we write filters.
Things to Remember About Filters | 133
www.it-ebooks.info
Prefer filters in services and controllers for optimization
If you are working with large and complex arrays and data structures but still want
to leverage filters because they are contained, modular, and reusable, then consider
using filters directly in the controller or service. Inject the filter into your controller
and service, and trigger the filter function as needed (like we saw in “Using Filters
in Controllers and Services” on page 130). This will lead to a snappier and responsive
UI, and is recommended over directly applying filters to large arrays. You can also
prevent the filter from executing even when it has not changed, which will save
some CPU cycles.
Conclusion
In this chapter, we explored AngularJS filters, which are great for formatting and con‐
verting data and values from one format to another. We then looked at the general way
of using and chaining filters before digging into each of the built-in core AngularJS
filters and the variety of ways to use and customize them. We then created our own
timeAgo filter to display how long ago some time was. Finally, we talked about some
general best practices and things to consider when working with or creating our own
filters.
In the next chapter, we will cover how to unit test these filters that we have created.
134
| Chapter 8: Working with Filters
www.it-ebooks.info
CHAPTER 9
Unit Testing Filters
In Chapter 8, we covered how we could use existing AngularJS filters, as well as create
our own filters. Filters are a great way of separating out common formatting and con‐
version logic into separate reusable components. In previous chapters, we saw how easy
it was to create our own controllers and services, as well as unit test them.
In this chapter, we will work with the timeAgo filter we created in the previous chapter.
We will first add complexity by adding optional parameters to the filter. Then we will
unit test it step by step. By the end of the chapter, we will have Jasmine unit tests for all
possible cases of our timeAgo filter.
The Filter Under Test
Let’s use the timeAgo filter from “Creating AngularJS Filters” on page 131 as a base, and
then add an optional argument to decide whether it should show the “seconds ago”
message, or if it should start from the “minutes ago” message level only:
// File: chapter9/timeAgoFilter.js
angular.module('filtersApp', [])
.filter('timeAgo', [function() {
var ONE_MINUTE = 1000 * 60;
var ONE_HOUR = ONE_MINUTE * 60;
var ONE_DAY = ONE_HOUR * 24;
var ONE_MONTH = ONE_DAY * 30;
return function(ts, optShowSecondsMessage) {
if (optShowSecondsMessage !== false) {
optShowSecondsMessage = true;
}
var currentTime = new Date().getTime();
var diff = currentTime - ts;
if (diff < ONE_MINUTE && optShowSecondsMessage) {
135
www.it-ebooks.info
return 'seconds ago';
} else if (diff < ONE_HOUR) {
return 'minutes ago';
} else if (diff < ONE_DAY) {
return 'hours ago';
} else if (diff < ONE_MONTH) {
return 'days ago';
} else {
return 'months ago';
}
};
}]);
The timeAgo filter is almost unchanged, but now has a configuration option to allow
the user to decide if he wants the messages to start at "seconds ago" or from "minutes
ago" only. The default shows the seconds message. One might use the above filter as
follows:
{{ myCtrl.ts | timeAgo }}
If we decided to only show messages from minutes, we might use it as follows with the
optional argument set to false:
{{ myCtrl.ts | timeAgo:false }}
Testing the timeAgo Filter
We saw in Chapter 8 how to create filters, and how to use them in HTML, controllers,
and services. In our unit tests, we’ll use the same flow as we would in our controllers
(i.e., we will inject our filter into our unit test, and then execute them directly as func‐
tions). We can then check the return values to see if they are executing correctly and
our logic is as expected:
// File: chapter9/timeAgoFilterSpec.js
describe('timeAgo Filter', function() {
beforeEach(module('filtersApp'));
var filter;
beforeEach(inject(function(timeAgoFilter) {
filter = timeAgoFilter;
}));
it('should respond based on timestamp', function() {
// The presence of new Date().getTime() makes it slightly
// hard to unit test deterministicly.
// Ideally, we would inject a dateProvider into the timeAgo
// filter, but we are trying to keep it simple here.
// So we will assume that our tests are fast enough to
// execute in mere milliseconds.
var currentTime = new Date().getTime();
136
| Chapter 9: Unit Testing Filters
www.it-ebooks.info
currentTime -= 10000;
expect(filter(currentTime)).toEqual('seconds ago');
var fewMinutesAgo = currentTime - 1000 * 60;
expect(filter(fewMinutesAgo)).toEqual('minutes ago');
var fewHoursAgo = currentTime - 1000 * 60 * 68;
expect(filter(fewHoursAgo)).toEqual('hours ago');
var fewDaysAgo = currentTime - 1000 * 60 * 60 * 26;
expect(filter(fewDaysAgo)).toEqual('days ago');
var fewMonthsAgo = currentTime - 1000 * 60 * 60 * 24 * 32;
expect(filter(fewMonthsAgo)).toEqual('months ago');
});
});
This example test is nothing very new or out of the ordinary. As part of beforeEach, we
instantiate our module and then inject our filter into the test (using timeAgoFilter;
don’t forget that AngularJS automatically adds the word Filter after the filter). We then
write our unit test, where we directly call the filter with the value it needs to filter. In
this case, we create the current timestamp and then modify it slightly so that we can hit
each of the if conditions in the filter.
This of course tests the filter without the optional argument. How would we modify
this to test with the additional Boolean?
// File: chapter9/timeAgoFilterOptionalArgumentSpec.js
describe('timeAgo Filter', function() {
beforeEach(module('filtersApp'));
var filter;
beforeEach(inject(function(timeAgoFilter) {
filter = timeAgoFilter;
}));
it('should respond based on timestamp', function() {
// The presence of new Date().getTime() makes it slightly
// hard to unit test deterministicly.
// Ideally, we would inject a dateProvider into the timeAgo
// filter, but we are trying to keep it simple here.
// So we will assume that our tests are fast enough to
// execute in mere milliseconds.
var currentTime = new Date().getTime();
currentTime -= 10000;
expect(filter(currentTime, false)).toEqual('minutes ago');
var fewMinutesAgo = currentTime - 1000 * 60;
expect(filter(fewMinutesAgo, false)).toEqual('minutes ago');
var fewHoursAgo = currentTime - 1000 * 60 * 68;
expect(filter(fewHoursAgo, false)).toEqual('hours ago');
var fewDaysAgo = currentTime - 1000 * 60 * 60 * 26;
expect(filter(fewDaysAgo, false)).toEqual('days ago');
var fewMonthsAgo = currentTime - 1000 * 60 * 60 * 24 * 32;
expect(filter(fewMonthsAgo, false)).toEqual('months ago');
Testing the timeAgo Filter | 137
www.it-ebooks.info
});
});
We can pass optional or other arguments to the filter as additional parameters to the
filter function. In this case, we pass false to tell the filter not to show the seconds
message. Our test changes minutely, such that both the currentTime and the fewMinu
tesAgo conditions return the "minutes ago" string as compared to "seconds ago" and
"minutes ago" previously.
Conclusion
Unit testing filters is quite simple, and simply requires us to inject the filter as we would
any other service dependency. After that, it’s a matter of calling the filter with various
arguments and seeing if it performs as expected under all the conditions.
In the next chapter, we will look at how AngularJS simplifies routing and allows us to
declaratively set up various routes in our application. We will use the optional ngRoute
module, as well as see how we can use it to perform access control in our application.
138
| Chapter 9: Unit Testing Filters
www.it-ebooks.info
CHAPTER 10
Routing Using ngRoute
Until this point, we have dealt with various parts of AngularJS, including controllers,
services, and filters. But we have not yet moved beyond having just one HTML template
that changes behavior depending on the service or the controller. In a real Single-Page
Application, we would usually have multiple views that would be loaded when the user
clicks certain links or goes to a URL in the browser. Replicating that in a pure JavaScript
framework is difficult, because implementing routing always involves:
• Creating a state machine
• Adding and removing items from the browser’s history
• Loading and unloading templates and relevant JS as the state changes
• Handling the various idiosyncrasies across different browsers
More often than not, these get wrapped into reusable plugins or reimplemented from
scratch. And we as developers are left hunting across the codebase to figure out how it
is implemented and how to deal with it.
AngularJS provides us with an optional module called ngRoute, which can be used to
do routing in an AngularJS application. Following the AngularJS philosophy, routing
in AngularJS is declarative, so all routes are defined in a single configuration section
where we can specify what the route is and what AngularJS needs to do when that route
is encountered.
In this chapter, we will implement our own multiview AngularJS application with dif‐
ferent routes while getting a detailed look at the various options that can be used to
configure AngularJS routing. We will also dive deep into the concept of resolve, and
add a page that can only be accessed by certain users under certain conditions. Finally,
we will look at some other alternatives that can be used for routing in AngularJS, like
ui-router.
139
www.it-ebooks.info
Routing in a Single-Page Application
First of all, when we talk about routing in a Single-Page Application, we are not talking
standard URLs, but what we call hashbang URLs. For example, a traditional URL might
look something like http://www.myawesomeapp.com/first/page. In a Single-Page Ap‐
plication, it would usually look like http://www.myawesomeapp.com/#/first/page or
http://www.myawesomeapp.com/#!/first/page.
This is because the browser treats URLs with hashes differently than URLs without.
When the browser sees http://www.myawesomeapp.com/first/page, it makes a server
request to http://www.myawesomeapp.com/first/page to fetch the relevant HTML and
JavaScript for that particular URL. And when the user navigates from there to, say, http://
www.myawesomeapp.com/second/page, it makes a full request to the server again to
fetch the entire HTML contents.
In a Single-Page Application, we want to avoid this and prevent a full page reload. We
only want to load the relevant data and HTML snippet instead of fetching the entire
HTML again and again, especially if most of it does not change between pages. When
the browser encounters a URL like http://www.myawesomeapp.com/#/first/page or
http://www.myawesomeapp.com/#!/first/page, it makes a server request to http://
www.myawesomeapp.com/. Any URL fragment after the # sign gets ignored by the
browser in the server call. It falls upon the client to then take that part of the URL and
deal with it. When the user navigates from http://www.myawesomeapp.com/#/first/
page to http://www.myawesomeapp.com/#/second/page, the browser does not make any
additional requests. There is no page reload happening. This flow is illustrated in
Figure 10-1.
Thus, Single-Page Applications take advantage of this fact and use the hash fragment
(the URL after the hash) to handle navigation. When the hash fragment changes, the
JavaScript responds and loads only the relevant data and HTML instead of reloading
the entire HTML. This makes the application faster and snappier because less data is
fetched from the server.
AngularJS leverages hash URLs for routing, so all AngularJS routes that we define will
be hash URLs. So if we defined a /first route, the /first would be added after the #
in the URL.
140
| Chapter 10: Routing Using ngRoute
www.it-ebooks.info
Figure 10-1. Flow of normal URLs versus hash URLs
Using ngRoute
AngularJS routing used to be part of the core AngularJS library before being split off
into an optional module that needs to be included if it is being used. This was done
because there were a lot of open source alternatives to routing that started being used
heavily. With AngularJS version 1.2, routing became an optional module for AngularJS.
To use AngularJS’s routing module (or technically any other routing module that follows
the AngularJS routing paradigm), the steps are as follows:
1. Include the optional module source code in the application’s HTML. Most of the
time, it’s as simple as:
<script type="text/javascript" src="/path/to/angular-route.min.js"></script>
2. Include the module as a dependency of our main AngularJS app module, like so:
angular.module("myApp", ["ngRoute"])
3. Mark which section of the page AngularJS should change when the route changes.
With the ngRoute module, this is done using the ng-view directive in the HTML.
4. Define our routes in the config section (which we saw in Chapter 6 while config‐
uring the $http service) using the $routeProvider service.
Using ngRoute | 141
www.it-ebooks.info
Let’s look at a very simple example that loads two different templates when the route
changes:
<!-- File: chapter10/simple-routing.html -->
<html>
<head>
<title>AngularJS Routing</title>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular-route.js">
</script>
</head>
<body ng-app="routingApp">
<h2>AngularJS Routing Application</h2>
<ul>
<li><a href="#/">Default Route</a></li>
<li><a href="#/second">Second Route</a></li>
<li><a href="#/asdasdasd">Nonexistent Route</a></li>
</ul>
<div ng-view></div>
<script type="text/javascript">
angular.module('routingApp', ['ngRoute'])
.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/', {
template: '<h5>This is the default route</h5>'
})
.when('/second', {
template: '<h5>This is the second route</h5>'
})
.otherwise({redirectTo: '/'});
}]);
</script>
</body>
</html>
This example shows a very simple AngularJS application that enables routing. As already
mentioned, before the code snippet, we do the following:
1. Include the angular-route.js file after angular.js is loaded to make the ngRoute
module available.
2. We mark in our HTML where we want the routing to take effect with the ng-view
directive.
142
| Chapter 10: Routing Using ngRoute
www.it-ebooks.info
3. When we create our module, we specify that it depends on the ngRoute module
(using angular.module("routingApp", ["ngRoute"])).
4. We then define our routes in AngularJS’s config section using the $routeProvider.
5. Routing inside this application can be done via simple anchor tags (like the ones
we have) or by manually editing the URL in the browser. In fact, try it out, and see
if you can directly get to the second route via a URL.
6. It takes care of the browser history, so you can actually use back and forward buttons
in your browser to navigate within the application.
The $routeProvider allows us to define our routes in one place using the when()
function. The when function takes two arguments:
• The first is a URL or a URL regex that specifies when this particular route is
applicable.
• The second is a configuration object that specifies what needs to happen when the
particular route is encountered.
We keep the previous example simple by telling AngularJS to load only a certain HTML
template that we specify inline. Both of these are very simple, but could include Angu‐
larJS bindings and all the other components we’ve seen so far in this book. We will cover
what else we can do with routing in the following section.
We also call an otherwise function on the $routeProvider that specifies what Angu‐
larJS needs to do if the user tries to go to a URL that is not specified in the configuration.
That is, if the user tries to go to /#/asdasdsa, it redirects the user to /, which loads the
default route template. Without the otherwise, the user would see an empty page be‐
cause AngularJS does not know what template or HTML to load for that particular URL.
Routing Options
In the previous section, we saw how to define a very simple AngularJS application that
leverages routing. We simply loaded different templates for different routes, and nothing
else. The AngularJS route definition allows us to define more complex templates. The
$routeProvider.when function takes a URL or URL regular expression as the first
argument, and the route configuration object as the second. The syntax is as follows:
$routeProvider.when(url, {
template: string,
templateUrl: string,
controller: string, function or array,
controllerAs: string,
resolve: object<key, function>
});
Routing Options
www.it-ebooks.info
| 143
The following are the options we can specify and define when specifying route defini‐
tions with the $routeProvider service:
url
This is the first argument passed to the $routeProvider when function. This speci‐
fies the URL (or URLs, in case it is a regular expression) for which the route con‐
figuration must be triggered. It also allows us to specify variables in the route that
could be used to have the ID or relevant information needed for the page. For
example, valid routes are /list, /recipe/:recipeId, and so on. In the case of the latter,
it tells the $routeProvider that the URL will have some variable content after /
recipe/, and the value of that needs to be picked up and passed to the controller
using the $routeParams service. We explore that in the next section.
template
In cases where the HTML to be displayed is not very large, it can directly be inlined
as a string as part of the route configuration object that is passed as the second
argument to the $routeProvider.when function. AngularJS directly inserts this
template HTML into the ng-view directive.
templateUrl
Often, the HTML for individual views will be significantly complex and large. In
these cases, we can extract the HTML into separate files, and give the URL to the
HTML file as the templateUrl. AngularJS loads the HTML file from the server
when it needs to display the particular route. Future requests for that template are
served from a local cache to prevent repeated calls to the server. An example of this
follows:
$routeProvider.when('/test', {
templateUrl: 'views/test.html',
});
This fetches views/test.html from the server and loads it into the ng-view.
controller
There are two ways in which we can define the controller for a particular route.
This is an optional argument in the $routeProvider.when definition, in case we
have not directly defined the controller in the HTML using the ng-controller
directive. If the controller has already been declared using the angularApp.con
troller("MyCtrl") syntax, we can specify the name of the controller as a string.
The controller key can also use the ng-controller’s controllerAs syntax, so we
can use it like MyCtrl as ctrl in the controller key for the route definition.
The other option is to define the controller inline, in which case we pass the con‐
troller function directly to the controller key. We can also use the array syntax to
inject our dependencies in a way that is uglification-safe. The code might look
something like:
144
| Chapter 10: Routing Using ngRoute
www.it-ebooks.info
$routeProvider.when('/test', {
template: '<h1>Test Route</h1>',
controller: ['$window', function($window) {
$window.alert('Test route has been loaded!');
}]
});
In this example, we define a route that has an inline controller with a dependency
on the $window service. All the route does is show a window alert on load. It is
recommended that we use the controller syntax to define our controllers; defining
it inline will make it hard to write unit tests or reuse it in other routes.
controllerAs
The controllerAs key is there as a convenience in case we don’t want to define
what the controller should be named inline in the controller key. The two route
definitions in the following example are equivalent in terms of functionality:
$routeProvider.when('/test', {
template: '<h1>Test Route</h1>',
controller: 'MyCtrl as ctrl'
});
$routeProvider.when('/test', {
template: '<h1>Test Route</h1>',
controller: 'MyCtrl',
controllerAs: 'ctrl'
});
Whether we use the controller key and define the renaming in it, or define it
separately using the controllerAs key, there is no functional difference. It is purely
a personal preference.
redirectTo
There are cases where some routes that used to exist have been renamed or cases
where multiple URLs in the application are actually the same page underneath. In
such cases, the redirectTo key can be used to specify the URL to which that par‐
ticular route must navigate when it is encountered. It can be used for error handling
and common route handling. For example:
$routeProvider.when('/new', {
template: '<h1>New Route</h1>'
});
$routeProvider.when('/old', {
redirectTo: '/new'
});
In this example, AngularJS opens the /new URL when the user enters ei‐
ther /#/new or /#/old in the browser.
Routing Options
www.it-ebooks.info
| 145
resolve
The final configuration, and most versatile and complex of the route configuration
options, is the resolve. In the next section, we cover how to implement resolves.
At a conceptual level, resolves are a way of executing and finishing asynchronous
tasks before a particular route is loaded. This is a great way to check if the user is
logged in and has authorization and permissions, and even preload some data be‐
fore a controller and route are loaded into the view.
Using Resolves for Pre-Route Checks
As mentioned in the previous section, when we define a resolve, we can define a set of
asynchronous tasks to execute before the route is loaded. A resolve is a set of keys and
functions. Each function can return a value or a promise. A sample resolve, which makes
a server call and returns a hardcoded value, is shown here:
angular.module('resolveApp', ['ngRoute'])
.value('Constant', {MAGIC_NUMBER: 42})
.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/', {
template: '<h1>Main Page, no resolves</h1>'
}).when('/protected', {
template: '<h2>Protected Page</h2>',
resolve: {
immediate: ['Constant', function(Constant) {
return Constant.MAGIC_NUMBER * 4;
}],
async: ['$http', function($http) {
return $http.get('/api/hasAccess');
}]
},
controller: ['$log', 'immediate', 'async',
function($log, immediate, async) {
$log.log('Immediate is ', immediate);
$log.log('Server returned for async', async);
}]
});
}]);
This example expects that there is a server-side API available at /api/hasAccess, which
on a GET request returns a status 200 response if the user has access, and a nonstatus
200 response if the user does not have access.
There are two routes in this example. The first route is a very standard route that loads
an HTML template when the route definition is encountered. We have no resolves on
this route, so it always loads properly.
The second definition contains a resolve defined with two keys, immediate and async.
Note that these are keys of our own choosing, so this could very well be myKey1 and
myKey2. AngularJS does not expect or force us to use any particular key. Each key takes
146
| Chapter 10: Routing Using ngRoute
www.it-ebooks.info
an array, which is the AngularJS Dependency Injection syntax. We define the depen‐
dencies for the resolve in the array, and get it injected into the resolve function. The
first resolve key, immediate, gets the Constant dependency injected into it, and returns
a constant value multiplied by some number. The second resolve key, async, gets the
$http dependency injected into it, and makes a server call to /api/hasAccess. It then
returns the promise for that particular server call. AngularJS guarantees the following:
• If the resolve function returns a value, AngularJS immediately finishes executing
and treats it as a successful resolve.
• If the resolve function returns a promise, AngularJS waits for the promise to return
and treats the resolve as successful if the promise is successful. If the promise is
rejected, the resolve is treated as a failure.
• Because of the resolve function, AngularJS ensures that the route does not load
until all the resolve functions are finished executing. If there are multiple re
solve keys that make asynchronous calls, AngularJS executes all of them in parallel
and waits for all of them to finish executing before loading the page.
• If any of the resolves encounter an error or any of the promises returned are rejected
(is a failure), AngularJS doesn’t load the route.
In the previous example, because the immediate resolve key is returning only a value,
it is treated as a successful resolve every time. The async resolve key makes a server call,
and if it is successful, the route is loaded. If the server returns a non-200 status response,
AngularJS doesn’t load the page. AngularJS still loads and caches the template if any of
the resolves fail, but the controller associated with the route isn’t loaded and the HTML
doesn’t make it into the ng-view.
In these cases, the user still sees the last page he was on. So it might not be a great user
experience, because the user won’t know that something went wrong. In “A Full Angu‐
larJS Routing Example” on page 150, we’ll see a full-fledged AngularJS routing example
that uses resolves in a more comprehensive pattern.
One other interesting thing about resolves is that we can get the value from each of the
resolve keys injected into our controller, if we want or need the data. Each key can
directly be injected into the controller by adding it as a dependency. This is over and
above any AngularJS service dependency we might have. The following items are in‐
jected into the controller:
• The value itself, if the resolve function was returning a value
• The resolution of a promise, if the resolve function was returning a promise
Routing Options
www.it-ebooks.info
| 147
In the case of the async resolve, we get the resolved value of the promise, which is the
response object from the server, with the config, status, headers, and data. This is
what’s normally passed to the success function of the then of the promise:
$http.get('/api/hasAccess').then(function(response) {
console.log('I am passed to the controller', response);
return response;
});
In this case, response is the value that async will take when it is injected into the
controller.
Using the $routeParams Service
The other thing to note, and which is often required in Single-Page Applications, is the
context for a route. For example, we might want to load a certain email thread or view
the details of a certain recipe. We want this information reflected in the URL so the user
can bookmark it or directly come back to it. In such cases, it is recommended that
controllers look up the IDs and information from the URL instead of relying on global
state. It uses this information to load the necessary details from the server.
That is, in an ideal Single-Page Application, a controller and a route should be able to
independently bootstrap themselves and not expect that the user first goes to a list page
and then to the details page. These URL parameters don’t have to be parsed from the
URL, but can directly be accessed from a convenient service that AngularJS provides,
called $routeParams:
angular.module('resolveApp', ['ngRoute'])
.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/', {
template: '<h1>Main Page</h1>'
}).when('/detail/:detId', {
template: '<h2>Loaded {{myCtrl.detailId}}' +
' and query String is {{myCtrl.qStr}}</h2>',
controller: ['$routeParams', function($routeParams) {
this.detailId = $routeParams.detId;
this.qStr = $routeParams.q;
}],
controllerAs: 'myCtrl'
});
}]);
In this example, the / route is pretty standard in terms of what we have seen so far. It
just loads a template when it is seen. The second route is defined as /detail/:detId. This
tells the AngularJS routing that there will be a value after the /detail in the URL that
needs to be picked up, stored, and provided as detId to the controller. For example,
URLs like /detail/123 will match the route, with detId taking the value 123. Even the
URL /detail/shyam will also match the route, with detId taking the value shyam. The
148
| Chapter 10: Routing Using ngRoute
www.it-ebooks.info
URL regex does not impose any restrictions on what kinds of values the parameters can
take.
We can access these values in our controllers by asking for the $routeParams service.
The $routeParams service is responsible for reading the URL, parsing it, and finding
all these variables and making them accessible to the controller in a nice way. If we go
to a URL like /detail/123?q=MySearchParam, AngularJS will parse this URL and the
$routeParams service will have the following value:
{
detId: '123',
q: 'MySearchParam'
}
Nowhere do we have to manually call parseURL or anything like that. Our controller
can directly access these keys from the $routeParams service and do what it needs to
from there. We can then make a server call to load the details, or process data and hide
and show UI elements as need be.
Things to Watch Out For
Before we jump into a full-fledged AngularJS routing example hooked up to an end-toend server, let’s quickly talk about a few things that are not well documented, but that
can cause headaches while developing an application:
Empty templates
AngularJS requires that each route be associated with a nonempty template or
templateUrl. That means if we leave out both template and templateUrl as part
of a route, AngularJS silently drops that route and doesn’t allow us to navigate to it
in the UI. If we have a route that does some work before navigating away from the
page (a logout route is an example), where the user will not be seeing any UI, make
sure we specify at least a template for the route that is nonempty. Even an empty
string will be treated the same as not specifying the template!
Resolve injection into controller
If we use resolves and want to inject the values of the dependency into our controller,
make sure we are defining our controller as part of the route definition, and not
directly in our controller with the ng-controller directive. Otherwise, AngularJS
does not know which controller needs those dependencies, and thus cannot inject
it properly.
$routeParam variable type
One potential problem when using the $routeParams service is when comparing
the values we get from $routeParams with objects from our database. For example,
if we’re storing IDs as numbers in our database and trying to compare that with
data from the $routeParams service, we’d better watch out! $routeParam returns
Routing Options
www.it-ebooks.info
| 149
string values by default for all keys. A === comparison of something from $rou
teParams with something from our database, which is a number, fails. We need to
make sure we convert both values into the same format before we do any compar‐
isons with data from $routeParams.
One ng-view per application
This is the last thing we should keep in mind when working with ngRoute. For every
AngularJS application that uses ngRoute, there can be one and only one ng-view
directive for that application. We cannot have multiple or nested ng-views. And
this is because the ng-view directive is quite simple in that it notices a URL change
and updates its content as per the route definition. If we have multiple ng-views,
we will see the same content multiple times. If we nest ng-views, we’ll see the content
inside the content, which won’t serve the purpose we are trying to solve.
A Full AngularJS Routing Example
We will now take a full end-to-end example that uses both AngularJS routing and the
$http service to communicate with our server. The next example is a FIFA Teams app,
which shows a list of some of the teams that play soccer.
Before we jump into the code, let’s lay out how the application works:
• A landing page shows a list of teams. Anybody can access this page.
• A login page allows users to log in to the application. Anybody can access this page.
• Details pages for teams are access-controlled. Only logged-in users can access the
details page. This is true whether the user logged in right before accessing, or logged
in and then closed the window and came back at any later point. The latter works
because the login session is maintained on the server, not on the client.
For the purpose of keeping the application simple and focused, the server has already
been created in NodeJS, which allows us to focus on the routing and server communi‐
cation aspects of the client-side application. You can grab the source code from chap‐
ter10 of the GitHub repository.
Run:
npm install
from the chapter10/routing-example folder to install its dependencies. Then execute:
node server.js
to get the server up and running. We can then navigate to http://localhost:8000 to view
the application in action.
The following is the index.html for our application:
150
| Chapter 10: Routing Using ngRoute
www.it-ebooks.info
<!-- File: chapter10/routing-example/app/index.html -->
<html>
<head>
<title>FIFA Teams</title>
<link rel="stylesheet" href="styles/bootstrap.css">
<link rel="stylesheet" href="styles/main.css">
</head>
<body ng-app="fifaApp" class="landing">
<div class="top-bar" ng-controller="MainCtrl as mainCtrl">
<div class="pull-left">
<span><a href="#/">FIFA TEAMS</a></span>
</div>
<div class="pull-right">
<span ng-hide="mainCtrl.userService.isLoggedIn">
<a href="#/login">Login</a>
</span>
<span ng-show="mainCtrl.userService.isLoggedIn">
<!-- server-side route, not a client-side route -->
<a href="/api/logout">Logout</a>
</span>
</div>
</div>
<div ng-view></div>
<script src="scripts/vendors/jquery-1.11.1.js"></script>
<script src="scripts/vendors/angular.js"></script>
<script src="scripts/vendors/angular-route.js"></script>
<script src="scripts/app.js"></script>
<script src="scripts/services.js"></script>
<script src="scripts/controllers.js"></script>
</body>
</html>
Our index.html file is noteworthy in the following ways:
• ng-app is on the body tag that dictates where to find the controllers, configuration,
etc.
• A top bar with a controller of its own is used to show a logo, as well as login and
logout links. To ensure that we don’t show both links at the same time, and that
the login state is handled at an application level, we have a UserService (which
we’ll look at in a bit) that holds the user’s logged-in state. The login and logout links
are then shown and hidden based on this service.
• An ng-view is the part of the HTML that responds to URL changes.
• We then load jQuery and AngularJS, followed by our own app code.
Routing Options
www.it-ebooks.info
| 151
The most noteworthy thing is the login/logout links, which are shown and hidden using
ng-show and ng-hide, respectively. They are backed by the UserService instead of a
controller, so that regardless of which page or screen the user is on, login and logouts
are reflected across the application.
Before we look at app.js, which is used to define our routes and configuration, let’s take
a look at the services we defined for our application in services.js:
// File: chapter10/routing-example/app/scripts/services.js
angular.module('fifaApp')
.factory('FifaService', ['$http',
function($http) {
return {
getTeams: function() {
return $http.get('/api/team');
},
getTeamDetails: function(code) {
return $http.get('/api/team/' + code);
}
}
}])
.factory('UserService', ['$http', function($http) {
var service = {
isLoggedIn: false,
session: function() {
return $http.get('/api/session')
.then(function(response) {
service.isLoggedIn = true;
return response;
});
},
login: function(user) {
return $http.post('/api/login', user)
.then(function(response) {
service.isLoggedIn = true;
return response;
});
}
};
return service;
}]);
We define two services, FifaService and UserService. FifaService is used to fetch
the list of teams, and the details of each team from the server using HTTP GET requests.
UserService also has two methods, one to check if the current user has an active session
on the server, and the second to log in the user. The server defines the following
endpoints:
152
| Chapter 10: Routing Using ngRoute
www.it-ebooks.info
• GET on /api/team returns the list of teams in the system as an array.
• GET on /api/team/:code, with code being the code of the team, returns the details
of a particular team as a single object.
• GET on /api/session returns either a 400 status if the user is not logged in, or an
object with the user details if he is logged in. This enables the client (the web ap‐
plication) to verify that the user is logged in to the server.
• POST on /api/login with POST data containing {username: 'myuser', pass
word: 'mypassword'} will try to log in the user. If successful, it returns what the
session call returns. Otherwise, it returns a status 400 error if the user authentication
fails with the msg field in the object containing the reason for the failure.
All the service APIs that make HTTP requests return promises that allow controllers
or other services to chain and perform their own postcompletion work.
The controllers.js file is next:
// File: chapter10/routing-example/app/scripts/controllers.js
angular.module('fifaApp')
.controller('MainCtrl', ['UserService',
function(UserService) {
var self = this;
self.userService = UserService;
// Check if the user is logged in when the application
// loads
// User Service will automatically update isLoggedIn
// after this call finishes
UserService.session();
}])
.controller('TeamListCtrl', ['FifaService',
function(FifaService) {
var self = this;
self.teams = [];
FifaService.getTeams().then(function(resp) {
self.teams = resp.data;
});
}])
.controller('LoginCtrl', ['UserService', '$location',
function(UserService, $location) {
var self = this;
self.user = {username: '', password: ''};
self.login = function() {
UserService.login(self.user).then(function(success) {
$location.path('/team');
Routing Options
www.it-ebooks.info
| 153
}, function(error) {
self.errorMessage = error.data.msg;
})
};
}])
.controller('TeamDetailsCtrl',
['$location', '$routeParams', 'FifaService',
function($location, $routeParams, FifaService) {
var self = this;
self.team = {};
FifaService.getTeamDetails($routeParams.code)
.then(function(resp){
self.team = resp.data;
}, function(error){
$location.path('/login');
});
}]);
Four controllers are defined for this application:
• MainCtrl is used to handle the top navigation bar, and basically exposes the User
Service to the view so that the HTML can hide and show the login/logout links
depending on the user’s state. It also makes a call to the server to see if the user is
logged in when the application loads.
• TeamListCtrl is used for the landing route, and just uses the FifaService to fetch
a list of teams when it loads. It then exposes this data for the view to display.
• LoginCtrl has only a function to let the user log in when he fills in his username
and password and clicks the login button. If the login is successful, it redirects the
user to the home page. In the case of an error, it shows an error message in the UI.
• The TeamDetailsCtrl is the only one that does something unique and interesting,
in that it loads a specific team based on the route. Suppose the user navigates to
http://localhost:8000/#/team/ESP. This triggers the route that loads TeamDe
tailsCtrl. Now TeamDetailsCtrl has to figure out which team it has been loaded
for. It can access this information from a service known as $routeParams. $route
Params has the current team’s code set in it at the key code. This is set up via routing,
which we will get to in just a bit. It then loads the team details from the server based
on this code from the URL.
All server requests happen via $http and return a promise, so we add a .then() function
when we care about getting notified about it finishing, or need the result (like loading
the teams, an individual team, or the login call).
Let’s also quickly take a look at the various HTMLs. These are all partials, so they aren’t
complete HTML documents and don’t have html, head, and body tags. First up is the
team_list.html file:
154
| Chapter 10: Routing Using ngRoute
www.it-ebooks.info
<!-- File: chapter10/routing-example/app/views/teams_list.html -->
<div class="team-list-container">
<div class="team"
ng-repeat="team in teamListCtrl.teams | orderBy: 'rank'">
<div class="team-info row">
<div class="col-lg-1 rank">
<span ng-bind="team.rank"></span>
</div>
<div class="col-sm-3">
<img ng-src="{{team.flagUrl}}" class="flag">
</div>
<div class="col-lg-6 name">
<a title="Image Courtesy: Wikipedia"
ng-href="#/team/{{team.code}}"
ng-bind="team.name"
style="color: cadetblue;"></a>
</div>
</div>
</div>
</div>
The teams list displays the teams from the controller using an ng-repeat. Each indi‐
vidual name in the list is a link to the details page of the team using the ng-href directive.
The images are shown using the ng-src directive.
The ng-href directive is used whenever we have dynamic URLs.
While we can use a statement like href="{{ctrl.myUrl}}", it is
recommended that we use ng-href instead.
This is because when the application initially loads, there is a small
window of time when the href has the value {{ctrl.myUrl}} be‐
fore AngularJS kicks in and replaces the content. To avoid this, we
use ng-href, which initially leaves the href as blank and places the
value of ctrl.myUrl in href after AngularJS is up and running.
Similarly, the ng-src directive is used for images that need dynam‐
ic URLs. If we use src="{{ctrl.myImg}}" in our HTML, the HTML
immediately makes a call to fetch {{ctrl.myImg}} as an image from
the server, which will obviously throw an error. Thus, the ng-src
directive allows AngularJS to kick in and set the src tag on the img
after the value has been calculated. Thus it prevents an extra erro‐
neous request.
Next up is the login.html page:
<!-- File: chapter10/routing-example/app/views/login.html -->
<div class="login-container"
ng-controller="LoginCtrl as loginCtrl">
<div class="alert alert-danger"
ng-bind="loginCtrl.errorMessage"
Routing Options
www.it-ebooks.info
| 155
ng-show="loginCtrl.errorMessage"></div>
<div class="card login-card">
<div class="login-form">
<form name="loginForm"
ng-submit="loginCtrl.login()"
class="form-horizontal"
role="form">
<div class="form-group">
<label for="email">Username</label>
<input type="text"
ng-model="loginCtrl.user.username"
class="form-control"
id="email"
placeholder="Enter Username"
required="">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password"
ng-model="loginCtrl.user.password"
class="form-control"
id="password"
placeholder="Enter Password"
required="">
</div>
<input type="submit"
class="btn btn-success btn-lg"
value="Login"
ng-disabled="loginForm.$invalid">
</form>
</div>
</div>
</div>
The login.html file displays a simple form with two fields for the username and pass
word. It defines the controller using the ng-controller directive directly, and uses the
ng-submit to trigger the login call from the controller. It also has a section that shows
the errorMessage if available.
The last page is the team_details.html, which displays all the details of the team in a nice,
orderly fashion:
<!-- File: chapter10/routing-example/app/views/team_details.html -->
<div class="team-details-container card">
<div class="team-logo">
<img title="Image Courtesy: Wikipedia"
ng-src="{{teamDetailsCtrl.team.logoUrl}}">
</div>
<div class="name">
<span ng-bind="teamDetailsCtrl.team.name"></span>
(<span ng-bind="teamDetailsCtrl.team.fifaCode"></span>)
</div>
156
| Chapter 10: Routing Using ngRoute
www.it-ebooks.info
<div class="detail">
<div class="label">
<span>Nickname</span>
</div>
<div class="title">
<span ng-bind="teamDetailsCtrl.team.nickname"></span>
</div>
</div>
<div class="detail">
<div class="label">
<span>FIFA Ranking</span>
</div>
<div class="title">
<span ng-bind="teamDetailsCtrl.team.fifaRanking">
</span>
</div>
</div>
<div class="detail">
<div class="label">
<span>Association</span>
</div>
<div class="title">
<span ng-bind="teamDetailsCtrl.team.association"></span>
</div>
</div>
<div class="detail">
<div class="label">
<span>Head Coach</span>
</div>
<div class="title">
<span ng-bind="teamDetailsCtrl.team.headCoach"></span>
</div>
</div>
<div class="detail">
<div class="label">
<span>Captain</span>
</div>
<div class="title">
<span ng-bind="teamDetailsCtrl.team.captain"></span>
</div>
</div>
</div>
The team details simply displays all the data using ng-bind in the UI, after the controller
has fetched the team details for the particular team from the server.
With all this done, let’s look at how to set up routing:
// File: chapter10/routing-example/app/scripts/app.js
angular.module('fifaApp', ['ngRoute'])
.config(function($routeProvider) {
$routeProvider.when('/', {
Routing Options
www.it-ebooks.info
| 157
templateUrl: 'views/team_list.html',
controller: 'TeamListCtrl as teamListCtrl'
})
.when('/login', {
templateUrl: 'views/login.html'
})
.when('/team/:code', {
templateUrl: 'views/team_details.html',
controller:'TeamDetailsCtrl as teamDetailsCtrl',
resolve: {
auth: ['$q', '$location', 'UserService',
function($q, $location, UserService) {
return UserService.session().then(
function(success) {},
function(err) {
$location.path('/login');
$location.replace();
return $q.reject(err);
});
}]
}
});
$routeProvider.otherwise({
redirectTo: '/'
});
});
We define our application (and create our module) in the app.js file. We define our
fifaApp module, and specify that it depends on the ngRoute module so that we can use
routing in our application. Let’s now dive deep into our routes:
• The / route introduces nothing new, in that it loads a templateUrl and attaches
the TeamListCtrl to it when it loads. There are no variables in the URL, nor any
access control checks.
• The /login route loads a templateUrl. The reason it doesn’t specify a controller
in the route configuration is because the HTML defines the controller using the
ng-controller syntax. For each route, we can decide to include the controller di‐
rectly in the HTML or using the controller configuration in the route.
• The largest and most complex of the routes is the Team Detail route. It is defined
as /team/:code. This tells AngularJS that as part of the URL, take everything af‐
ter /team/ and make it available to the controller in case it requires it as the variable
code in $routeParams.
• Lastly, there is an otherwise route, which redirects the user to the / route if the
user enters a URL that the route configuration doesn’t recognize.
Let’s dive into the Team Detail route definition a bit more. Here are the key highlights:
158
| Chapter 10: Routing Using ngRoute
www.it-ebooks.info
• The route itself has the variable code defined in it, which is made available to con‐
trollers using $routeParams service. A controller could get the $routeParams ser‐
vice injected, and then access $routeParams.code, as our TeamDetailsCtrl does.
• The controller is defined in the route, and uses the controllerAs syntax to name
the instance of the controller teamDetailsCtrl.
• We use a resolve object with one key, auth. This key is arbitrary, but resolve is
used as a way of checking with the server to see if the user is currently logged in.
The auth key takes a function, using the Dependency Injection syntax, so we can
inject any services we need into each individual resolve key.
• The authentication resolve injects UserService into it, and makes a call to User
Service.session() (which makes a server call to /api/session). The auth resolve
function returns a promise. AngularJS then guarantees the following:
— AngularJS won’t load the page until the promise is successfully fulfilled.
— AngularJS will prevent the page from loading if the promise fails.
• We also chain the promise and do nothing in the case of success. This ensures that
the returned promise succeeds if the server call succeeds.
• We add an error handler in the then of the promise to redirect the user to the login
screen, in case the server returns a non-200 response. We also make sure that we
reject the promise in the case of an error, because we still want the promise to fail.
If we don’t $q.reject, that tells AngularJS that the error was handled successfully
This ensures that if the user is not logged in when he clicks the link for the details of the
team, AngularJS will redirect him to the login page. And because we make a call to the
server to authorize the user, if the user logs in from another window or tab, and then
clicks it from this screen, he will still proceed to the page without the client prompting
for a unneeded login. To check what happens when the user enters the correct username
and password, try logging in with “admin” as the username and “admin” as the password.
Notice that we set the redirection path using $location.path(), as
well as call $location.replace(). This prevents the path the user
accessed from entering the browser’s history.
What this doesn’t protect from is the login session expiring while the user is on the page,
for which we would use HTTP interceptors. Resolves are great for pre-route checks, but
not for checks that need to happen when the user gets to the page.
Routing Options
www.it-ebooks.info
| 159
Additional Configuration
When using AngularJS routing, there are a few other concerns which, while not com‐
mon, are still important for many Single-Page Applications. In this section, we cover a
few of those, including how to have non-# URLs, and how to handle SEO and analytics
with AngularJS.
HTML5 Mode
Hash URLs or hashbang URLs are common when working with Single-Page Applica‐
tions. But it is possible to make an SPA using AngularJS look and behave just like a
normal multipage application using something called HTML5 mode and the browser’s
pushState API. In this mode, when the page initially loads, AngularJS hooks into the
browser URL to capture all location changes and handle them within AngularJS without
causing a full page reload. That is, a URL like http://www.myawesomeapp.com/#/first/
page would look like http://www.myawesomeapp.com/first/page with HTML5 mode
enabled. After the page loads, AngularJS is aware enough to realize which URLs con‐
stitute an AngularJS route and which ones it shouldn’t override.
To enable HTML5 mode, server-side support is also needed. While AngularJS can han‐
dle the initial index.html load, and handle all page URLs subsequent to the page load,
the server needs to be aware of what URLs AngularJS supports, and what URLs need to
be responded by the server. Let’s assume that we have an application with two URLs, /
first/page and /second/page. If we navigate to http://www.myawesomeapp.com/first/
page, assuming that HTML5 mode is enabled, realize that the browser is actually going
to make a server request. AngularJS will not have loaded at that point for it to take over
and control the browser. Thus it is imperative that the server receives the request for
http://www.myawesomeapp.com/first/page, and then in turn returns the index.html that
would have been served for http://www.myawesomeapp.com. After that, AngularJS
loads with /first/page as its route, and handles the routing from there. This is illustrated
in Figure 10-2.
Figure 10-2. How HTML5 mode is handled
160
| Chapter 10: Routing Using ngRoute
www.it-ebooks.info
Thus to enable HTML5 mode, three things are needed:
• Enable HTML5 mode as part of the application config on the client side as follows:
angular.module('myHtml5App', ['ngRoute'])
.config(['$locationProvider', '$routeProvider',
function($locationProvider, $routeProvider) {
$locationProvider.html5Mode(true);
//Optional
$locationProvider.hashPrefix('!');
// Route configuration here as normal
// Route for /first/page
// Route for /second/page
}]);
To set HTML5 mode, we ask for $locationProvider as part of the configuration,
and call the function html5Mode with true on it. It’s recommended that we also set
the hashPrefix as ! to easily support SEO, as we will see in the next section. This
is all we need to do on the client side for nonhash URLs in AngularJS.
• In index.html, we need to add the <base> tag with an href attribute to the <head>
portion. This is to tell the browser where, in relation to the URL, the static resources
are served from, so that if the application requests an image or CSS file with a relative
path, it doesn’t take it from the current URL necessarily.
For example, let us say our base application is served from http://www.myweb‐
site.com/app, and it has HTML5 mode enabled. So when a user navigates to http://
www.mywebsite.com/app/route/15, our server still serves the index.html page, but
for the browser, the application path is /app/route/15. So all relative files will be
relative to this URL. Instead, we need something like the following in our HTML:
<html>
<head>
<base href="/app" />
</head>
</html>
This would ensure that regardless of the URL, all relative paths would be resolved
relative to /app and not to some other URL. If your application is being served
from /, then have <base href="/"> in your <head> tag.
• On the server side, we need a rule that states that when the server sees a request
for /first/page or /second/page, it needs to serve the content that it normally serves
for the / request, which is usually index.html. In NodeJS, it might look something
like this:
var express = require('express'),
url = require('url');
Additional Configuration | 161
www.it-ebooks.info
var app = express();
// express configuration here
var INDEX_HTML = fs.readFileSync(
__dirname + '/index.html', 'utf-8');
var ACCEPTABLE_URLS = ['/first/page', '/second/page'];
app.use(function(req, res, next) {
var parts = url.parse(req.url);
for (var i = 0; i < ACCEPTABLE_URLS.length; i++) {
if (parts.pathname.indexOf(ACCEPTABLE_URLS[i]) === 0) {
// We found a match to one of our
// client-side routes
return res.send(200, INDEX_HTML);
}
}
return next();
});
// Other routes here
We have a simple node server that reads and caches the contents of index.html into
the INDEX_HTML variable. After that, if it sees a request for /first/page or /second/
page, it returns the contents of INDEX_HTML; otherwise, it continues to the correct
response handler by calling next().
SEO with AngularJS
Because Single-Page Applications are heavily dependent on JavaScript executing after
the HTML loading to display and render the relevant content, supporting search engine
crawlers becomes a little bit more involved and complicated. That said, it is still possible
to get search engines to properly crawl our SPA like they would any other normal
website.
Google and AJAX App Indexing
Google has plans to start trying to parse and view JavaScript-based
pages as the user might see them by actually executing the Java‐
Script in the page. This is in contrast to the accepted norms up until
now, which required just loading the HTML page. The Google Web‐
master Central page has more details on it, along with tools to un‐
derstand how Google views these pages.
Here are the key things to keep in mind when dealing with search engines:
162
| Chapter 10: Routing Using ngRoute
www.it-ebooks.info
• Search engines like Google and Bing have defined patterns to work with SinglePage Applications. In particular, to support search engine crawling, it is expected
that the SPA will use hashbang URLs instead of pure hash URLs (#! instead of #).
• When search engines crawl, they replace the #! with ?escaped_fragment=, and the
request is made to the server instead of the client handling it.
At this point, to ensure that the search engine gets the correct content instead of some
HTML fragment with {{ }} and no content filled out, we need to ensure that the server
recognizes these URLs as coming from the search engine, and handle them differently
than the normal user flow. In such a case, there are one of two options:
• Create an HTML snapshot of our entire web application, and serve those HTMLs
when the search engine makes a request. This is faster but has the overhead of
making sure we keep the HTML snapshots updated. The AngularJS documenta‐
tion, which is an AngularJS application, uses this strategy to make sure the Angu‐
larJS documentation is searchable on Google.
• Serve live, rendered HTML content when the search engine makes a request. This
could be done with something like PhantomJS, a headless browser, running on the
server. When the server sees a request from the search engine, it could get Phan‐
tomJS to render the proper page with all JavaScript execution, and then return the
fully rendered content to the search engine.
Both of these are involved processes, and if you’re looking for a fully packaged solution
or pointers on how this might be done, here are a few links and SaaS solutions for SPAs:
• YearOfMoo has a great in-depth article on enabling SEO for your AngularJS ap‐
plication, which goes into the nitty-gritty details of how you might want to enable
it from scratch. It is a recommended read so you understand how it works under
the covers.
• The GitHub project angular-seo is a great starting point for taking YearOfMoo’s
ideas and implementing them using PhantomJS.
• There are SaaS implementations that take the pain out of indexing your SPA, like
BromBone, Prerender.io, and GetSeoJS, which offer all of these in a single package
at a convenient price if you don’t want to implement and maintain your own version.
Analytics with AngularJS
Traditional Analytics (like Google Analytics) don’t work easily on SPAs, because they
add an analytics event for each page load. With an SPA, we need to manually trigger
and tell Google Analytics of route changes and other events. Thankfully, the wonderful
open source community around AngularJS has already tried multiple approaches and
Additional Configuration | 163
www.it-ebooks.info
implementations for this. One of the more commonly used options is Angularytics,
which provides a great service to manually track events. It comes with a default inte‐
gration for logging events to the console, or to Google Analytics.
It provides the following out of the box:
• Automated event tracking for page and route changes
• A nice service for manually triggering events at various times from our controllers,
services, and directives
• A filter for triggering events from the HTML, such as on the click of a button
Setting it up is as breezy as including the JavaScript source code for Angularytics in our
application and then configuring it and instantiating it correctly as part of our app as
follows:
angular.module('myTrackingApp', ['angularytics'])
.config(['AngularyticsProvider',
function(AngularyticsProvider) {
AngularyticsProvider.setEventHandlers(
['GoogleUniversal', 'Console']);
}]).run(['Angularytics', function(Angularytics) {
Angularytics.init();
}])
This code snippet assumes that we have done the following:
• Set up Universal Google Analytics code in our index.html file correctly
• Loaded angular.js and angularytics.js
After that’s done, we depend on the Angularytics module and set up the event handlers
we want. In the previous example, we add both the Console handler and the Google
Analytics handler. We also add a run method, which executes first in our entire appli‐
cation to ensure that Angularytics is initialized first.
After this is done, we can use this as follows in our HTML:
<button ng-click="myCtrl.login() | trackEvent:'Login Page':'clicked login'">
Login</button>
The trackEvent filter takes the category and type of event that was triggered, and can
be appended to any action easily in our HTML.
We can also use it in the controller or service as follows:
Angularytics.trackEvent('Create Page', 'Opened');
In this line of code, Angularytics is a service that we dependency-inject wherever we
need it, like a service or controller.
164
| Chapter 10: Routing Using ngRoute
www.it-ebooks.info
Alternatives: ui-router
The AngularJS router is great and satisfies about 70–80% of our needs with routing.
Most SPAs have one general section of the page that responds to URL changes, and
shows and displays different content.
But what if we had more complex requirements and wanted to change different parts
of our UI differently depending on the URL? Maybe we had a different menu and view
for admins versus normal users. Also, we might have sections inside a page that might
need to be hidden or shown depending on certain flags and events. With ngRoute, we
can use ng-show and ng-hide directives, or even ng-switch to implement these
behaviors.
But those can get messy, with extra variables and large files to maintain. It requires rigor
and oversight to keep our codebase clean and modular in such a case. Instead, when we
have such requirements, we can also use the optional ui-router for our routing needs,
instead of ngRoute. We saw that we can only have one ng-view per application. The uirouter does away with that restriction. Here are the things to keep in mind when
switching to using ui-router:
• ui-router uses the concepts of states, instead of routes. As part of our configura‐
tion, we define the various states in our application and what the ui-router should
do when it encounters a particular state. The language and syntax for defining states
is similar to ngRoute, in that we define our state, followed by a state configuration
that includes:
— template
— templateUrl
— controller
— resolve
• Instead of using hrefs and anchors to navigate in our application, the ui-router
provides a directive called ui-sref that allows us to navigate to states. This can be
added not only on anchors, but on buttons, images, and any other element on which
we need the behavior.
• Instead of using the $routeProvider to define our routes, we use the $statePro
vider to define our states.
• We can have multiple named ui-views in our application. We could have, in our
index.html, a code snippet like the following:
<div ui-view="leftNav"></div>
<div ui-view="mainContent"></div>
Alternatives: ui-router
www.it-ebooks.info
| 165
In this example, there are two ui-views: one for the left navigation section and
another for the main content. Each of these can respond to URL and state changes
differently, which allows us to separate and modularize our code even further.
• We can also nest ui-views within ui-views, unlike ng-views. If we have two states,
main and main.child, that means the main state corresponds to the first ui-view
in index.html. This is similar to ng-view. When the state transitions to
main.child, it looks for a ui-view directive inside the template of main. If it finds
it, it is changed as per the route definition.
There are many more advantages and fine-grained control that we can achieve when
we use ui-router instead of ngRoute. The detailed documentation is available at the
UI-Router guide.
ui-router is highly configurable and modular, but does add some complexity to the
codebase. While most of the concepts transfer from ngRoute to ui-router, there are a
few things that don’t do it immediately. ui-router is state-oriented, and by default does
not modify URLs. We need to specify the URL for each state individually.
Adding named and nested views can also be overkill for small or medium projects in
terms of complexity, file structure, and loading. We should consider using ui-router
if our project needs or has the following requirements:
• We need different parts of the page to react differently to URL changes or user
interactions.
• We have multiple different (nested) sections of the page that are conditionally
shown for various actions and events.
• We don’t need the URL to change while the user navigates throughout our
application.
• The entire UI layout needs to change completely across different pages.
In such cases, ui-router makes sense. If we don’t have any of these requirements, the
ngRoute module should be good enough for our application.
Conclusion
We covered routing in a Single-Page Application, from the simple task of changing
templates for each URL change, to a more in-depth example of loading different tem‐
plates and controllers depending on the route. We also dove into the concept of resolves,
which allow us to complete asynchronous tasks before a certain page or route is loaded.
This allows us to load data beforehand, as well as do any access control and permission
checks we might want to. We then looked at a detailed, in-depth example that brought
all of these together into a single application.
166
| Chapter 10: Routing Using ngRoute
www.it-ebooks.info
After the example, we covered some other common use cases that need to be handled
for certain projects, like SEO, analytics, and more. We also took a brief glance at uirouter, which is a more configurable, modular alternative to the AngularJS routing.
In the next chapter, we dive into directives, the AngularJS version of custom compo‐
nents. We look at the basics of creating directives and the various options that allow us
to configure how they work.
Conclusion | 167
www.it-ebooks.info
www.it-ebooks.info
CHAPTER 11
Directives
Having explored all the other parts of AngularJS, like controllers, services, and filters,
we dive deep into directives in this chapter. Directives are the AngularJS way of dealing
with DOM manipulation and rendering reusable UI widgets. They can be used for
simple things like reusing HTML snippets, to more complex things like modifying the
behavior of existing elements (think ng-show, ng-class, or making elements draggable)
or integrating with third-party components like charts and other fancy doodads.
In this chapter, we start with developing a very basic directive, and explore some of the
more common options like template, templateUrl, link, and scopes. By the end of
the chapter, we will have created multiple versions of our simple reusable widgets, each
one building on top of the previous, along the way explaining how each directive defi‐
nition object changes the functioning of our directive.
What Are Directives?
When we hear the word “directives,” the very first association that should come to our
minds is dealing directly with the UI or the HTML that the user sees. Directives are of
two major types in AngularJS (though they can be subclassified further and further):
Behavior modifiers
These types of directives work on existing UI and HTML snippets, and just add or
modify the existing behavior of what the UI does. Examples of such directives would
be ng-show (which hides or shows an existing element based on a condition), or
ng-model (which adds the AngularJS data-binding hooks to any input to which it
is attached).
Reusable components
These types of directives are the more common variety, in which the directive cre‐
ates a whole new HTML structure. These directives have some rendering logic (how
and what should it display) and some business logic (where should it get the data,
169
www.it-ebooks.info
what happens when the user interacts with it) attached to it. Some examples of these
directives could be a tab widget, a carousel/accordian directive (though HTML5/6/7
might introduce these as a control), and a pie chart directive. These are also the best
way to integrate third-party UI components into AngularJS (think jQuery UI or
Google Charts).
While directives are the most powerful and complex part of AngularJS, the concepts
they are built upon are quite simple. To ensure that we understand how they work, we’ll
introduce the options one at a time. This chapter focuses on some of the more straight‐
forward and commonly used options for defining a directive, and the next chapter
introduces some of the more complex and less used hooks of a directive.
Alternatives to Custom Directives
Before we jump into directives, let’s quickly walk through some other options that might
serve us well in the case that we want reusable HTML, or business logic in our HTML.
We have two directives, ng-include and ng-switch, which can help us in extracting
HTML into smaller chunks and deciding when to show and hide them in our HTML.
In many cases, we can use these two directives instead of writing our custom directives.
ng-include
The ng-include directive takes an AngularJS expression (similar to ng-show and ngclick) and treats its value as the path to an HTML file. It then fetches that HTML file
from the server and includes its content as the child (and the only child, replacing all
other existing content) of the element that ng-include is placed on. Imagine that we
extracted a stock.html file with the following content:
<!-- File: chapter11/ng-include/stock.html -->
<div class="stock-dash">
Name:
<span class="stock-name"
ng-bind="stock.name">
</span>
Price:
<span class="stock-price"
ng-bind="stock.price | currency">
</span>
Percentage Change:
<span class="stock-change"
ng-bind="mainCtrl.getChange(stock) + '%'">
</span>
</div>
This HTML is very simple in that it takes a variable called stock and displays its name
and price in separate spans. In the last span, it calls a function on mainCtrl to calculate
170
| Chapter 11: Directives
www.it-ebooks.info
and display the percentage change for the current stock. Let’s now take a look at
index.html for this application:
<!-- File: chapter11/ng-include/index.html -->
<html>
<head>
<title>Stock Market App</title>
</head>
<body ng-app="stockMarketApp">
<div ng-controller="MainCtrl as mainCtrl">
<h3>List of Stocks</h3>
<div ng-repeat="stock in mainCtrl.stocks">
<div ng-include="mainCtrl.stockTemplate">
</div>
</div>
</div>
<script src="http://code.angularjs.org/1.2.16/angular.js"></script>
<script src="app.js"></script>
</body>
</html>
The main index.html file loads AngularJS and our application code, and instantiates
ng-app (stockMarketApp). It then loads a controller on the main div, and displays a list
of stocks inside of it. We’re extracting the content of ng-repeat into stock.html, instead
of having it inline. We then tell the ng-include to load whatever mainCtrl.stockTem
plate points to. Let’s now see what the controller is doing:
// File: chapter11/ng-include/app.js
angular.module('stockMarketApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.stocks = [
{name: 'First Stock', price: 100, previous: 220},
{name: 'Second Stock', price: 140, previous: 120},
{name: 'Third Stock', price: 110, previous: 110},
{name: 'Fourth Stock', price: 400, previous: 420}
];
self.stockTemplate = 'stock.html';
self.getChange = function(stock) {
return Math.ceil((
(stock.price - stock.previous) / stock.previous) * 100);
};
}]);
MainCtrl defines a list of stocks in its controller, each with a name, price, and previous
price. It also defines a getChange function, which is used to figure out the percentage
Alternatives to Custom Directives
www.it-ebooks.info
| 171
change for a particular stock that is passed to it (it also multiplies it by 100, and rounds
it off for easier display).
Note that in the previous example, we included the following in
index.html:
<div ng-include="mainCtrl.stockTemplate"></div>
This required us to define a variable in the controller and refer to it.
Another option is to inline the string directly in the HTML for the
ng-include. When we inline it, we have to ensure that AngularJS
understands that we have not passed it a variable on our controller,
but the actual value itself. We could do that as follows:
<div ng-include="'views/stock.html'"></div>
Notice the single quotes inside the double quotes. The single quotes
tell AngularJS that the value passed to it is a string literal, not a vari‐
able. If we don’t add the single quotes, AngularJS will look for a vari‐
able called views/stock.html (which obviously is an illegal variable
name, and doesn’t exist), and throw an error saying that it is unable
to parse the expression. So don’t forget to include the single quotes if
you’re directly using the filename in the HTML.
Any time we have to serve HTML partials, we need an HTTP server because the browser
does not allow serving or requesting files on the file:// protocol. So to make this appli‐
cation work on our local machine, we have a few options:
• Install Node’s http-server by running sudo npm install -g http-server (drop
sudo if you’re on Windows). Then run http-server from the directory that con‐
tains index.html.
• Python addicts can run python -m SimpleHTTPServer from the folder where the
index.html file is as well.
• Finally, WebStorm can start a built-in server when you ask it to open the index.html
file in a browser.
At this point if we run this application, we will see an HTML page that displays four
stocks, each with its own name, price, and percentage change. ng-include helped us by
allowing us to extract the HTML that would otherwise have been in index.html (tech‐
nically, this could be any file that we are working on), and extract it to a smaller, reusable,
and easier-to-maintain file.
This is the best feature of ng-include. If we have large HTML files, we can easily extract
them out into smaller, easier-to-manage HTML files and make our HTML modular as
well.
172
| Chapter 11: Directives
www.it-ebooks.info
Limitations of ng-include
While ng-include is great at extracting snippets of HTML into smaller files, it is not
without limitations. The stock.html file we created has two major limitations if we in‐
clude it using the ng-include directive in AngularJS:
• The stock.html file currently looks for a variable called stock and displays its name,
price, and percentage change information. Now, in index.html, if we ended up
changing the repeater from stock in mainCtrl.stocks to each in
mainCtrl.stocks, we would end up showing four empty blocks without the name,
price, or change information. This is because although we changed the name of the
variable in the main index.html file, the stock.html file still expects a variable called
stock for it to display. Thus, if we are using ng-include, each file that includes
stock.html must name the variable containing the information stock.
• The stock.html file is also currently dependent on being used along with
mainCtrl. This is because it expects there to be a mainCtrl.getChange function
that it uses to calculate the percentage change of the stock. If stock.html is used in
some other HTML that either does not have the controller named mainCtrl, or if
the controller does not have the function getChange(), then the HTML will not be
able to display the percentage change. Thus, the behavior that the extracted HTML
depends on will have to be manually included with the right name every time this
HTML is used.
In “Creating a Directive” on page 175, we will see how to fix both of these problems using
directives that we create.
ng-switch
The ng-switch is another directive that allows us to add some functionality to the UI
for selectively displaying certain snippets of HTML. It gives us a way of conditionally
including HTML snippets by behaving like a switch case directly in the HTML. Here is
how a simple usage of ng-switch might operate:
<!-- File: chapter11/ng-switch/index.html -->
<html>
<head>
<title>Switch App</title>
</head>
<body ng-app="switchApp">
<div ng-controller="MainCtrl as mainCtrl">
<h3>Conditional Elements in HTML</h3>
<button ng-click="mainCtrl.currentTab = 'tab1'">
Tab 1
</button>
Alternatives to Custom Directives
www.it-ebooks.info
| 173
<button ng-click="mainCtrl.currentTab = 'tab2'">
Tab 2
</button>
<button ng-click="mainCtrl.currentTab = 'tab3'">
Tab 3
</button>
<button ng-click="mainCtrl.currentTab = 'something'">
Trigger Default
</button>
<div ng-switch="mainCtrl.currentTab">
<div ng-switch-when="tab1">
Tab 1 is selected
</div>
<div ng-switch-when="tab2">
Tab 2 is selected
</div>
<div ng-switch-when="tab3">
Tab 3 is selected
</div>
<div ng-switch-default>
No known tab selected
</div>
</div>
</div>
<script src="http://code.angularjs.org/1.2.16/angular.js"></script>
<script type="text/javascript">
angular.module('switchApp', [])
.controller('MainCtrl', [function() {
this.currentTab = 'tab1';
}]);
</script>
</body>
</html>
This example is a simple application that shows five buttons. Clicking any of the first
three buttons opens that tab. The last two buttons set a random value for the current
tab. In such a case, the default case is triggered and the last div is shown.
Running this example requires the same steps as the previous example, so get a locally
running server started.
It accomplishes this using the ng-switch directive. We add an ng-switch based on the
value of mainCtrl.currentTab. Inside the div, we add multiple divs, which are shown
selectively. We accomplish this by adding ng-switch-when attributes to the children
elements. We add the conditions (like a select statement). Each ng-switch-when takes
a string value (for example, “hello”). If this string value matches the value of the ex‐
pression passed to ng-switch (in this case, the value of the variable mainCtrl.current
174
| Chapter 11: Directives
www.it-ebooks.info
Tab), then the element is displayed. If none of the ng-switch-when values match the
value of the original expression, then the ng-switch-default case is triggered.
There are a few things to note about ng-switch:
• ng-switch loads its content, and then based on the condition, comments out all
the ng-switch-when conditions that are not satisfied. So even if we use nginclude inside ng-switch-when, they will not get loaded up front, and will be
loaded only when the condition is met.
• ng-switch-when is treated as an attribute, and thus the value passed to it is expected
to be direct, and not an AngularJS expression. That is, suppose we have
ng-switch="mainCtrl.currentTab" and then ng-switch-when="mainCtrl.
possibleValue". This would expect the value of currentTab in the controller to
be equal to the string "mainCtrl.possibleValue", and not the value of
mainCtrl.possibleValue. ng-switch-when does not understand AngularJS
expressions.
Understanding the Basic Options
We now know how and when to use ng-include and ng-switch, and understand their
shortcomings. Now let’s see how we can create a directive to solve some of these
problems.
The main intentions of a directive are:
• To make our intention declarative by specifying in the HTML what something is
or tries to do.
• To make something reusable so the same functionality can be achieved easily
without copying and pasting the code.
• To achieve abstraction in the sense that the user of the directive doesn’t need to
know or understand how something is performed, but only cares about the end
result. The corollary of this is that the underlying implementation can be changed
without having to change every single usage.
Let’s jump into the options that are available when creating a directive using the example
from the ng-include, and see how we might convert that into a proper, reusable
directive.
Creating a Directive
Creating a directive is just like creating controllers, services, and filters in that the An‐
gularJS module function allows us to create a directive by name. The first argument to
Understanding the Basic Options
www.it-ebooks.info
| 175
the function is the name of the directive, and the second argument is the standard
Dependency Injection array syntax, with the last element in the array being our directive
function.
Suppose we want to turn the stock.html file from the previous example into a reusable
directive. Let’s start with how we want to use it in our HTML:
<div stock-widget></div>
If we want to be able to declare that the current div is a stock widget (used as stated
previously), we need to declare or create our directive as follows:
angular.module('stockMarketApp', [])
.directive('stockWidget', [function() {
return {
// Directive definition will go here
};
}]);
We define the stockWidget directive and provide it with a function. This function sets
up our directive using what we call a directive definition object and returns this defini‐
tion. AngularJS looks at this definition each time it encounters our directive in the
HTML.
By default, when we create a directive this way, we can use it only as an attribute of
existing elements in our HTML. That is, by default, we can only use <div stockwidget> and not <stock-widget>.
Directive and Attribute Naming
One thing to note is the naming of the directive. HTML is caseinsensitive by default. To deal with this when translating names of
attributes and directives from HTML to JavaScript, AngularJS con‐
verts dashes to camelCase. Thus, stock-widget (or STOCK-WIDGET or
even Stock-Widget) in HTML becomes stockWidget in JavaScript.
Next, let’s take a look at some commonly used options when creating directives.
Template/Template URL
The very first thing we can define as part of our directive is whether it has any content
that needs to be inserted when the directive is encountered. We do this using the tem
plate and templateUrl keys of the directive definition object (similar to routing).
176
| Chapter 11: Directives
www.it-ebooks.info
Let’s now achieve the same functionality that we had with the ng-include, but using a
directive so that it’s more declarative. First, the app.js file, which remains unchanged:
// This is chapter11/directive-with-template/app.js
angular.module('stockMarketApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.stocks = [
{name: 'First Stock', price: 100, previous: 220},
{name: 'Second Stock', price: 140, previous: 120},
{name: 'Third Stock', price: 110, previous: 110},
{name: 'Fourth Stock', price: 400, previous: 420}
];
self.getChange = function(stock) {
return Math.ceil((
(stock.price - stock.previous) / stock.previous) * 100);
};
}]);
Similarly, the stock.html file also remains unchanged from the previous example:
<!-- File: chapter11/directive-with-template/stock.html -->
<div class="stock-dash">
Name:
<span class="stock-name"
ng-bind="stock.name">
</span>
Price:
<span class="stock-price"
ng-bind="stock.price | currency">
</span>
Percentage Change:
<span class="stock-change"
ng-bind="mainCtrl.getChange(stock) + '%'">
</span>
</div>
The index.html file next changes slightly because it now uses our directive instead of
ng-include:
<!-- File: chapter11/directive-with-template/index.html -->
<html>
<head>
<title>Stock Market App</title>
</head>
<body ng-app="stockMarketApp">
<div ng-controller="MainCtrl as mainCtrl">
<h3>List of Stocks</h3>
<div ng-repeat="stock in mainCtrl.stocks">
<div stock-widget>
Understanding the Basic Options
www.it-ebooks.info
| 177
</div>
</div>
</div>
<script src="http://code.angularjs.org/1.2.16/angular.js"></script>
<script src="app.js"></script>
<script src="directive.js"></script>
</body>
</html>
Only the content of ng-repeat has changed; it now states <div stock-widget></
div>. Finally, let’s see how this directive is defined and created:
// File: chapter11/directive-with-template/directive.js
angular.module('stockMarketApp')
.directive('stockWidget', [function() {
return {
templateUrl: 'stock.html'
};
}]);
In this file, we create a directive with a very simple definition. We tell AngularJS that
any time we encounter a directive in our HTML called stock-widget (because we
named it stockWidget, which translates to stock-widget in HTML), it should fetch
the template stock.html from our server and insert it as a child of the element the directive
is placed on.
Note that AngularJS will be smart and fetch the HTML template that’s
at the templateUrl location only once when the directive is encoun‐
tered the very first time. After that, it saves the template in its local
cache and serves it from there.
If our HTML is small enough, we could possibly look at inlining it directly in our di‐
rective using the template key in the directive definition object. Here is how our di‐
rective might look if we did away with the stock.html file and inlined it in our directive:
angular.module('stockMarketApp')
.directive('stockWidget', [function() {
return {
template: '<div class="stock-dash">' +
'Name: ' +
'<span class="stock-name"' +
'ng-bind="stock.name">' +
'</span>' +
'Price: ' +
'<span class="stock-price"' +
'ng-bind="stock.price | currency">' +
'</span>' +
'Change: ' +
178
| Chapter 11: Directives
www.it-ebooks.info
'<span class="stock-change"' +
'ng-bind="mainCtrl.getChange(stock) + '%'">' +
'</span>' +
'</div>'
};
}]);
Note that this is a replacement for the directive we created before. Both have the exact
same effect in terms of the UI, in that they load an HTML template and place it as a
child of the element that the directive is on.
The template key makes sense if our HTML snippet is small and easy to maintain. For
larger, more complex templates, it almost always makes sense to load them via the
templateUrl key.
Restrict
The restrict keyword defines how someone using the directive in their code might
use it. As mentioned previously, the default way of using directives is via attributes of
existing elements (we used <div stock-widget> for ours).
When we create our directive, we have control in deciding how it’s used. The possible
values for restrict (and thus the ways in which we can use our directive) are:
A
The letter A in the value for restrict specifies that the directive can be used as an
attribute on existing HTML elements (such as <div stock-widget></div>). This
is the default value.
E
The letter E in the value for restrict specifies that the directive can be used as a
new HTML element (such as <stock-widget></stock-widget>).
C
The letter C in the value for restrict specifies that the directive can be used as a
class name in existing HTML elements (such as <div class="stock-widget">
</div>).
M
The letter M in the value for restrict specifies that the directive can be used as
HTML comments (such as <!-- directive: stock-widget -→). This was previ‐
ously necessary for directives that needed to encompass multiple elements, like
multiple rows in tables, etc. The ng-repeat-start and ng-repeat-end directives
were introduced for this sole purpose, so it’s preferable to use them instead of com‐
ment directives.
Understanding the Basic Options
www.it-ebooks.info
| 179
Each of these can be used by itself as an argument to the restrict key, or could be used
in combination with each other. Here’s how we might update our stock-widget direc‐
tive to be able to use it as either attributes or elements in our HTML:
// File: chapter11/directive-with-restrict/directive.js
angular.module('stockMarketApp')
.directive('stockWidget', [function() {
return {
templateUrl: 'stock.html',
restrict: 'AE'
};
}]);
We added a restrict key to our directive definition object, and gave it a value of AE.
This tells AngularJS that we can use the widget in our HTML as either <div stockwidget> or directly as <stock-widget>. This also prevents us from using <div
class="stock-widget">, because we have not allowed it to be used as class names.
Expressions in Class Directives
We have seen four possibilities for the restrict key. We will ex‐
plore in the following sections how to pass values to our directive, but
we might wonder how that is possible with the class-based direc‐
tive. That is, how would <div my-widget="someExp"> translate to a
class-based directive?
Class-based directives translate to <div class="my-widget: some
Exp;"> and AngularJS would treat this as similar to passing a value
to an attribute in HTML.
Now that we’ve explored the various ways in which directives can be used, let’s cover
some best practices around their usage:
• Internet Explorer 8 and below do not like custom HTML elements. If we plan on
using them, we need to manually tell Internet Explorer that we have some new
elements by calling document.createElement('stock-widget') and so on for
each new element. AngularJS, with version 1.3 onwards, has dropped support for
(or rather, testing on) Internet Explorer 8.
• Class-based directives are ideal for rendering-related work (like the ng-cloak di‐
rective that hides and shows elements, or image loading directives).
• Element directives are recommended if we are creating entirely new HTML content.
• Attribute directives are usually preferred for behavior modifiers (like ng-show, ngclass, and so on).
180
| Chapter 11: Directives
www.it-ebooks.info
The link Function
The link keyword in the directive definition object is used to add what we call a “link
function” for the directive. The link function does for a directive what a controller does
for a view—it defines APIs and functions that are necessary for the directive, in addition
to manipulating and working with the DOM.
AngularJS executes the link function for each instance of the directive, so each instance
can get its own, fully contained business logic while not affecting any other instance of
the directive. The link function gets a standard set of arguments passed to it that remain
consistent across directives, which looks something like the following:
link: function($scope, $element, $attrs) {}
The link function gets passed the scope of the element the directive is working on, the
HTML DOM element the directive is operating on, and all the attributes on the element
as strings. If we need to add functionality to our instance of the directive, we can add it
to the scope of the element we’re working with.
Let’s take our example from before and move the functionality from mainCtrl into our
directive, so the directive doesn’t have to depend on there being a MainCtrl with a
function called getChange:
// File: chapter11/directive-with-link/directive.js
angular.module('stockMarketApp')
.directive('stockWidget', [function() {
return {
templateUrl: 'stock.html',
restrict: 'AE',
link: function($scope, $element, $attrs) {
$scope.getChange = function(stock) {
return Math.ceil(((stock.price - stock.previous) /
stock.previous) * 100);
};
}
};
}]);
In this code snippet, the directive defines a link function that adds a function called
getChange() on its scope. This makes each instance of the directive have a getCh
ange() function on its own scope. The function was moved from the controller to the
directive, without any other changes. Now let’s see how we might use it in stock.html:
<!-- File: chapter11/directive-with-link/stock.html -->
<div class="stock-dash">
Name:
<span class="stock-name"
ng-bind="stock.name">
</span>
Price:
Understanding the Basic Options
www.it-ebooks.info
| 181
<span class="stock-price"
ng-bind="stock.price | currency">
</span>
Percentage Change:
<span class="stock-change"
ng-bind="getChange(stock) + '%'">
</span>
</div>
The stock.html file remains largely unchanged, except for the change in the last ngbind directive. Instead of binding to mainCtrl.getChange(stock), we bind to getCh
ange(stock). This implies that it’s looking for a getChange() function on its own scope.
Now regardless of which controller we use our directive in, or even if we rename our
controller, the stockWidget directive has its own version of the getChange function. It
becomes independent.
The link function is also where we can define our own listeners, work directly with the
DOM element, and much more. We’ll explore more of these in Chapter 13.
What’s the Scope?
You might wonder what scope we are adding these functions to. You’re
right to be worried, and it is something we should always keep in mind
when we add functions to the scope in the link function. In the
example in this section, ng-repeat in the main index.html file cre‐
ates a scope for each stock in our array, and it is to this scope that
we’re adding the functions. Because of this, we’re not affecting the
controller’s scope directly, but that is an unintentional side effect of
ng-repeat. If we had used our directive outside ng-repeat, we would
have ended up modifying the controller’s scope directly, which is bad
practice.
The default scope given in the link function (unless specified other‐
wise) is the scope that the parent has. Adding functions to the par‐
ent scope should always be frowned upon, because the parent should
ideally not be changed from within a child.
We are also still dependent on the variable name stock, which if renamed breaks our
UI. We’ll see in the next section how to remove this dependency as well.
Scope
By default, each directive inherits its parent’s scope, which is passed to it in the link
function. This can lead to the following problems:
• Adding variables/functions to the scope modifies the parent as well, which suddenly
gets access to more variables and functions.
182
| Chapter 11: Directives
www.it-ebooks.info
• The directive might unintentionally override an existing function or variable with
the same name.
• The directive can implicitly start using variables and functions from the parent.
This might cause issues if we start renaming properties in the parent and forget to
do it in the directive.
AngularJS gives us the scope key in the directive definition object to have complete
control over the scope of the directive element. The scope key can take one of three
values:
false
This is the default value, which basically tells AngularJS that the directive scope is
the same as the parent scope, whichever one it is. So the directive gets access to all
the variables and functions that are defined on the parent scope, and any modifi‐
cations it makes are immediately reflected in the parent as well.
true
This tells AngularJS that the directive scope inherits the parent scope, but creates
a child scope of its own. The directive thus gets access to all the variables and func‐
tions from the parent scope, but any modifications it makes are not available in the
parent. This is recommended if we need access to the parent’s functions and infor‐
mation, but need to make local modifications that are specific to the directive.
object
We can also pass an object with keys and values to the scope. This tells AngularJS
to create what we call an isolated scope. This scope does not inherit anything from
the parent, and any data that the parent scope needs to share with this directive
needs to be passed in through HTML attributes. This is the best option when cre‐
ating reusable components that should be independent of how and where they are
used.
In the object, we can identify what attributes are to be specified in the HTML when the
directive is used, and the types of values that will be passed in to the directive. In par‐
ticular, we can specify three types of values that can be passed in, which AngularJS will
directly put on the scope of the directive:
=
The = sign specifies that the value of the attribute in HTML is to be treated as a
JSON object, which will be bound to the scope of the directive so that any changes
done in the parent scope will be automatically available in the directive.
@
The @ sign specifies that the value of the attribute in HTML is to be treated as a
string, which may or may not have AngularJS binding expressions ({{ }}). The value
Understanding the Basic Options
www.it-ebooks.info
| 183
is to be calculated and the final value is to be assigned to the directive’s scope. Any
changes in the value will also be available in the directive.
&
The & sign specifies that the value of the attribute in HTML is a function in some
controller whose reference needs to be available to the directive. The directive can
then trigger the function whenever it needs to.
To make our directive fully contained and reusable, we can now pass the stock object
to our widget. This way, if the variable is renamed outside, the new variable can be passed
to our directive, making it independent of the name. This can be done using the =
binding with the scope object, and we can reassign the value to a consistent name on
the directive’s isolated scope. Let’s first see how we might want to change the usage of
the directive in the index.html file:
<!-- File: chapter11/directive-with-scope/index.html -->
<html>
<head>
<title>Stock Market App</title>
</head>
<body ng-app="stockMarketApp">
<div ng-controller="MainCtrl as mainCtrl">
<h3>List of Stocks</h3>
<div ng-repeat="s in mainCtrl.stocks">
<div stock-widget stock-data="s">
</div>
</div>
</div>
<script src="http://code.angularjs.org/1.2.16/angular.js"></script>
<script src="app.js"></script>
<script src="directive.js"></script>
</body>
</html>
The entire index.html file remains mostly the same except for a slight change in ngrepeat and the way we use our directive. We renamed the repeater from stock in
mainCtrl.stocks to s in mainCtrl.stocks. Our directive now says stock-data="s",
which is basically a way for us to pass the value we care about to our directive in a
consistent way. Now whether stock is named stock, s, or xyz, we pass it to our widget
using the stock-data key.
Let’s see how this is implemented in our directive:
// File: chapter11/directive-with-scope/directive.js
angular.module('stockMarketApp')
.directive('stockWidget', [function() {
return {
templateUrl: 'stock.html',
184
| Chapter 11: Directives
www.it-ebooks.info
restrict: 'A',
scope: {
stockData: '='
},
link: function($scope, $element, $attrs) {
$scope.getChange = function(stock) {
return Math.ceil(((stock.price - stock.previous) /
stock.previous) * 100);
};
}
};
}]);
We define an isolated scope in this code snippet by passing an object to the scope key
of the directive definition object. Inside the object, we define a stockData key whose
value is =. This has the following effects:
• It creates a variable called stockData on the directive’s isolated scope.
• In the HTML, the value of stockData can be set by using the attribute stock-data.
• The value of stockData is bound to the object that the HTML attribute stockdata points to. Any change in the controller’s value is immediately relected in the
stockData variable in the directive’s scope as well.
Let’s see how stock.html changes now:
<!-- File: chapter11/directive-with-scope/stock.html -->
<div class="stock-dash">
Name:
<span class="stock-name"
ng-bind="stockData.name">
</span>
Price:
<span class="stock-price"
ng-bind="stockData.price | currency">
</span>
Percentage Change:
<span class="stock-change"
ng-bind="getChange(stockData) + '%'">
</span>
</div>
In the HTML, all references to the parent’s stock variable have been replaced with the
directive’s own instance, stockData.
At this point, the HTML and the directive are no longer dependent on the context in
which they are used, because all the data and logic that the directive needs are either
contained (using the link function) or passed to it (using the isolated scope).
Understanding the Basic Options
www.it-ebooks.info
| 185
If at this point we had left the original variable in ng-repeat as stock (instead of s),
and our HTML referred to stock instead of stockData, we would see an empty value.
This is because the moment we isolate the scope, the directive can no longer access
anything from its parent scope because it is removed from the traditional scope
heirarchy.
Whenever we pass data using object binding to directives, it is done by reference. An‐
gularJS uses this fact to ensure that any changes done to the variable in the controller
are reflected inside the directive. But this also means that if the reference to the variable
gets reassigned in the directive, then the data-binding breaks in AngularJS.
Let’s take an example to see this in action. First, we add a method in our controller to
create all new stock objects called changeAllStocks, and a method called changeFirst
Stock to change the name of the first stock only:
// File: chapter11/directive-broken-reference/app.js
angular.module('stockMarketApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.stocks = [
{name: 'First Stock', price: 100, previous: 220},
{name: 'Second Stock', price: 140, previous: 120},
{name: 'Third Stock', price: 110, previous: 110},
{name: 'Fourth Stock', price: 400, previous: 420}
];
self.changeAllStocks = function() {
for (var i = 0; i < 4; i++) {
self.stocks[i] = {
name: 'Controller Stock',
price: 200,
previous: 250
};
}
};
self.changeFirstStock = function() {
self.stocks[0].name = 'Changed First Stock';
};
}]);
The index.html file adds two buttons that trigger these functions:
<!-- File: chapter11/directive-broken-reference/index.html -->
<html>
<head>
<title>Stock Market App</title>
</head>
<body ng-app="stockMarketApp">
<div ng-controller="MainCtrl as mainCtrl">
186
| Chapter 11: Directives
www.it-ebooks.info
<h3>List of Stocks</h3>
<div ng-repeat="s in mainCtrl.stocks">
<div stock-widget stock-data="s">
</div>
</div>
<button ng-click="mainCtrl.changeAllStocks()">
Change All Stock From Controller
</button>
<button ng-click="mainCtrl.changeFirstStock()">
Change First Stock From Controller
</button>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script src="app.js"></script>
<script src="directive.js"></script>
</body>
</html>
Our directive also adds a similar function that changes the current stock, called change
Stock:
// File: chapter11/directive-broken-reference/directive.js
angular.module('stockMarketApp')
.directive('stockWidget', [function() {
return {
templateUrl: 'stock.html',
restrict: 'A',
scope: {
stockData: '='
},
link: function($scope, $element, $attrs) {
$scope.getChange = function(stock) {
return Math.ceil(((stock.price - stock.previous) /
stock.previous) * 100);
};
$scope.changeStock = function() {
$scope.stockData = {
name: 'Directive Stock',
price: 500,
previous: 200
};
};
}
};
}]);
The stock.html file adds a button that triggers the directive’s changeStock function:
Understanding the Basic Options
www.it-ebooks.info
| 187
<!-- File: chapter11/directive-broken-reference/stock.html -->
<div class="stock-dash">
Name:
<span class="stock-name"
ng-bind="stockData.name">
</span>
Price:
<span class="stock-price"
ng-bind="stockData.price | currency">
</span>
Percentage Change:
<span class="stock-change"
ng-bind="getChange(stockData) + '%'">
</span>
<button ng-click="changeStock()">
Change Stock in Directive
</button>
</div>
Now when we run this, we’ll try out a few flows in the UI:
1. First, we click either of the two buttons in the main HTML (change all stocks, or
change first stock). After we click either of these, we click any of the individual (or
just the first, if the first stock name button in the controller was clicked) buttons
on a stock. When we do this, the UI first updates when we click the button in the
controller, and then updates when we click one of the individual buttons. The UI
updates as we would expect.
2. Next, we click the button on the first stock, followed by “Change First Stock from
Controller.” When we click the first button, the stock updates its values. But the
controller button now has no effect. This is because while previously an update
from the controller trickled down into the directive, a reference change (we reas‐
signed $scope.stockData) in the directive ensures that any changes in the directive
are no longer visible in the controller. Think of it this way: if the controller referred
to reference R1, then the directive initially also pointed to R1. But now that we
included $scope.stockData = something, the directive refers to R2. So a change
in R1 now has no effect in R2, which is why the stock-widget still shows old data
even though we change it in the controller.
3. Finally, we can try clicking the button on an individual stock first. Then we click
“Change All Stock from Controller.” This updates the individual stock first, and
then updates all the stocks to the same name. The reason this works, whereas it
didn’t before, is because we have the list of stocks in an ng-repeat. When we change
the reference from the controller, ng-repeat triggers again and creates new direc‐
tive instances with the correct reference. This is why this flow works and the pre‐
vious doesn’t.
188
| Chapter 11: Directives
www.it-ebooks.info
Now let’s expand our directive such that we can use the other two types of attributes on
the scope. We want to pass a string from our controller and get a hook from our directive
to trigger a function in our controller any time the user clicks a button inside the directive
instance. Let’s do this step by step. First, let’s look at our controller:
// File: chapter11/directive-with-scope-advanced/app.js
angular.module('stockMarketApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.stocks = [
{name: 'First Stock', price: 100, previous: 220},
{name: 'Second Stock', price: 140, previous: 120},
{name: 'Third Stock', price: 110, previous: 110},
{name: 'Fourth Stock', price: 400, previous: 420}
];
self.onStockSelect = function(price, name) {
console.log('Selected Price ', price, 'Name ', name);
};
}]);
We modify our controller to add a function to be triggered whenever a stock is selected.
This function is called with the price and name of the stock that was selected, and just
logs it to the console. We already removed the getChange function from this controller
when we moved it into the link function of the directive:
<!-- File: chapter11/directive-with-scope-advanced/index.html -->
<html>
<head>
<title>Stock Market App</title>
</head>
<body ng-app="stockMarketApp">
<div ng-controller="MainCtrl as mainCtrl">
<h3>List of Stocks</h3>
<div ng-repeat="s in mainCtrl.stocks">
<div stock-widget
stock-data="s"
stock-title="Stock {{$index}}. {{s.name}}"
when-select="mainCtrl.onStockSelect(stockPrice, stockName)">
</div>
</div>
</div>
<script src="http://code.angularjs.org/1.2.16/angular.js"></script>
<script src="app.js"></script>
<script src="directive.js"></script>
</body>
</html>
Understanding the Basic Options
www.it-ebooks.info
| 189
Our main index.html file has changed how we use our stock-widget. In addition to the
stock data, we now pass it a title as well as a function to call when the stock is selected.
The stock-title attribute takes a string value, which also allows for string interpola‐
tions (or handling the {{ }} syntax), so we can pass it a combination of hardcoded text
and variable values. when-select takes the reference of the function it calls, along with
the parameter names for the function. We define it as stockPrice and stockName, and
these keys are important as we’ll see when we define them in our HTML. Do note that
unlike an ng-bind or ng-show in which saying mainCtrl.onStockSelect() would ac‐
tually call this function, the when-select only makes a reference of this function avail‐
able to our directive, and it’s up to us to decide when this function is actually triggered:
<!-- File: chapter11/directive-with-scope-advanced/stock.html -->
<div class="stock-dash">
Name:
<span class="stock-name"
ng-bind="stockTitle">
</span>
Price:
<span class="stock-price"
ng-bind="stockData.price | currency">
</span>
Percentage Change:
<span class="stock-change"
ng-bind="getChange(stockData) + '%'">
</span>
<button ng-click="onSelect()">Select me</button>
</div>
Our directive’s template has also slightly changed. Instead of displaying the stock.name
for the name of the stock, we now display the passed-in stockTitle. This variable is
coming directly from the HTML attribute that was passed in to the stock-widget
directive. We also added a button to allow users to click to simulate the user selecting a
stock. Now let’s see how the directive has changed to get all of this to work:
// File: chapter11/directive-with-scope-advanced/directive.js
angular.module('stockMarketApp')
.directive('stockWidget', [function() {
return {
templateUrl: 'stock.html',
restrict: 'A',
scope: {
stockData: '=',
stockTitle: '@',
whenSelect: '&'
},
link: function($scope, $element, $attrs) {
$scope.getChange = function(stock) {
return Math.ceil(((stock.price - stock.previous) /
stock.previous) * 100);
190
| Chapter 11: Directives
www.it-ebooks.info
};
$scope.onSelect = function() {
$scope.whenSelect({
stockName: $scope.stockData.name,
stockPrice: $scope.stockData.price,
stockPrevious: $scope.stockData.previous
});
};
}
};
}]);
There are three things of note that changed in our directive.js file:
• Our scope key now has two additional attributes. The first is stockTitle (again,
stockTitle in JavaScript becomes stock-title in HTML), and its value is set to
@. This allows us to pass in strings to our directive, which can also have AngularJS
bindings (using the {{ }} syntax).
• We also added a whenSelect key with a value set to &. This makes the function that
was specified in our HTML available to the directive on its scope as whenSelect.
• Finally, we added an onSelect function to the scope that gets triggered on click of
the button in stock.html. This function ensures that the controller’s function gets
called whenever the user clicks the button in the directive.
The stockTitle binding takes the value of the interpolated string and sets it to
$scope.stockTitle automatically. We can then access it from the link function using
$scope.stockTitle, or directly from our HTML as we did in ng-bind for the name.
The whenSelect binding parses the function mentioned in the HTML and makes it
available on $scope.whenSelect. The difference is in how this function is triggered.
With function bindings, AngularJS tries to make passing in a function as generic and
customizable as possible. Let’s consider a simple example to demonstrate this.
Suppose the directive allows us to pass in a function that can take up to three parameters:
the stock price, the stock name, and the stock’s previous price. Now assume that the
directive always calls the function that’s passed in with these three parameters, in the
order specified before. That means that if a controller cares only about the stock’s pre‐
vious price, it has to take all three parameters and access only the third one. If a controller
only cares about the price and the previous price, again, it has to list all three parameters
and use only the first and third. What if there are more than three parameters? Then it
gets even more messy to use.
AngularJS tries to relieve this by making the following changes:
Understanding the Basic Options
www.it-ebooks.info
| 191
• Each directive, when triggering a function passed to it by the & key on its scope, can
define the various parameters that it makes available to the controller.
• Each controller can then decide which of these parameters it cares about (by using
the key related to the parameter in the HTML).
• The controller can decide the order and the number of parameters it wants from
the directive.
Thus, if a controller only cares about the price and the previous keys on the stock object,
it can just ask for those particular ones. This way, each controller is allowed to ask for
whatever it wants, in the order it wants.
The directive accomplishes this by passing an object to the function passed to it. In this
object, each key dictates one parameter that the controller can ask for, and the value
assigned to that key is the value the controller receives when it asks for that particular
key. In our previous example, the whenSelect function is called with an object with
three keys:
• stockName
• stockPrice
• stockPrevious
And its values correspond to the individual directive’s stock values. Our controller de‐
cides that it only cares about stockPrice and stockName in that order (by specifying it
in the HTML in that order, with those keys). Do note that the key names specified in
the HTML (in our index.html) have to exactly match those provided by our directive.
AngularJS will then inject those values in the order the controller asks for. Our controller
decided to ignore stockPrevious, while another controller could have just asked for
stockPrevious, ignoring the rest.
We can thus define a set of parameters that each controller can ask for as part of the
function it’s passing to a directive.
Replace
The last attribute of the directive we look at in this chapter is replace. In all the previous
examples, when AngularJS encounters our directive, it fetches the HTML for the di‐
rective and inserts it as a child of the element the directive is encountered on. In some
cases, we might not want the original element to remain and instead be completely
replaced with new HTML.
For such cases, AngularJS offers the replace key as part of the directive definition object.
The replace key takes a Boolean, and it defaults to false. If we specify it to true,
AngularJS removes the element that the directive is declared on, and replaces it with
192
| Chapter 11: Directives
www.it-ebooks.info
the HTML template from the directive definition object (or the contents from
templateUrl). Any existing attributes and classes on the HTML element that the di‐
rective is on are migrated from the old element to the new one.
If we run our existing directive without the replace attribute or after setting replace
to false, the generated HTML for each individual instance of the directive would look
something like the following:
<div stock-widget stock-data="s" stock-title="XYZ">
<div class="stock-dash">
Name:
<span class="stock-name"
ng-bind="stockTitle">
</span>
Price:
<span class="stock-price"
ng-bind="stockData.price | currency">
</span>
Percentage Change:
<span class="stock-change"
ng-bind="getChange(stockData) + '%'">
</span>
<button ng-click="onSelect()">Select me</button>
</div>
</div>
If we add replace: true to our directive definition object, the generated HTML for
each instance of the directive would change to the following:
<div stock-widget stock-data="s" stock-title="XYZ" class="stock-dash">
Name:
<span class="stock-name"
ng-bind="stockTitle">
</span>
Price:
<span class="stock-price"
ng-bind="stockData.price | currency">
</span>
Percentage Change:
<span class="stock-change"
ng-bind="getChange(stockData) + '%'">
</span>
<button ng-click="onSelect()">Select me</button>
</div>
We would lose the wrapper div, and all the attributes (stock-widget, stock-data, etc.)
would be migrated to the main div element of the HTML template of the directive.
In simple terms, the replace key decides whether the template of the directive replaces
the current element or the contents of the current element.
Understanding the Basic Options
www.it-ebooks.info
| 193
replace Is Deprecated
With AngularJS version 1.3 forward, the replace keyword in the
directive definition object has been deprecated. The next release of
AngularJS plans to remove it completely, so be careful and avoid
using the replace keyword if at all possible.
Conclusion
We looked at how simple it is to migrate snippets of our HTML along with functionality,
in a step-by-step manner, to its own directive. We first looked at directives like nginclude and ng-switch, which allow us to split our HTML into smaller, more man‐
ageable chunks. We then created our own directive using the directive definition object.
We added a template to our directive using the templateUrl key, its own functionality
and business logic using the link function, and made it fully contained and reusable by
passing it any data it needed using an isolated scope. We explored the various options
and ways in which we can pass data to isolated scope, including objects, strings, and
functions. Finally, we looked at the replace option of the directive.
In Chapter 12, we dig into unit testing of directives. We explore how to create simple
directive instances in our unit test, as well as considerations to keep in mind when
writing unit tests for directives.
194
| Chapter 11: Directives
www.it-ebooks.info
CHAPTER 12
Unit Testing Directives
In Chapter 11, we saw how to create simple, reusable components using directives. We
explored basic configuration of directives like template and templateUrl, the link
function, scope, and restrict. We have also seen in previous chapters how we can unit
test controllers and services. Both controllers and services are pure JavaScript functions
at the end of the day, and thus we could easily instantiate them and test them in our unit
tests. Directives, on the other hand, are directly associated with the DOM, because they
create HTML elements or modify their behavior.
In this chapter, we deal with the distinctions that we have to keep in mind while unit
testing directives. We see how we can instantiate instances of a directive in a unit test,
and learn how AngularJS works under the covers at the same time. By the end of this
chapter, we will have written a comprehensive unit test for our stock widget from the
previous chapter, while still making sure it is stable and runs fast.
Steps Involved in Testing a Directive
At its core, there are a few key steps (some of which parallel the unit tests for our
controllers) that you can use as a checklist when writing unit tests for a directive:
1. Get the $compile service injected into the unit test.
2. Create the HTML element that will trigger the directive you have created.
3. Create the scope against which you want the directive to be tested again.
4. Remember that there is no server in the unit test. If the directive loads a template
using the templateUrl key, add an expectation on $httpBackend for loading the
templateUrl and designate the HTML that’s to be used instead of the template in
the test.
195
www.it-ebooks.info
5. Compile the HTML element using the $compile service with the scope you’ve
created.
6. Write expectations on how the directive should be rendered and on the functions
that are defined in the link function.
The first five tests are going to be standard for any unit test we write for a directive. Only
the last two—where we start testing the rendering and business logic encapsulated in a
directive—change from one directive to another.
The Stock Widget Directive
To quickly recap what our directive definition was, let’s look at our stock directive
definition:
// File: chapter12/stockDirective.js
angular.module('stockMarketApp', [])
.directive('stockWidget', [function() {
return {
templateUrl: 'stock.html',
restrict: 'A',
scope: {
stockData: '=',
stockTitle: '@',
whenSelect: '&'
},
link: function($scope, $element, $attrs) {
$scope.getChange = function(stock) {
return Math.ceil(((stock.price - stock.previous) /
stock.previous) * 100);
};
$scope.onSelect = function() {
$scope.whenSelect({
stockName: $scope.stockData.name,
stockPrice: $scope.stockData.price,
stockPrevious: $scope.stockData.previous
});
};
}
};
}]);
In this example, we removed all the other controllers from the module, so we define a
new module that has only the stockWidget directive. We defined our directive with the
following key points:
• The rendering logic of the directive is encapsulated in stock.html, which is loaded
using templateUrl.
196
| Chapter 12: Unit Testing Directives
www.it-ebooks.info
• The directive is restricted to attributes and cannot be used as elements or a class
name.
• It has an isolated scope with the stockData and the stockTitle passed in along
with whenSelect, the function to be called when the user selects a stock.
• The link function defines a function to calculate the percentage change as well as
a function for the UI to call when a button is clicked.
Setting Up Our Directive Unit Test
In this section, we explore how to set up our unit test to a point where we can start
testing the functionality of our directive. To recap, the steps we need to accomplish are:
1. Get the $compile service injected into our test.
2. Set up our directive instance HTML.
3. Create and set up our scope with the necessary variables.
4. Determine the template to load because our server is mocked out.
5. Instantiate an instance of our directive using the $compile service.
6. Write our expectations for rendering and behavior.
In these examples, we are using the karma.conf.js file that we used
previously. The examples were run against AngularJS version 1.2.19
with Karma version 0.12.16. Because of the way we set up Karma, we
must make sure that the karma.conf.js file is in the same folder as the
tests and source code.
Let’s take a look at our unit test setup:
// File: chapter12/stockDirectiveRenderSpec.js
describe('Stock Widget Directive Rendering', function() {
beforeEach(module('stockMarketApp'));
var compile, mockBackend, rootScope;
// Step 1
beforeEach(inject(function($compile, $httpBackend, $rootScope) {
compile = $compile;
mockBackend = $httpBackend;
rootScope = $rootScope;
}));
it('should render HTML based on scope correctly', function() {
Setting Up Our Directive Unit Test | 197
www.it-ebooks.info
// Step 2
var scope = rootScope.$new();
scope.myStock = {
name: 'Best Stock',
price: 100,
previous: 200
};
scope.title = 'the best';
// Step 3
mockBackend.expectGET('stock.html').respond(
'<div ng-bind="stockTitle"></div>' +
'<div ng-bind="stockData.price"></div>');
// Step 4
var element = compile('<div stock-widget' +
' stock-data="myStock"' +
' stock-title="This is {{title}}"></div>')(scope);
// Step 5
scope.$digest();
mockBackend.flush();
// Step 6
expect(element.html()).toEqual(
'<div ng-bind="stockTitle" class="ng-binding">' +
'This is the best' +
'</div>' +
'<div ng-bind="stockData.price" class="ng-binding">' +
'100' +
'</div>');
});
});
Let’s walk through the preceding example step by step to see how we test the rendering
of our directive:
Step 1
The very first thing we do is inject all the necessary services to create and test our
directive in beforeEach. This includes $compile, which is necessary to create in‐
stances of our directive; $rootScope to be able to create scopes to test our directives
against; and the $httpBackend, to simulate and handle the server call to load the
template.
Step 2
We set up scope against which we will create our directive instance. This is similar
to the controller, which will have our data. We create the stock instance myStock as
well as a title variable on this scope.
198
| Chapter 12: Unit Testing Directives
www.it-ebooks.info
Step 3
Because our directive loads the template by URL, and because there is no server in
a unit test, we have to set expectations in $httpBackend on what server template
will be loaded and what its content will be. Because it is a unit test, we just give it
some dummy HTML that can be used to test element rendering and data accuracy.
Step 4
We create an instance of the directive. We first compile the HTML that triggered
our directive. This returns a compiled function, which we then call with scope to
compile it against.
Step 5
We digest the scope and flush the server requests. This is done to tell AngularJS
to update all the bindings in the HTML and ensures that the HTML that we specified
in the $httpBackend call gets loaded and rendered to write the rest of our test. We
dig into the $digest cycle in more detail in “The Digest Cycle” on page 206.
Step 6
At this point, we have a fully instantiated version of our directive. Here, we write
the expectations and tests for rendering to see if the data was picked up from the
scope correctly and that the HTML attributes were correctly passing along the data.
To run these tests, execute karma start from the folder containing the directive and
the unit test. The karma.conf.js file remains the same as before.
Next, let’s see how we might test the business logic and behavior of a directive. We use
the same setup and naming convention from before, but focus our tests on the functions
added to the scope instead of the HTML:
// File: chapter12/stockDirectiveBehaviorSpec.js
describe('Stock Widget Directive Behavior', function() {
beforeEach(module('stockMarketApp'));
var compile, mockBackend, rootScope;
// Step 1
beforeEach(inject(function($compile, $httpBackend, $rootScope) {
compile = $compile;
mockBackend = $httpBackend;
rootScope = $rootScope;
}));
it('should have functions and data on scope correctly',
function() {
// Step 2
var scope = rootScope.$new();
var scopeClickCalled = '';
scope.myStock = {
Setting Up Our Directive Unit Test | 199
www.it-ebooks.info
name: 'Best Stock',
price: 100,
previous: 200
};
scope.title = 'the best';
scope.userClick = function(stockPrice,
stockPrevious,
stockName) {
scopeClickCalled = stockPrice +
';' + stockPrevious +
';' + stockName;
};
// Step 3
mockBackend.expectGET('stock.html').respond(
'<div ng-bind="stockTitle"></div>' +
'<div ng-bind="stockData.price"></div>');
// Step 4
var element = compile(
'<div stock-widget' +
' stock-data="myStock"' +
' stock-title="This is {{title}}"' +
' when-select="userClick(stockPrice, ' +
'stockPrevious, stockName)">' +
'</div>'
)(scope);
// Step 5
scope.$digest();
mockBackend.flush();
// Step 6
var compiledElementScope = element.isolateScope();
expect(compiledElementScope.stockData)
.toEqual(scope.myStock);
expect(compiledElementScope.getChange(
compiledElementScope.stockData)).toEqual(-50);
// Step 7
expect(scopeClickCalled).toEqual('');
compiledElementScope.onSelect();
expect(scopeClickCalled).toEqual('100;200;Best Stock');
});
});
200
| Chapter 12: Unit Testing Directives
www.it-ebooks.info
In this example, Steps 1 through 5 are exactly the same as the previous example. What
changes are Step 6 and the newly added Step 7 (which is really just an extended Step 6,
because it adds more expectations on the functionality):
Step 6
We ask for the isolated scope of the element we’re working with. This is different
from element.scope(), which would give us the parent scope if called on an ele‐
ment with a directive. We then check if the directive has the correct stock data on
its own scope. We also check if the getChange function defined in the directive
works as expected.
Step 7
The last thing we test is the function callback. We have a variable defined in our
test, which is initially set to empty. We use this as a log of what happened in the test.
We then trigger the directive’s onSelect function (we could have also triggered it
through the UI, if our rendered HTML had a button). This should trigger the scope
userClick function, which sets the string variable. We then check if it is called with
the right values after the function is triggered.
In this way, we can test the behavior of our directive, and ensure that it is performing
the correct things given the right inputs and conditions.
Other Considerations
Some things to keep in mind when we’re unit testing directives:
How do we test the real HTML rendering?
If we use templateUrl while defining our directive, we can’t load the actual HTML
in a unit test because we don’t have a server. We have a few options at this point.
We can test our directive with a dummy template, which we’ve done previously in
our example. Another option would be to use template instead of templateUrl,
which ensures that the template is loaded as part of the directive. But this can get
quite messy. One final option is to use something like Grunt-HTML2JS, which takes
HTML templates and converts them into AngularJS services and factories. This
way, we can load our templates as services and make them available in our unit test.
What about dependent AngularJS services?
If our directive depends on an AngularJS service or factory, we can go about testing
it the very same way as we did earlier. AngularJS will figure out the dependencies
and inject them automatically. We can also get the services injected into our unit
tests like we did in Chapter 7.
The AngularJS bindings don’t work
Because we are manually creating the HTML and compiling it, the AngularJS life
cycle that takes care of updating the HTML with the latest and greatest data from
Other Considerations | 201
www.it-ebooks.info
the scope is not running. So we have to manually trigger it to let AngularJS know,
by calling scope.$digest(). We dive into this in detail in “The Digest Cycle” on
page 206. This is critical in a unit test.
Conclusion
In this chapter, we covered the last and most complex of unit test cases, which is unit
testing a directive. We took our stockWidget directive from Chapter 11 and created a
scaffolding to be able to unit test it. We then tested the rendering behavior, followed by
the functionality and business logic encapsulated in the directive.
In Chapter 13, we dig further into AngularJS directives. In particular, we cover the other
directive options we didn’t touch upon in Chapter 11, like controller, transclude,
and compile.
202
| Chapter 12: Unit Testing Directives
www.it-ebooks.info
CHAPTER 13
Advanced Directives
We covered creating simple, reusable components in Chapter 11, and saw how to unit
test them in Chapter 12. We dug into some common options, like template and tem
plateUrl, scope, restrict, and replace, and saw how to pass data to our directive to
make it fully self-contained. These cover 70–80% of the general use cases. The topics
we cover in this chapter address the other 20%, which deal with getting a deeper un‐
derstanding of the AngularJS life cycle.
In this chapter, we first review the AngularJS life cycle in detail. We then go over the
remaining options of the directive definition object, including transclude, compile,
controller, and require, as well as when to use them. With all that covered, we dig
into the common use case of extending ng-model with either creating new input direc‐
tives or creating our own custom validators. Finally, we see how we can integrate with
third-pary UI components like graphs and charts.
Life Cycles in AngularJS
Before we jump into any of the advanced directive definition options, it helps to get a
better understanding of how AngularJS goes about instantiating a directive and using
the directive definition object. And any talk about the directive life cycle is not complete
without understanding how AngularJS accomplishes much of its magic underneath the
covers.
AngularJS Life Cycle
When an AngularJS application is loaded in our browser window, the following events
are executed in order:
203
www.it-ebooks.info
1. The HTML page is loaded:
a. The HTML page loads the AngularJS source code (with jQuery optionally loaded
before).
b. The HTML page loads the application’s JavaScript code.
2. The HTML page finishes loading.
3. When the document ready event is fired, AngularJS bootstraps and searches for
any and all instances of the ng-app attribute in the HTML:
a. If AngularJS is bootstrapped manually, this needs to be triggered by the code we
write.
4. Within the context of each (there could be more than one) ng-app, AngularJS starts
its magic by running the HTML content inside ng-app through what is known as
the compile step:
a. The compile step goes through each line of HTML and looks for AngularJS
directives.
b. For each directive, it executes the necessary code as defined by that directive’s
definition object. In case the directive creates or loads new HTML, it recursively
descends into HTML until all directives are recognized by the compiler.
c. At the end of the compile step, a link function is generated for each directive
that has a handle on all the elements and attributes that need to be controlled by
AngularJS.
5. AngularJS takes the link function and combines it with scope. The scope has the
variables and contents that need to be displayed in the UI. This combination gen‐
erates the live view that the user sees and interacts with:
a. AngularJS will take the variables in scope, and display them in the UI if the
HTML refers to a scope variable.
b. Each controller and subcontroller is instantiated with its own scope that will be
used to display data in the UI.
c. AngularJS also adds watchers and listeners for all the directives and bindings to
ensure it knows which fields it needs to track and which data field to bind with.
6. At the end of this, we have a live, interactive view with the content filled in for the
user.
This entire flow is demonstrated in Figure 13-1.
204
| Chapter 13: Advanced Directives
www.it-ebooks.info
Figure 13-1. The AngularJS initial life cycle
At this point, as we mentioned, the first view that the user sees and interacts with has
been generated, with the content dynamically being retrieved from the controller and
scopes. Now how and when should the UI be updated? Think about the ng-bind and
ng-model directives that change the state and the UI based on the state. When would
the values from it need to be updated in the HTML?
• The first inclination might be to periodically check if any of the variables have
changed, and then update the UI if they have. This requires a polling function that
checks every 1 second, for example. But this is suboptimal and would result in the
entire framework slowing to a crawl for a medium-sized application. So clearly this
is a bad idea.
Life Cycles in AngularJS
www.it-ebooks.info
| 205
• The second, better approach (and the one AngularJS uses) is to be smart about
updating the UI. If we think about it, the model that is driving the application cannot
randomly change. It can change only in response to one of the following events:
— The user makes a modification (types in, checks a checkbox, clicks a button, etc.)
in a form or input element.
— A server request is made and its response comes back (XHR or asynchronous
request returning).
— A $timeout or $interval is used to execute something asynchronously.
Outside of these events, the data cannot change randomly on its own. So AngularJS
adds watchers for all its bindings and ng-model. And whenever one of the aforemen‐
tioned events happens, AngularJS checks its watchers and bindings to see if anything
has changed. If nothing has changed, AngularJS proceeds without doing anything. But
if AngularJS finds that one of the fields it’s controlling has changed underneath, it trig‐
gers an update of the UI.
The Digest Cycle
The update cycle that we mentioned earlier has a name in AngularJS: the digest cycle.
The digest cycle in AngularJS is responsible for keeping the UI up to date in an AngularJS
application. The AngularJS UI update cycle happens as follows:
1. When the application loads, or when any HTML is loaded within AngularJS, An‐
gularJS runs its compilations step, and keeps track of all the watchers and listeners
that are needed for the HTML (these would be from ng-bind, ng-class, and so
on). When linked with the scope, we get the actual current values that are then
displayed in the UI.
2. AngularJS also keeps track of all the elements that are bound to the HTML for each
scope.
3. When one of the events mentioned in the previous section (such as a user click)
happens, AngularJS triggers the digest cycle.
4. In the digest cycle, AngularJS starts from $rootScope and checks each watcher in
the scope to see if the current value differs from the value it’s displaying in the UI.
5. If nothing has changed, it recurses to all the parent scopes and so on until all the
scopes are verified.
6. If AngularJS finds a watcher at any scope that reports a change in state, AngularJS
stops right there, and reruns the digest cycle.
7. The digest cycle is rerun because a change in a watcher might have an implication
on a watcher that was already evaluated beforehand. To ensure that no data change
is missed, the digest cycle is rerun.
206
| Chapter 13: Advanced Directives
www.it-ebooks.info
8. AngularJS reruns the digest cycle every time it encounters a change until the digest
cycle stabilizes. On average, this takes two to three cycles for a normal AngularJS
application.
a. To prevent AngularJS from getting into an infinite loop where one watcher up‐
dates another model and vice versa, AngularJS caps the digest cycle reruns to 10
by default.
9. When the digest stabilizes, AngularJS accumulates all the UI updates and triggers
them at once.
This entire flow is represented in Figure 13-2.
Figure 13-2. AngularJS digest life cycle
Life Cycles in AngularJS
www.it-ebooks.info
| 207
Lightweight Watchers
Because watchers and listeners can be executed multiple times by
AngularJS for a single update, it’s recommended that any watch that
we add execute really fast. Thus it is almost always a bad idea to do
time- and CPU-intensive tasks within watchers.
The general rule of thumb is that no individual watcher function take
more than 20 microseconds, and no more than 2,000 variables be
tracked or watched at any one point in time.
Directive Life Cycle
An individual directive has its own life cycle that follows a similar structure to the life
cycle we saw before, but with a few key additions and differences. Here are the key steps
in a directive’s life:
1. When the application loads, the directive definition object is triggered. This hap‐
pens only once, so anything declared or expressed before returning the directive
definition object can be treated as the constructor for the directive, and will be
executed once the very first time the application loads.
2. Next, when the directive is encountered in the HTML the very first time, the tem‐
plate for the directive is loaded (either asynchronously from the server or directly
as a template from the definition). In either case, the template is cached and reused
in further instances of the directive.
3. This template is then compiled and AngularJS handles the other directives present
in the HTML. This generates a link function that can be used to link the directive
to a scope.
4. The scope for the directive instance is created or acquired. This could be the parent
scope, a child of the parent scope, or an isolated scope as the case might be (as
discussed in Chapter 11).
5. The link function (and the controller) execute for the directive. This is where we
add functionality that is specific to each instance of the directive.
We will now see how the other directive definition keys help us in creating more ad‐
vanced directives.
Transclusions
AngularJS directives have a concept of transclusions to allow us to create reusable di‐
rectives where each implementation might need to render a certain section of the UI
differently. To understand how this works, we will first take the stock widget directive
from Chapter 11 and then modify it to use transclusions. The following code snippets
208
| Chapter 13: Advanced Directives
www.it-ebooks.info
are mostly unchanged stock widget directives and related HTML and controller code
from Chapter 11.
First up is the directive declaration:
// File: chapter13/directive-no-transclusion/directive.js
angular.module('stockMarketApp')
.directive('stockWidget', [function() {
return {
templateUrl: 'stock.html',
restrict: 'A',
scope: {
stockData: '='
},
link: function($scope, $element, $attrs) {
$scope.getChange = function(stock) {
return Math.ceil(((stock.price - stock.previous) /
stock.previous) * 100);
};
}
};
}]);
This is mostly unchanged, and uses the simpler version (instead of the one with function
binding that was used in later examples in Chapter 11). The HTML for the directive
also remains largely unchanged:
<!-- File: chapter13/directive-no-transclusion/stock.html -->
<div class="stock-dash">
Name:
<span class="stock-name"
ng-bind="stockData.name">
</span>
Price:
<span class="stock-price"
ng-bind="stockData.price | currency">
</span>
Percentage Change
<span class="stock-change"
ng-bind="getChange(stockData) + '%'">
</span>
</div>
The controller also remains unchanged from the example in Chapter 11:
// File: chapter13/directive-no-transclusion/app.js
angular.module('stockMarketApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.stocks = [
{name: 'First Stock', price: 100, previous: 220},
{name: 'Second Stock', price: 140, previous: 120},
Transclusions | 209
www.it-ebooks.info
{name: 'Third Stock', price: 110, previous: 110},
{name: 'Fourth Stock', price: 400, previous: 420}
];
}]);
The main index.html file is the only file that gets a minor change, but only in how we
use the directive we created:
<!-- File: chapter13/directive-no-transclusion/index.html -->
<html>
<head>
<title>Stock Market App</title>
</head>
<body ng-app="stockMarketApp">
<div ng-controller="MainCtrl as mainCtrl">
<h3>List of Stocks</h3>
<div ng-repeat="s in mainCtrl.stocks">
<div stock-widget stock-data="s">
This content will be blown away
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script src="app.js"></script>
<script src="directive.js"></script>
</body>
</html>
In index.html, we added some content inside the stock-widget directive by trying to
provide some custom rendering data that we want included as part of our rendering.
Now when we run this application (remember to start a local server first, as we saw in
previous chapters), we see that the content we added as a child of our directive in
index.html is ignored, and only the contents of template are loaded. This is the de‐
fault behavior of any AngularJS directive, in that if it has a template or templateUrl,
it will remove the content of the element where the directive is found and replace it with
the template specified in the directive definition object.
This is fine for a majority of cases, but when we need custom HTML to be added as part
of our directive (think accordions or tab widgets), this is not what we want. In these
cases, where we want AngularJS to respect both the inner contents where the directive
is used, as well as the original template of the directive, we use the concept of
transclusion.
210
| Chapter 13: Advanced Directives
www.it-ebooks.info
Basic Transclusion
Basic transclusion can be thought of as a two-step process:
1. First, we tell the directive that we are going to use transclusion as part of this di‐
rective. This tells AngularJS that whenever the directive is encountered in the
HTML, to make a copy of its content and store it so that it’s not lost when AngularJS
replaces it with the directive’s template. This is accomplished by setting the key
transclude to true as part of the directive definition object.
2. Second, we need to tell AngularJS where to put the content that was stored in the
template. This is accomplished by using the ng-transclude directive, which en‐
sures that the content that was captured is made a child of the element in the di‐
rective template.
Let’s take a look at how we might modify the preceding example to use transclusions.
The controller and the index.html file remain unchanged from before, so we won’t repeat
it. The first change we make is to the directive definition:
// File: chapter13/directive-transclusion/directive.js
angular.module('stockMarketApp')
.directive('stockWidget', [function() {
return {
templateUrl: 'stock.html',
restrict: 'A',
transclude: true,
scope: {
stockData: '='
},
link: function($scope, $element, $attrs) {
$scope.getChange = function(stock) {
return Math.ceil(((stock.price - stock.previous) /
stock.previous) * 100);
};
}
};
}]);
In the directive definition, we added the key transclude and set its value to true. There
is no other change in the definition.
Next we take a look at the stock.html file to see how it changes:
<!-- File: chapter13/directive-transclusion/stock.html -->
<div class="stock-dash">
<span ng-transclude></span>
Price:
<span class="stock-price"
Transclusions | 211
www.it-ebooks.info
ng-bind="stockData.price | currency">
</span>
Percentage Change:
<span class="stock-change"
ng-bind="getChange(stockData) + '%'">
</span>
</div>
We removed the name and the binding for stockData.name and replaced it with a span,
and added the ng-transclude directive on it. The end effect of this change is as follows:
• When AngularJS encounters the stock-widget directive in index.html, it grabs its
inner content (Recommended Stock : {{s.name}}) and stores it for later use.
• When it includes the directive’s template in the instance, it looks for the ngtransclude directive, and places the content that it stored in step 1 as a child in the
element with ng-transclude.
• Thus, when the UI renders for each stock in the stocks array, we get the Recom‐
mended Stock text followed by each stock’s name.
Scope of an ng-transclude
You might be wondering how ng-transclude works, especially be‐
cause we isolated the scope for our directive, whereas the ngtransclude content explicitly refers to something that is available in
the scope of ng-repeat, but not inside the directive’s scope.
When AngularJS encounters transclude, it clones the HTML be‐
fore replacing it with the template or templateUrl contents. Then,
when it encounters ng-transclude, it compiles the transcluded con‐
tent, but links it to the parent scope instead of the isolated scope of
the directive. Thus, the transcluded content still has access to the
parent controller and its content, while the directive HTML has an
isolated scope (or a new scope, as the case might be).
Thus, the transcluded content and the directive content form a sib‐
ling relationship but do not share the same scope. This is because the
transcluded content is not expected to know the inner workings of
the directive, but is instead expected to be dependent on the usage
and context in which it is used.
Advanced Transclusion
In the previous example, we covered the case where we have some content specific to
the usage of the directive that we need placed inside our directive at a certain location.
We accomplished this using the transclude keyword along with the ng-transclude
directive inside our directive.
212
| Chapter 13: Advanced Directives
www.it-ebooks.info
Another common use case with dynamic user-dependent content is to have multiple
copies of the template made and used as and when we need it. For example, instead of
displaying the content of our directive once, we might need to show it multiple times
inside a carousel. This is also what happens with the ng-repeat directive, where we
define a template that needs to be repeated, and then for each instance we create the
template and insert it in our directive dynamically.
Let’s see how we can accomplish this use case using the transclude property of our
directive definition object. In our next example, we try to create a trivial replacement
for the ng-repeat that will pick up some variables from our outer scope, and add some
variables for each instance. We won’t make it auto-update, but will instead focus on how
to use transclusion to render multiple instances of our template.
First up, the app.js file, which doesn’t change:
// File: chapter13/directive-advanced-transclusion/app.js
angular.module('stockMarketApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.stocks = [
{name: 'First Stock', price: 100, previous: 220},
{name: 'Second Stock', price: 140, previous: 120},
{name: 'Third Stock', price: 110, previous: 110},
{name: 'Fourth Stock', price: 400, previous: 420}
];
}]);
This remains exactly the same. Next up is the main index.html file, which also gives us
a peek into how we want to use this directive:
<!-- File: chapter13/directive-advanced-transclusion/index.html -->
<html>
<head>
<title>Stock Market App</title>
</head>
<body ng-app="stockMarketApp">
<div ng-controller="MainCtrl as mainCtrl">
<h3>List of Stocks</h3>
<div simple-stock-repeat="mainCtrl.stocks">
We found {{stock.name}} at {{currentIndex}}
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script src="app.js"></script>
<script src="directive.js"></script>
</body>
</html>
Transclusions | 213
www.it-ebooks.info
We replaced the usage of our stock-widget directive. We replaced ng-repeat with our
new directive, called simple-stock-repeat. This directive takes any array from our
controller and uses the content of the directive as a template to create one for each
instance in our array. The directive exposes the current item as the variable stock, and
also exposes additional information like the index of the item in the variable curren
tIndex. When we run the previous HTML, we expect that the “We found…” statement
be repeated four times, once for each stock, each one with its index in the array.
Now let’s see how we accomplish this in the directive definition:
// File: chapter13/directive-advanced-transclusion/directive.js
angular.module('stockMarketApp').directive('simpleStockRepeat',
[function() {
return {
restrict: 'A',
// Capture and replace the entire element
// instead of just its content
transclude: 'element',
// A $transclude is passed in as the fifth
// argument to the link function
link: function($scope, $element, $attrs, ctrl, $transclude) {
var myArray = $scope.$eval($attrs.simpleStockRepeat);
var container = angular.element(
'<div class="container"></div>');
for (var i = 0; i < myArray.length; i++) {
// Create an element instance with a new child
// scope using the clone linking function
var instance = $transclude($scope.$new(),
function(clonedElement, newScope) {
// Expose custom variables for the instance
newScope.currentIndex = i;
newScope.stock = myArray[i];
});
// Add it to our container
container.append(instance);
}
// With transclude: 'element', the element gets replaced
// with a comment. Add our generated content
// after the comment
$element.after(container);
}
};
}]);
We introduced quite a few new things in this directive, so let’s walk through each one
individually:
214
| Chapter 13: Advanced Directives
www.it-ebooks.info
• The first change is in the transclude key of the directive definition object. In the
previous example, we had set it to true, which tells AngularJS to pick up the content
of the element on which the directive is applied and retain it. When we specify
transclude to element, it notifies AngularJS to copy the entire element, along with
any directives that might be present on it for transclusion.
• The link function, as we saw in Chapter 11, takes three arguments by default: the
scope of the directive, the element on which the directive is present, and the at‐
tributes on the element. In addition, we can pass directive controllers to the directive
as the fourth argument. But the fifth argument is what we care about, which is a
transclusion function that is generated only when we use the transclude key in the
directive definition object.
This transclude function is a constructor that allows us to create new instances of
our template as many times as needed depending on our use case. The function
takes an optional scope (if a new scope is needed for the element; otherwise, it
inherits the directive’s scope) and a mandatory clone linking function as the second
argument.
• In the very first line, we evaluate the variable mentioned in the HTML along with
the directive to get a handle on the array that we want to repeat on. This is accom‐
plished by calling $eval on the scope with a string that contains the JavaScript we
want to evaluate in the context of the scope.
• Because transclude element copies the entire element, it also removes the element
from the HTML. So we create a container element within which to put all our
instances that we create.
• We then run a for loop for each instance in our array, and call the transclude
function that is passed to the linking function. This returns a new HTML element
that is a fully compiled and linked version of our template that can then be inserted
into our main body.
• As mentioned previously, the first argument to the transclude function is an op‐
tional scope. In our example, we create a new child scope. This is so that any mod‐
ification made to the scope does not get reflected in the parent scope. This is always
a good practice to make sure no global states step on each other.
• The second argument that we pass to the transclude function is a linking function
for the cloned element. This is where we add any behavior or variable that is specific
to this instance of the template (like currentIndex and stock for us).
• We then add the created instance of our template to the container element, and then
finally add the container element after our directive instance (which at this point
is just a comment node in the HTML). Without this step, we would have fully
Transclusions | 215
www.it-ebooks.info
compiled, working AngularJS-ready DOM elements that wouldn’t appear in the
rendered UI.
In general, we can use the transclude concept any time we need a component whose
templating and UI changes depend on its usage and context. We can decide whether or
not we need transclude as follows:
• Does each user of the directive need to specify his own template or rendering logic?
If so, then use transclude.
• Is only the content of the directive important, or is the element on which the di‐
rective is applied necessary as well? Use transclude: true in the former, and
transclude: element in the latter.
• If it’s a simple matter of displaying the transcluded content as is, use the ngtransclude directive directly in your directive template.
• Do we need to generate multiple copies of the template or add behavior, variables,
and business logic to the scope on which the transclusion is done? If so, inject the
transcluding function into our link function.
• Call the transclusion function with an optional new scope (this is recommended)
and linking function for that instance. Within the linking function, add the func‐
tions and variables that the template needs.
When creating directives for tasks such as carousels and infinite scrolling, you can
consider using transclusions to make your life easier.
Directive Controllers and require
In Chapter 11, we introduced the concept of the link function for directives, which we
mentioned is the ideal place to add behavior and business logic to your directives. But
if you peruse the AngularJS source code, or go through the AngularJS documentation
on directives, you will notice that directives also allow you to define controllers.
The reason we use the link function and not controllers to define our directive-specific
behavior is that directive controllers are present for a completely different use case.
Directive controllers are used in AngularJS for inter-directive communication, while
link functions are fully contained and specific to the directive instance. By interdirective communication, we mean when one directive on an element wants to com‐
municate with another directive on its parent or on the same element. This encompasses
sharing state or variables, or even functions.
Let’s see how we might create and use directive controllers to create a tabs directive.
Now instead of letting the top-level tabs directive recursively create tabs inside of it,
we will also create a tab directive for each individual tab in the set of tabs. Now obviously,
216
| Chapter 13: Advanced Directives
www.it-ebooks.info
the tab directive can only be used in the context of the set of tabs. It also needs to be able
to let the parent know it was selected so the tab set can decide to hide and show content
accordingly. Let’s see how we can accomplish this using directive controllers:
// File: chapter13/directive-controllers/app.js
angular.module('stockMarketApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.startedTime = new Date().getTime();
self.stocks = [
{name: 'First Stock', price: 100, previous: 220},
{name: 'Second Stock', price: 140, previous: 120},
{name: 'Third Stock', price: 110, previous: 110},
{name: 'Fourth Stock', price: 400, previous: 420}
];
}]);
The app.js file that defines our controller has not changed much. It still defines a list of
stocks, and adds a variable called startedTime. Each of these variables will be shown
in separate tabs in our final UI. Next, let’s take a look at how we use the tabs and tab
directives we’re building in the index.html file:
<!-- File: chapter13/directive-controllers/index.html -->
<html>
<head>
<title>Stock Market App</title>
<link rel="stylesheet" href="main.css">
</head>
<body ng-app="stockMarketApp">
<div ng-controller="MainCtrl as mainCtrl">
<tabs>
<tab title="First Tab">
This is the first tab.
The app started at {{mainCtrl.startedTime | date}}
</tab>
<tab title="Second Tab">
This is the second tab
<div ng-repeat="stock in mainCtrl.stocks">
Stock Name: {{stock.name}}
</div>
</tab>
</tabs>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script src="app.js"></script>
<script src="tabs.js"></script>
<script src="tab.js"></script>
Directive Controllers and require | 217
www.it-ebooks.info
</body>
</html>
We define a div that’s controlled by MainCtrl, inside of which we want to display two
tabs. The first tab has the title "First Tab", and when the tab is selected, displays the
date and time on which the application was opened. This content comes from
MainCtrl. The second tab as the title "Second Tab", and displays the names of each
stock in the stocks array in MainCtrl.
With the usage out of the way, we can now take a look at how the directive is created.
Let’s first look at the tabs directive, which acts as the container for a set of tabs:
// File: chapter13/directive-controllers/tabs.js
angular.module('stockMarketApp')
.directive('tabs', [function() {
return {
restrict: 'E',
transclude: true,
scope: true,
template: '<div class="tab-headers">' +
' <div ng-repeat="tab in tabs"' +
'
ng-click="selectTab($index)"' +
'
ng-class="{selected: isSelectedTab($index)}">' +
'
<span ng-bind="tab.title"></span>' +
' </div>' +
'</div>' +
'<div ng-transclude></div> ',
controller: function($scope) {
var currentIndex = 0;
$scope.tabs = [];
this.registerTab = function(title, scope) {
if ($scope.tabs.length === 0) {
scope.selected = true;
} else {
scope.selected = false;
}
$scope.tabs.push({title: title, scope: scope});
};
$scope.selectTab = function(index) {
currentIndex = index;
for (var i = 0; i < $scope.tabs.length; i++) {
$scope.tabs[i].scope.selected = currentIndex === i;
}
};
$scope.isSelectedTab = function(index) {
return currentIndex === index;
};
}
};
}]);
218
| Chapter 13: Advanced Directives
www.it-ebooks.info
The tabs directive leverages the transclusion concept from before, as well as defines a
controller. Let’s take a more detailed look at the most interesting parts of the tabs
directive:
• The directive uses transclusion (not element-level transclusion, though) to pick
up the individual tabs and add the tab titles above them.
• The tabs directive also defines its own scope, because it needs to add certain func‐
tions to the scope, and we don’t want to collide or override any properties or func‐
tions on the parent.
• The directive template defines a section to repeat over individual tabs (stored in a
tabs array on the scope) and display them. The template also handles clicking an
individual tab as well as highlighting the selected tab using functions on the scope
of the directive.
• The tabs directive template defines a div in the template where the contents are
translcuded into using ng-transclude. This is where the entire content of the tabs
directive in the HTML (each individual tab) gets placed during runtime.
• Next, instead of defining a link function, we define a directive controller. The
reason we do this is because we want children directives of the tabs directive to be
able to access certain functionality from the tabs directive. Whenever we need to
communicate between child and parent directives, or between sibling directives,
we should consider using directive controllers.
• A directive controller is a function that gets the scope and element injected in.
This is similar to the link function that we’ve been using so far, but the difference
is that functions we define in the controller on this can be accessed by child or
sibling controllers (we’ll see how in just a bit). Thus, the controller can define
functions that are specific to the directive instance by defining them on $scope as
we have been doing so far, and define the API or accessible functions and variables
by defining them on this or the controller’s instance.
• In this case, we define the tabs variable on the scope, as well as selectTab and
isSelected on it, which are used by the directive template for selecting tabs and
highlighting the selected tab. Both of these functions set or unset a selected variable
on the scope of an individual tab to handle showing and hiding tabs.
• We also define a function named registerTab on the controller’s instance. This
function, because it is not defined on the scope, will not be accessible from the
directive’s HTML. This function adds a title and scope object to an array. This
array is used to display the list of tabs at the top.
Now, let’s look at the tab directive to see how it hooks into the tabs directive and
leverages the controller we defined:
Directive Controllers and require | 219
www.it-ebooks.info
// File: chapter13/directive-controllers/tab.js
angular.module('stockMarketApp')
.directive('tab', [function() {
return {
restrict: 'E',
transclude: true,
template: '<div ng-show="selected" ng-transclude></div>',
require: '^tabs',
scope: true,
link: function($scope, $element, $attr, tabCtrl) {
tabCtrl.registerTab($attr.title, $scope);
}
};
}]);
The tab directive is much simpler:
1. The first thing the tab directive does is set up transclusion, because it defines a
template of its own.
2. In the template, we use the ng-transclude to add the content inside a div, and add
a condition to selectively hide and show the div based on a selected variable on
the scope.
3. We add a new key, require, and use the value ^tabs (we’ll see what the ^ symbol
means in “require Options” on page 221). This tells AngularJS that for the tab directive
to work, it requires that one of the parent elements in the HTML be the tabs
directive, and we want its controller to be made available to the tab directive.
4. We define a new scope for this directive so that local variables don’t override any‐
thing in the parent scope.
5. In the link function, we get the controller we required as the fourth argument (after
scope, element, and attributes). This is an instance of the controller we defined in
the tabs directive, and is dynamically injected based on what AngularJS finds.
6. Inside the link function, we register the tab with the parent tabs directive function
that we defined earlier.
Now the flow in the application is as follows:
1. When the tabs directive (the parent one) is found in the HTML, the content is
transcluded and a space for the tabs is preserved in the HTML (using ng-repeat).
The actual tabs are inserted below it.
2. Each individual tab registers with the parent, so that the parent tabs controller can
decide which tab is currently selected, and highlight and hide/show the other tabs
as needed.
220
| Chapter 13: Advanced Directives
www.it-ebooks.info
3. Each tab again uses transclusion on its content to wrap it in a container so that it
can be hidden and shown as needed.
4. On registration, the tabs controller sets the very first tab as selected (using a scope
variable on the scope passed up to the tabs controller).
5. After that, the ng-click handles hiding and showing of the individual tabs using
the functions defined on the scope of the tabs directive.
require Options
The require keyword in the directive definition object either takes a string or an array
of strings, each of which is the name of the directive that must be used in conjunction
with the current directive.
For example:
require: 'tabs'
tells AngularJS to look for a directive called tabs, which exposes a controller on the
same element the directive is on. Similarly:
require: ['tabs', 'ngModel']
tells AngularJS that both the tabs and ng-model directives must be present on the ele‐
ment our directive is used on. When used as an array, the link function gets an array
of controllers as the fourth argument, instead of just one controller.
Now, each individual string can take some prefixes, which define how AngularJS should
behave when finding these directives. For example:
require: 'tabs'
implies that AngularJS should locate the directive tabs on the same element, and throw
an error if it’s not found:
require: '?tabs'
This implies that AngularJS should try to locate the directive tabs on the same element,
but pass null as the fourth argument to the link function if it isn’t found. That is,
prefixing ? tells AngularJS to treat the directive as an optional dependency.
Furthermore, we can also tell AngularJS to look for a directive not on itself, but on its
parent chain. This can be done as:
require: '^tabs'
This is what we used in our previous example, which tells AngularJS that the tabs
directive must be present on one of the parent elements (not necessarily the immediate
parent).
Directive Controllers and require | 221
www.it-ebooks.info
We can also mix and match these prefixes. For example:
require: '?^tabs'
implies that a parent element of our directive may or may not have the tabs directive,
but if it is present, it should be injected into our directive link function.
Input Directives with ng-model
We saw in the previous section how we can create directive controllers and use them to
communicate between directives and to shared state. In this section, we leverage this
concept, extend the already existing ng-model directive, and integrate with third-party
input widgets. The thought behind this is that ng-model is already good at what it does,
which is the two-way data-binding. If we introduce a new input widget in our AngularJS
application, we want it to behave the same way, in that we add the widget in out HTML,
add an ng-model to it, and be done with it.
We’ll look at how easy it is to accomplish this by incorporating a third-party input slider
into our application. For the purpose of this example, we use the awesome jQuery-based
noUiSlider and wrap it inside a reusable directive. We start with the main index.html
file, which demonstrates how we want to use this directive:
<!-- File: chapter13/directive-slider/index.html -->
<html>
<head>
<title>Slider App</title>
<link rel="stylesheet" href="jquery.nouislider.css">
<style type="text/css">
.slider {
display: block;
height: 20px;
margin: 20px;
}
</style>
</head>
<body ng-app="sliderApp">
<div ng-controller="MainCtrl as mainCtrl">
<div>
The current value of the slider is {{mainCtrl.selectedValue}}
</div>
<no-ui-slider class="slider"
ng-model="mainCtrl.selectedValue"
range-min="500"
range-max="5000">
</no-ui-slider>
222
| Chapter 13: Advanced Directives
www.it-ebooks.info
<div>
<input type="number"
ng-model="mainCtrl.textValue"
min="500"
max="5000"
placeholder="Set a value">
<button ng-click="mainCtrl.setSelectedValue()">
Set slider value
</button>
</div>
</div>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="jquery.nouislider.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script src="app.js"></script>
<script src="noui-slider.js"></script>
</body>
</html>
The HTML is mostly straightforward. We included the CSS for noUiSlider in the
HEAD tag, and at the end, added a dependency on jQuery first, and then the noUiSlid
er JavaScript file before adding our standard AngularJS dependencies.
As part of the main div controlling the application, we first display the current value of
the selectedValue variable from the controller. We then use our no-ui-slider direc‐
tive and bind it to the same selectedValue variable that we print before using ngmodel. We also give it a fixed range using the range-min and range-max attributes
on it.
We then have another section that holds a text box bound to a different variable (text
Value) and a button. Clicking the button should set the value of the slider to the value
in the text box.
Let’s look at the controller driving this application:
// File: chapter13/directive-slider/app.js
angular.module('sliderApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.selectedValue = 2000;
self.textValue = 4000;
self.setSelectedValue = function() {
self.selectedValue = self.textValue;
};
}]);
Directive Controllers and require | 223
www.it-ebooks.info
The controller for our slider application is really tiny. It’s just a bunch of model variables
(selectedValue and textValue), and a function to take the value of textValue and set
it on selectedValue. The aim is that when the setSelectedValue function is triggered,
the slider takes the value that is present in the text box. Both variables have some initial
value so that the the UI is slightly more interesting.
Now we can finally see how the noUiSlider directive accomplishes this:
// File: chapter13/directive-slider/noui-slider.js
angular.module('sliderApp')
.directive('noUiSlider', [function() {
return {
restrict: 'E',
require: 'ngModel',
link: function($scope, $element, $attr, ngModelCtrl) {
$element.noUiSlider({
// We might not have the initial value in ngModelCtrl yet
start: 0,
range: {
// $attrs by default gives us string values
// nouiSlider expects numbers, so convert
min: Number($attr.rangeMin),
max: Number($attr.rangeMax)
}
});
// When data changes inside AngularJS
// Notify the third party directive of the change
ngModelCtrl.$render = function() {
$element.val(ngModelCtrl.$viewValue);
};
// When data changes outside of AngularJS
$element.on('set', function(args) {
// Also tell AngularJS that it needs to update the UI
$scope.$apply(function() {
// Set the data within AngularJS
ngModelCtrl.$setViewValue($element.val());
});
});
}
};
}]);
How do we accomplish creating the slider directive? Let’s take a look, step by step, at
the directive we created:
1. We created an element directive, which requires that the ngModel directive be used
on the same element as the noUiSlider directive that we’re creating.
224
| Chapter 13: Advanced Directives
www.it-ebooks.info
2. In the link function, we first create the noUiSlider by calling its constructor with
the appropriate parameters. We use the attributes from the HTML, but make sure
that we convert them from strings to numbers.
3. Because noUiSlider is a jQuery plugin, and we load jQuery before we load Angu‐
larJS in index.html, we get to directly call the noUiSlider function on our element,
because jQuery seamlessly integrates into AngularJS.
4. Then, to finish integrating ngModel into our third-party input integration, we need
to accomplish two steps:
a. When the data changes within AngularJS, we need to update the third-party UI
component. We do this by overriding the $render method on the ngMo
delCtrl, and setting the value in the third-party component inside of it. The
latest and greatest value that’s currently set in the variable referred to by ngMo
del is available in the ngModelCtrl in the $viewValue variable. AngularJS calls
the $render method whenever the model value changes inside AngularJS (for
example, when it is initialized to a value in our controller).
b. When the data changes outside AngularJS, we need to update AngularJS with
the new value. We do this by calling the $setViewValue function on the ngMo
delCtrl with the latest and greatest value inside the set listener.
5. Also, as mentioned in the AngularJS life cyle, AngularJS updates the UI whenever
it knows that things within its control have changed. A third-party UI component
is outside the AngularJS life cycle, so we need to manually call $scope.$apply()
to ensure that AngularJS updates the UI. The $scope.$apply() call takes an op‐
tional function as an argument and ensures that the AngularJS digest cycle that’s
responsible for updating the UI with the latest values is triggered.
When we integrate any third-party UI component that needs to act as an input widget,
it always makes sense to integrate it and leverage the ngModel directive so that it works
seamlessly like any other input widget. When we do this, we need to take care of the
two-way data-binding:
• When the data inside AngularJS changes, we need to update the third-party com‐
ponent data with the latest and greatest values (handled by overriding the ngMo
delCtrl.$render function).
• When the data outside AngularJS changes (through an event outside AngularJS),
we need to capture it and update AngularJS’s model (by calling ngModelCtrl.$set
ViewValue with the updated value).
Because the only listeners we add are on the element, when the element is destroyed,
those listeners are removed. If we add another listener, then we’re responsible for clean‐
ing them up as well.
Directive Controllers and require | 225
www.it-ebooks.info
Custom Validators
Now that we understand directive controllers as well as how to leverage ngModel to
create our own input directives, we can create our own custom validators. As we saw in
Chapter 2, AngularJS has a lot of built-in directives for form and input validation, like
required, ng-required, ng-minlength, and so on. Between them, they give us a strong
base of validators to start using in our application.
But after a certain point, each user needs certain validation that they find themselves
repeating across multiple use cases. In such a case, it would be better for us to create
our own custom validators using AngularJS.
Let’s take a slightly contrived example where we want to ensure that the text the user
enters in a text box is a valid US zip code, which is one of the three following forms:
• 12345
• 12345 1234
• 12345-1234
Let’s look at index.html for this, which demonstrates the usage of our new validator
directive:
<!-- File: chapter13/directive-custom-validator/index.html -->
<html>
<head>
<title>Stock Market App</title>
<style>
input.ng-invalid {
background: pink;
}
</style>
</head>
<body ng-app="stockMarketApp">
<div ng-controller="MainCtrl as mainCtrl">
<h3>Zip Code Input</h3>
<h5>Zips are allowed in one of the following formats</h5>
<ul>
<li>12345</li>
<li>12345 1234</li>
<li>12345-1234</li>
</ul>
<form novalidate="">
Enter valid zip code:
<input type="text"
ng-model="mainCtrl.zip"
valid-zip>
</form>
226
| Chapter 13: Advanced Directives
www.it-ebooks.info
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js">
</script>
<script src="app.js"></script>
<script src="directive.js"></script>
</body>
</html>
The only interesting part of the HTML is the form, which houses an input with ngmodel on it. We want to ensure that the user types valid US zip codes into it, which is
why we add the valid-zip validator directive on it. The app.js file is very trivial and
simple:
// File: chapter13/directive-custom-validator/app.js
angular.module('stockMarketApp', [])
.controller('MainCtrl', [function() {
this.zip = '1234';
}]);
app.js creates a controller and adds a default, invalid value to the zip field on the con‐
structor. Finally, let’s look at the directive that creates our custom validator:
// File: chapter13/directive-custom-validator/directive.js
angular.module('stockMarketApp')
.directive('validZip', [function() {
var zipCodeRegex = /^\d{5}(?:[-\s]\d{4})?$/g;
return {
restrict: 'A',
require: 'ngModel',
link: function($scope, $element, $attrs, ngModelCtrl) {
// Handle DOM update --> Model update
ngModelCtrl.$parsers.unshift(function(value) {
var valid = zipCodeRegex.test(value);
ngModelCtrl.$setValidity('validZip', valid);
return valid ? value : undefined;
});
// Handle Model Update --> DOM
ngModelCtrl.$formatters.unshift(function(value) {
ngModelCtrl.$setValidity('validZip',
zipCodeRegex.test(value));
return value;
});
}
};
}]);
Our zipCode directive defines a regular expression that we can use to validate whether
or not a given string is a valid US zip code. Our directive itself depends on and leverages
the ngModel controller by requiring it as part of the directive definition. Finally, we get
Directive Controllers and require | 227
www.it-ebooks.info
to the link function, and here again, similar to the previous input directive, we have to
handle both ways of data flow in the application.
When the data changes in the DOM, AngularJS goes through each parser, and the parser
gets to check the validity of the data before passing it along in the chain. We add our
own parser to the chain of parsers, and perform our validity check here using the regular
expression. In either case, we set the validity of our directive on the ngModel controller.
The parser function has to return the correct value (if the data is valid) or undefined
(in case the data isn’t).
We also need to handle the case where the model is updated (due to a server response,
say). In this case, AngularJS runs the data through a formatting step to ensure that it’s
taking the correct API. We again check for validity here and return the value.
In both of these cases, we set the validity of the element using the name of the directive
on the ngModel controller.
Now, we could very well do asynchronous validation in either of these cases by making
a server call through $http. So email or username availability checks could be wrapped
in their own validators. Your imagination is the only limit with these validators!
Compile
In the directive life cycle, we mentioned that a directive goes through two distinct phases:
a compile step and a link step. We explored the link step in detail; now we dig into the
compile step.
By the time we get to the link step of the directive, the directive’s HTML has already
been parsed and all the relevant directives within it have been picked up by the AngularJS
compiler and attached to the correct scope. At this point, if we dynamically add any
directives to the HTML or do large-scale DOM manipulation that involves integration
with existing AngularJS directives, it won’t work correctly.
The compile step in a directive is the correct place to do any sort of HTML template
manipulation and DOM transformation. We never use the link and compile functions
together, because when we use the compile key, we have to return a linking function
from within it instead. Let’s take an example of a form-element directive to see how it
works in practice:
<!-- File: chapter13/directive-compile/index.html -->
<html>
<head>
<title>Dynamic Form App</title>
</head>
<body ng-app="dynamicFormApp">
<div ng-controller="MainCtrl as mainCtrl">
228
| Chapter 13: Advanced Directives
www.it-ebooks.info
<form novalidate="" name="mainForm">
<form-element type="text"
name="uname"
bind-to="mainCtrl.username"
label="Username"
required
ng-minlength="5">
<validation key="required">
Please enter a username
</validation>
<validation key="minlength">
Username must be at least 5 characters
</validation>
</form-element>
Username is {{mainCtrl.username}}
<form-element type="password"
name="pwd"
bind-to="mainCtrl.password"
label="Password"
required
ng-pattern="/^[a-zA-Z0-9]+$/">
<validation key="required">
Please enter a password
</validation>
<validation key="pattern">
Password must only be alphanumeric characters
</validation>
</form-element>
Password is {{mainCtrl.password}}
<button>Submit</button>
</form>
</div>
<script src="http://code.angularjs.org/1.2.16/angular.js"></script>
<script src="app.js"></script>
<script src="directive.js"></script>
</body>
</html>
In this HTML, we want to define a form with a bunch of labels and input fields. Each
field has its own validation logic and different error messages that we need to display
for each particular field. Instead of writing complex HTML using what we saw in
Chapter 4, we instead want to abstract some of that using the form-element directive,
on which we specify the validation and binding rules. The form-element directive also
allows us to define validation messages to show on different error conditions. These
validation messages would then be shown and hidden under the right conditions
Compile | 229
www.it-ebooks.info
automatically without the need for any extra code. One requirement for our new di‐
rective is that it must be used inside a form in the HTML.
Here are the fields we support with our form-element directive:
• The type of input field (text, password, and so on)
• The name of the input field to be able to find errors for it correctly
• The ngModel variable to bind to
• The label to be shown beside the form field
• Any ngModel-based validation, like required, ngMinlength, ngPattern, and so on
The controller for this is pretty trivial, and is as follows:
// File: chapter13/directive-compile/app.js
angular.module('dynamicFormApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.username = '';
self.password = '';
}]);
The controller defines some variables for the HTML to bind to, and nothing else. We
don’t implement an onClick function for the form, because we are not focusing on that
particular flow.
Now our directive needs to look at the HTML, and do the following:
• Generate the right input tag with the correct ng-model, and the validation rules.
• Generate the template for all the error messages and ensure they are shown on the
correct condition.
• Ignore any attributes that are not known or handled by the directive correctly.
• Add functions to the scope to show the error messages correctly.
Let’s see how we can accomplish this using our directive:
// File: chapter13/directive-compile/directive.js
angular.module('dynamicFormApp')
.directive('formElement', [function() {
return {
restrict: 'E',
require: '^form',
scope: true,
compile: function($element, $attrs) {
var expectedInputAttrs = {
'required': 'required',
230
| Chapter 13: Advanced Directives
www.it-ebooks.info
'ng-minlength': 'ngMinlength',
'ng-pattern': 'ngPattern'
// More here to be implemented
};
// Start extracting content from the HTML
var validationKeys = $element.find('validation');
var presentValidationKeys = {};
var inputName = $attrs.name;
angular.forEach(validationKeys, function(validationKey) {
validationKey = angular.element(validationKey);
presentValidationKeys[validationKey.attr('key')] =
validationKey.text();
});
// Start generating final element HTML
var elementHtml = '<div>' +
'<label>' + $attrs.label + '</label>';
elementHtml += '<input type="' + $attrs.type +
'" name="' + inputName +
'" ng-model="' + $attrs.bindTo + '"';
$element.removeAttr('type');
$element.removeAttr('name');
for (var i in expectedInputAttrs) {
if ($attrs[expectedInputAttrs[i]] !== undefined) {
elementHtml += ' ' + i + '="' +
$attrs[expectedInputAttrs[i]] + '"';
}
$element.removeAttr(i);
}
elementHtml += '>';
elementHtml +=
'<span ng-repeat="(key, text) in validators" ' +
' ng-show="hasError(key)"' +
' ng-bind="text"></span>';
elementHtml += '</div>';
$element.html(elementHtml);
return function($scope, $element, $attrs, formCtrl) {
$scope.validators = angular.copy(presentValidationKeys);
$scope.hasError = function(key) {
return !!formCtrl[inputName]['$error'][key];
};
};
}
};
}]);
Compile | 231
www.it-ebooks.info
The following are the points of interest in the formElement directive that we previously
defined:
1. We add require to ensure the formElement directive is used as a subchild of any
form, and give it a new child scope so that any functions we add are restricted and
do not override any global variables or functions.
2. We give it a compile function, which gets called with the element and the attributes.
The compile function executes before the scope is available, so it does not get the
scope injected in.
3. We start extracting and parsing the existing form-element tag from the HTML,
and picking out the validation rules, messages, and existing attributes that we care
about.
4. After that, we start generating the new HTML that will be used for the directive.
Because we will be adding AngularJS directives dynamically, we are doing this in
the compile. If we do this in the link step, AngularJS won’t detect these directives
and our application won’t work.
5. We add the input tag with the name ng-model and all the validations that were
present in the HTML.
6. We then replace the existing content of the directive with this newly generated
content.
7. Finally, we return a postLink function (we cannot have a link keyword along with
compile; we need to return the link function from within compile instead), which
adds the validators array and a hasError function to show each of the validation
messages under the correct conditions. This uses the form controller, which was
required by the directive as per the standards defined in Chapter 4.
AngularJS Convenience Functions
You might have noticed that we called a function named angular.forEach as part of
the formElement directive compile function. AngularJS exposes a bunch of global
functions that we can use when writing our application to perform common tasks that
are not part of the basic JavaScript API. These include the following functions:
angular.forEach
Iterator over objects and arrays, to help you write code in a functional manner.
angular.fromJson and angular.toJson
Convenience methods to convert from a string to a JSON object and back from a
JSON object to a string.
232
| Chapter 13: Advanced Directives
www.it-ebooks.info
angular.copy
Performs a deep copy of a given object and returns the newly created copy.
angular.equals
Determines if two objects, regular expressions, arrays, or values are equal. Does
deep comparison in the case of objects or arrays.
angular.isObject, angular.isArray, and angular.isFunction
Convenience methods to quickly check if a given variable is an object, array, or
function.
angular.isString, angular.isNumber, and angular.isDate
Convenience methods to check if a given variable is a string, number, or date object.
There are many more. Check out all the functions in the official AngularJS docs.
When we execute this application, we will see a form with two input fields and the error
messages for required immediately. As we type in, we will see the required message
switch with another validation message before finally showing the valid form.
As mentioned before, compile is only used in the rarest of cases, where you need to do
major DOM transformations at runtime. In a majority of cases, you might be able to
accomplish the same with transclusion, or pure link function. But it does give you that
extra flexibility when you need it.
Pre- and Post-Linking
The link function we generally write (and even return from the compile function) is
what is known as a post-link function. When a post-link function executes, all chil‐
dren directives have been compiled and linked at this point. DOM transformations (not
adding new AngularJS directives, but creating a chart, for example) are safe at this point
as well.
But in case we needed a hook to execute something before the children are linked, we
can add what is called pre-link function. At this point, the children directives aren’t
linked, and DOM transformations are not safe and can have weird effects.
The way to have a pre- and post-link function is to define it as an object instead of just
having a function. That is, instead of:
{
link: function($scope, $element, $attrs) {}
}
You can have:
{
link: {
Compile | 233
www.it-ebooks.info
pre: function($scope, $element, $attrs) {},
post: function($scope, $element, $attrs) {}
}
}
This is true for the return from the compile function as well, where instead of returning
the post-link function, we can return an object with a pre- and post-key.
Priority and Terminal
The last two options we look at when creating directives are priority and terminal.
priority is used to decide the order in which directives are evaluated when there are
multiple directives used on the same element. For example, when we use the ngModel
directive along with ngPattern or ngMinlength, we need to ensure that ngPattern or
ngMinlength executes only after ngModel has had a chance to execute. Thus, we can
give ngPattern a lower priority than ngModel to ensure it executes afterwards.
By default, any directive we create has a priority of 0. The larger the number, the higher
the priority, and higher priority directives are compiled and linked before lower priority
ones.
The terminal keyword in a directive is used to ensure that no other directives are
compiled or linked on an element after the current priority directives are finished. Also,
children directives will not be touched or compiled when terminal is set to true. By
default, it is set to false. Any directives on an element at the same priority will execute,
because the order of execution of directives at the same priority is not defined.
Third-Party Integration
By now we have covered all the important features of the directive definition object, as
well as created some complex directives. In this section, we will explore common steps
that are involved in integrating or bringing in display-oriented third-party directives.
These could be things like charts and graphs, or any other integration where instead of
an input, we deal with data display.
In the case of a simple HTML display of data (like our stockWidget directive), we can
simply use the AngularJS data-binding directives to accomplish the task. But when it
comes to third-party components, we need to take care of the data-binding ourselves.
In these kinds of directives, as directive creators, we have a few tasks we need to
accomplish:
1. Depending on the library, wait for the library to load before starting our directive.
234
| Chapter 13: Advanced Directives
www.it-ebooks.info
2. Take in the data from a controller, and convert it into a format suitable for our thirdparty component.
3. Display the data using the third-party component.
4. Whenever the data changes inside AngularJS, update the third-party component.
5. Listen for events from the third-party component and pass them to the relevant
controller through function bindings.
We will now integrate Google Charts, which uses an asynchronous loader to load its
API, and create a pieChart directive for easy use. Most directives will not be as complex,
but regardless, the following example should give you a framework within which you
can integrate any and every type of display directive you might need.
As always, we start with the main index.html file, to take a look at the setup and usage
before we dig into the implementation:
<!-- File: chapter13/directive-google-chart/index.html -->
<html>
<head>
<title>Google Chart App</title>
</head>
<body ng-app="googleChartApp">
<div ng-controller="MainCtrl as mainCtrl">
<div>
<button ng-click="mainCtrl.changeData()">
Change Pie Chart Data
</button>
</div>
<div pie-chart
chart-data="mainCtrl.pieChartData"
chart-config="mainCtrl.pieChartConfig">
</div>
</div>
<script
<script
<script
<script
<script
</body>
</html>
src="http://www.google.com/jsapi"></script>
src="http://code.angularjs.org/1.2.16/angular.js"></script>
src="app.js"></script>
src="googleChartLoader.js"></script>
src="pieChart.js"></script>
The HTML that’s using pieChart is quite straightforward. We use the pie chart as an
attribute directive, and pass it some configuration as well as the data it needs in order
to display as arguments to the directive. This allows us to make it reusable instead of
tying it down to a specific service or the same configuration object every time. We also
have a button that’s used to change the data that’s driving the pie chart.
Third-Party Integration
www.it-ebooks.info
| 235
In terms of script dependencies, we also load the Google Charts asynchronous API
loader, which provides the API to load the Google Charts API. We will see how to
leverage AngularJS promises to wait for this API to load before starting to draw our
charts.
Next, let’s look at our main controller for the application, which defines the data and
the configuration for the pie chart:
// File: chapter13/directive-google-chart/app.js
angular.module('googleChartApp', [])
.controller('MainCtrl', [function() {
var self = this;
self.pieChartData = [
{label: 'First', value: 25},
{label: 'Second', value: 54},
{label: 'Third', value: 75}
];
self.pieChartConfig = {
title: 'One Two Three Chart',
firstColumnHeader: 'Counter',
secondColumnHeader: 'Actual Value'
};
self.changeData = function() {
self.pieChartData[1].value = 25;
};
}]);
The app.js file declares the pieChartData variable, which is an array of keys and values
in its own format. This is hardcoded here, but could be retrieved from a server call using
$http as well. We also have some hardcoded configuration that dictates the name of the
chart, and the columns for the chart. Finally, we have a very simple function (change
Date()) that changes the value of one element of the data to see if the pie chart updates
itself automatically as a result.
The next part we dig into is the asynchronous loader, which ensures that the pie
Chart directive doesn’t try drawing the chart before our API is loaded:
// File: chapter13/directive-google-chart/googleChartLoader.js
angular.module('googleChartApp')
.factory('googleChartLoaderPromise',
['$q', '$rootScope', '$window',
function($q, $rootScope, $window) {
// Create a Deferred Object
var deferred = $q.defer();
// Load Google Charts API asynchronously
$window.google.load('visualization', '1',
{
236
| Chapter 13: Advanced Directives
www.it-ebooks.info
packages: ['corechart'],
callback: function() {
// When loaded, trigger the resolve,
// but inside an $apply as the event happens
// outside of AngularJS life cycle
$rootScope.$apply(function() {
deferred.resolve();
});
}
});
// Return the promise object for the directive
// to chain onto.
return deferred.promise;
}]);
The googleChartLoaderPromise factory loads the visualization library once at load,
and returns a promise that can be chained on to know when the load is complete. It
does so using the $q service (refer to “The $q Service” on page 94 for a quick refresher
on $q) in AngularJS, which we also saw previously in Chapter 10 for handling re
solve in a route. In addition to being able to reject the current promise using $q.re
ject(data), the $q service also allows us to create and work with our own promises,
which is what we use here.
We create a deferred object, which represents an asynchronous task that will be fulfilled
in the future. We create it by calling $q.defer(). We then return deferred.promise,
on which users of the API can add .then() to be notified when this asynchronous task
will be fulfilled (or rejected). We then call the Google API with the appropriate argu‐
ments to load the visualization library and give it a callback to be notified when this
asynchronous task is completed.
Inside the callback, we resolve the deferred object we created, which is the trigger for
all the .then to execute. But because this callback is called outside the life cycle of
AngularJS, we need to wrap it in a $rootScope.$apply function to ensure AngularJS
knows to redraw the UI and run a complete digest cycle as needed.
Finally, we can look at the pieChart directive to see how it integrates with the google
ChartsLoaderPromise service as well as Google Charts:
// File: chapter13/directive-google-chart/pieChart.js
angular.module('googleChartApp')
.directive('pieChart', ['googleChartLoaderPromise',
function(googleChartLoaderPromise) {
var convertToPieChartDataTableFormat =
function(firstColumnName, secondColumnName, data) {
var pieChartArray = [[firstColumnName, secondColumnName]];
for (var i = 0; i < data.length; i++) {
pieChartArray.push([data[i].label, data[i].value]);
}
Third-Party Integration
www.it-ebooks.info
| 237
return google.visualization.arrayToDataTable(
pieChartArray);
};
return {
restrict: 'A',
scope: {
chartData: '=',
chartConfig: '='
},
link: function($scope, $element) {
googleChartLoaderPromise.then(function() {
var chart = new google.visualization.PieChart(
$element[0]);
$scope.$watch('chartData', function(newVal, oldVal) {
var config = $scope.chartConfig;
if (newVal) {
chart.draw(
convertToPieChartDataTableFormat(
config.firstColumnHeader,
config.secondColumnHeader,
newVal),
{title: $scope.chartConfig.title});
}
}, true);
});
}
};
}]);
Our actual pieChart directive is quite simple if you ignore the Google Chart–specific
API calls and look at it at a conceptual level. Here’s what is happening:
1. Our pieChart directive depends on the service we defined previously, and injects
it in.
2. We define a function convertToPieChartDataTableFormat, which takes the data
that we have in our controller and converts it into a format that we can pass to the
Google Charts API.
3. We define a pretty standard directive with an isolated scope that defines the at‐
tributes that need to be passed to it.
4. In our link function, we use the promise returned from the service, and do our
work in the success handler inside the then of the promise. This ensures that we
don’t try calling a Google Charts API unless and until the Google Charts API has
successfully finished loading as per our service.
238
| Chapter 13: Advanced Directives
www.it-ebooks.info
5. Inside the success handler of the promise, we create an instance of a Google Pie
Chart, using the element that we are currently on as the target. This ensures that
we don’t go looking for a random element in our body, or use ID-based selectors,
each of which would make our directive hard to reuse.
6. We then add a watch on the chartData field on the scope, and give it a function to
call as the second argument, and the Boolean true as the third argument. This tells
AngularJS to do what we call a deep watch on $scope.chartData, and whenever it
(or any element inside of it) changes, call the function.
7. The change function is called with both the old and the new value. When we get a
valid new value, we draw the chart after converting the data from the format passed
to the directive to a format that Google Charts understands.
8. Whenever the data in AngularJS changes (either because of a user change or newer
data from the server), this function is automatically called, so we don’t have to
manually do any other work to ensure that our chart is updated.
Now, when you open up the index.html file in your browser, you will see a chart rendered
with the initial data. When we click the button in the page, the data gets updated in the
controller. Because the data is passed to the directive by reference, the directive gets the
latest data as well and the watch function is triggered. This updates the chart with the
latest values and we get an automatically updating UI with no additional work.
If we wanted to only add a data point, or be restrictive on how and when we want to
update the chart, we can add that logic inside the watch as needed. The following same
concepts apply to almost any display-oriented directives you might want to build, and
thus these building blocks go a long way for any application you’re working on:
• Waiting for the API to load.
• Passing in the data to a reusable directive.
• Converting the data to the necessary format and doing the initial draw.
• Watching the data, and updating the UI as necessary.
Best Practices
Now that we know how to create pretty much any type of directive of any complexity,
we’ll cover some things to keep in mind to ensure that the directives we create behave
cleanly and are fast under all conditions.
Best Practices
www.it-ebooks.info
| 239
Scopes
If in your directive, you find yourself adding variables and functions to the scope in the
link or the controller, then it is recommended that either you set the scope key in
the directive definition object to true, or you create an isolated scope for the directive.
For example, say our controller has a Boolean variable called selected to decide whether
or not a certain checkbox is selected. If our tabs and tab directive do not create a new
scope or use an isolated scope, then the directive is going to override the selected
variable on the controller and cause all kinds of unintended effects.
If our directive needs access to functions and variables from the parent scope, we have
one of two options:
• Create a child scope, and add any variables and functions to the child scope. This
can be accomplished by setting scope: true in the directive definition object. But
if the parent scope has any functions or variables, those would still be accessible in
the directive.
• Create an isolated scope, and pass in any variables and functions using data- and
function-binding. This is the ideal pattern, because this removes any possibility that
the directive might need a certain variable or function in the parent scope for the
directive to work successfully. Directives with isolated scopes are the most reusable
directives.
Clean Up and Destroy
AngularJS adds listeners and watchers to keep its UI updated when we use bindings and
other directives. To ensure that none of them leak or stay around past their need, An‐
gularJS removes them when their scopes and elements are destroyed.
When we create directives in AngularJS with their own scope (child or isolated), any
watchers we add on the scope and any listeners we add on the element passed to the
directive are automatically cleaned up when that directive is destroyed in the UI.
That said, AngularJS cannot clean up event listeners we add on elements outside of the
scope and HTML of the directive. When we add these listeners or watchers, it becomes
our responsibility to clean up when the directive gets destroyed. We can listen for the
destruction of a directive in two possible ways:
Listen for the $destroy event on the scope
As we saw previously, we can add event listeners on the scope itself. Each scope
broadcasts an event called $destroy, which is a notification that the scope is about
to be destroyed and cleaned up. Any controller or directive can listen for it and do
additional clean-up when this event is triggered. Any listeners that we manually
240
| Chapter 13: Advanced Directives
www.it-ebooks.info
add or intervals or timeouts that are currently executing need to be cleaned up in
the $destroy event handler. A sample handler would be as follows:
$scope.$on('$destroy', function() {
// Do clean up here
});
Listen for the $destroy event on the element
If the scope is inherited (not a new scope or an isolated scope), but we still need to
do clean-up when the directive is destroyed, the other alternative is to listen for the
$destroy event on the element itself. This is a jQuery event that is fired by AngularJS
when the element is about to be removed from the DOM. A sample handler for this
would be as follows:
$element.$on('$destroy', function() {
// Do clean-up here
});
Watchers
AngularJS allows us to add our own event listeners (or watchers, in AngularJS termi‐
nology) on scope variables and functions. These basically get triggered by AngularJS
whenever the variable under watch changes, and we get access to both the new and the
old value in such a case.
There are a few kinds of watches we can add, and it helps to be aware of the implications
of each:
$watch
The most standard watch, which takes:
• A string, which is the name of a variable on the scope
• A function, whose return value is evaluated
In either case, whenever the value changes (a straightforward shallow check), then
the function passed to it as the second argument is triggered with the old and new
value.
Deep $watch
The same as the standard watch, but takes a Boolean true as the third argument.
This forces AngularJS to recursively check each object and key inside the object or
variable and use angular.equals to check for equality for all objects. Obviously, it
catches all changes, but also consumes more CPU cycles. So be careful that you
don’t abuse deep watches across your application. Instead, it’s preferable to have a
Boolean that signals if something internally has changed and watch that.
Best Practices
www.it-ebooks.info
| 241
$watchCollection
Slightly optimized version of the watch aimed at arrays. Similar arguments as the
$watch, but expects that the value is an array. The function is triggered any time an
item is added, removed, or moved in the array. It does not watch for changes to
individual properties in an item in the array.
$apply (and $digest)
The most common mistake or error that happens when integrating with a third-party
component is that we hook everything up correctly and then stare at our screen won‐
dering why the UI is not getting updated. And the root cause more often than not is a
missing $apply() call, or triggering the digest cycle manually by calling $digest().
Whenever you’re working with third-party components, remember that there are two
distinct life cycles at play. The first is the AngularJS life cycle that is responsible for the
keeping the UI updated and the second is a third-party component’s life cycle. When
the two meet, developers are responsible for letting AngularJS know that something
outside its life cycle has changed and that it needs to update its UI. And this is done by
triggering $scope.$apply(), which starts a digest cycle on the $rootScope.
Sometimes, another event in AngularJS will automatically trigger and take care of this,
but in any case if you are updating any scope variables in response to an external event,
make sure you manually trigger the $scope.$apply() or $scope.$digest().
Conclusion
In this chapter, we dove deep into the most complex parts of AngularJS and covered
some of the rarer, but very powerful features of directives. We created a stock widget
that could take in custom templates and thus could be customized for each usage as per
the need using simple template transclusion. We also created a very dumb and simple
equivalent of a one-time repeater using advanced transclusion and transclusion func‐
tions. We also saw how to communicate across the tabs and tab directives using di‐
rective controllers, and how to leverage existing controllers of directives like ngModel
to create input directives like sliders and custom validators. The last directive definition
object configuration we covered was how to create a declarative form-element directive
and generate custom dynamic templates using the compile step of directives.
We then saw how simple it was to create a pie chart directive using Google Charts while
exploring the concepts and fundamentals of creating any display component and the
simple steps for integrating third-party UI widgets. Finally, we saw some best practices
to keep in mind when creating directives to ensure that we have don’t suffer any per‐
formance issues or strange bugs.
242
| Chapter 13: Advanced Directives
www.it-ebooks.info
With this, we have covered all of the major parts of the core AngularJS codebase. In the
next chapter, we see how to write end-to-end scenario tests for AngularJS using the
Protractor test runner.
Conclusion | 243
www.it-ebooks.info
www.it-ebooks.info
CHAPTER 14
End-to-End Testing
By now, we have covered all the moving parts that comprise an AngularJS application,
including controllers, views, services, filters, and directives. We also talked about the
importance of unit testing, and saw how we can individually test each part and com‐
ponent of AngularJS. A great set of unit tests can save an amazing amount of time for
developers, from debugging to preventing bugs to catching regressions.
But unit tests are only great up to a certain point. They can test whether your application
works correctly, assuming that the server behaves in a certain way. We saw this with the
unit tests for services and XHRs in Chapter 7, in which we mocked out the server using
the $httpBackend service in the unit test. This allowed us to write rapid unit tests that
were reliable, stable, and super fast. These tests will catch the logic of your controllers
and service, but what if the server changes its return value? Or what if the server URLs
themselves have changed? What about formatting and display of the HTML, especially
if we have made a typo in an ng-bind expression?
To catch these, we need to write end-to-end tests, which open the browser, navigate to
a live running version of our web application, and click around using the application as
a real-world user would. To accomplish this, we use Protractor.
In this chapter, we see how to set up a very simple end-to-end test for a demo application
using Protractor. We create a Protractor config, write an end-to-end test, and see it in
action. We also go over the initial setup and requirements needed to run these tests as
well as best practices when working with them. By the end of the chapter, you should
be familiar with Protractor and tests using Protractor.
The Need for Protractor
So why Protractor? Why yet another testing tool? With AngularJS, the very first attempt
at making it easy to write end-to-end scenario tests was something known as AngularJS
245
www.it-ebooks.info
Scenario Runner. This used to be a full end-to-end runner that was AngularJS-aware
so that tests would be more stable and deterministic.
We realized that simulating user actions like clicks and typing through JavaScript was
not ideal, and did not replicate the real user flow. So with Protractor, the aim was to
build on top of something like Selenium WebDriver, which actually works at the OS
level to work with the browser and perform actual clicks and keystrokes.
But at the same time, we still want to avoid one of the major issues with end-to-end tests
of AJAX applications, primarily, waiting for the page to load. With a normal web page,
we can figure out when a page is loaded, and then when the user clicks a link, the entire
page reloads. Thus, we can know exactly when a page is loaded to continue testing.
With a Single-Page Application, there is only one page load. And data can be fetched
asynchronously (and usually is) even after the page load is completed. So how do we
know in our end-to-end test when to check if a particular data item is shown or not?
We have a couple options:
• We wait for an arbitrary amount of time after a page loads or a link is clicked—
about five seconds.
• We wait for a certain element on the page to be shown before performing our checks.
Both of these, though, are very nondeterministic. All it takes is for a certain server call
to take 5.1 seconds instead of 5 and our test breaks. Over time, we start waiting for
another test run, even if a test fails to catch these nondeterministic failures. And finally,
we just stop relying on end-to-end tests.
With Protractor, the need to wait an arbitrary time for arbitrary events disappears.
Protractor is built on top of WebDriver, but is AngularJS-aware. Thus, when a button
is clicked and a server call is made, Protractor knows to wait for the server call to return
before proceeding with the rest of the test. Thus, as developers, we can focus on writing
the test and expect it to execute similar to how a user would see it, without having
conditions and timeouts for certain elements to load or disappear.
Initial Setup
Protractor is a NodeJS package, and so can be installed using npm (make sure you have
node installed first) by running:
sudo npm install -g protractor
This installs Protractor along with all its dependencies as a global package for use across
projects.
246
| Chapter 14: End-to-End Testing
www.it-ebooks.info
We also need WebDriver to actually start and control the browsers on which we run
unit tests. When we install Protractor, it gives us the necessary scripts to download and
install WebDriver locally as well. We can install WebDriver by running:
sudo webdriver-manager update
At this point, we have all the necessary tools installed to run our Protractor tests. Run‐
ning the tests is as simple as executing:
protractor path/to/protractor.conf.js
What does this protractor.conf.js file look like? We dive into this next.
Protractor Configuration
The Protractor configuration file is a JavaScript file that basically holds all the config‐
uration elements that Protractor needs to be able to run the end-to-end tests. These
include configuration options like:
• Where is the server running?
• Where is the Selenium WebDriver on which to run the tests?
• What tests should be executed?
• What browsers should the tests be run on?
And much more. Let’s take a look at a sample configuration with the most commonly
used options, which is what we will be using for the tests in this chapter:
// File: chapter14/protractor.conf.js
exports.config = {
// The address of a running Selenium server
seleniumAddress: 'http://localhost:4444/wd/hub',
// The URL where the server we are testing is running
baseUrl: 'http://localhost:8000/',
// Capabilities to be passed to the WebDriver instance
capabilities: {
'browserName': 'chrome'
},
// Spec patterns are relative to the location of the
// spec file. They may include glob patterns.
specs: ['*Spec*.js'],
// Options to be passed to Jasmine-node
jasmineNodeOpts: {
showColors: true // Use colors in the command-line report
}
};
Protractor Configuration | 247
www.it-ebooks.info
This configuration file is the simplest Protractor configuration file we could use. It has
the following things of note:
• Specifies that the Selenium server is running locally on port 4444.
• Specifies that the server is running at http://localhost:8000/.
• Specifies that the browser to automatically run is Chrome.
• Points out the spec.js file that holds the end-to-end test code.
• Some configuration options for Jasmine to show colors in the command line.
More detail about the configuration options that Protractor supports is available in the
Reference Configuration file on GitHub.
Now what do we need to do before we can run this test? From the chapter14 folder on
GitHub, do the following:
1. Start Selenium locally (this can be done with webdriver-manager start).
2. Start the server locally (node server.js in our example).
3. Start Protractor (protractor test/e2e/protractor.conf.js).
Before we do this, let’s see how an end-to-end test looks.
An End-to-End Test
Protractor tests use the same Jasmine scaffolding syntax we’ve been using for our unit
tests, so we have the same describe blocks for a set of tests, and individual it blocks
for each test. In addition to these, Protractor exposes some global variables that are
needed for writing end-to-end tests, namely:
browser
This is a wrapper around WebDriver that allows us to interact with the browser
directly. We use this object to navigate to different pages and page-level information.
element
The element object is a helper function to find and interact with HTML elements.
It takes a strategy to find the elements as the argument, and then gives you back an
element that you can interact with by clicking and sending keystrokes to it.
by
The by is an object with a collection of element-finding strategies. We can find
elements by id or CSS classes, which are built-in strategies of WebDriver. Protractor
adds a few strategies on top of that to find elements by model, binding, and repeater
as well, which are AngularJS-specific ways to find certain elements on the page.
248
| Chapter 14: End-to-End Testing
www.it-ebooks.info
Without further ado, let’s take a look at how a test for the routing application we wrote
in Chapter 10 would work (the code for the app is available at GitHub in the chap‐
ter14 folder). Start the app by first installing the dependent packages using npm in
stall followed by node server.js:
// File: chapter14/simpleRoutingSpec.js
describe('Routing Test', function() {
it('should show teams on the first page', function() {
// Open the list of teams page
browser.get('/');
// Check whether there are 5 rows in the repeater
var rows = element.all(
by.repeater('team in teamListCtrl.teams'));
expect(rows.count()).toEqual(5);
// Check the first row details
var firstRowRank = element(
by.repeater('team in teamListCtrl.teams')
.row(0).column('team.rank'));
var firstRowName = element(
by.repeater('team in teamListCtrl.teams')
.row(0).column('team.name'));
expect(firstRowRank.getText()).toEqual('1');
expect(firstRowName.getText()).toEqual('Spain');
// Check the last row details
var lastRowRank = element(
by.repeater('team in teamListCtrl.teams')
.row(4).column('team.rank'));
var lastRowName = element(
by.repeater('team in teamListCtrl.teams')
.row(4).column('team.name'));
expect(lastRowRank.getText()).toEqual('5');
expect(lastRowName.getText()).toEqual('Uruguay');
// Check that login link is shown and
// logout link is hidden
expect(element(by.css('.login-link')).isDisplayed())
.toBe(true);
expect(element(by.css('.logout-link')).isDisplayed())
.toBe(false);
});
it('should allow logging in', function() {
// Navigate to the login page
browser.get('#/login');
var username = element(
by.model('loginCtrl.user.username'));
An End-to-End Test
www.it-ebooks.info
| 249
var password = element(
by.model('loginCtrl.user.password'));
// Type in the username and password
username.sendKeys('admin');
password.sendKeys('admin');
// Click on the login button
element(by.css('.btn.btn-success')).click();
// Ensure that the user was redirected
expect(browser.getCurrentUrl())
.toEqual('http://localhost:8000/#/');
// Check that login link is hidden and
// logout link is shown
expect(element(by.css('.login-link')).isDisplayed())
.toBe(false);
expect(element(by.css('.logout-link')).isDisplayed())
.toBe(true);
});
});
We have two tests in this example. The first test:
• Opens up the main page of the teams application.
• Fetches all the rows by using the repeater, and then checks whether there are five
rows present on the main page.
• Fetches the name and rank of the first row and asserts that they are as expected.
• Fetches the name and rank of the last row and asserts that they are as expected.
• Checks that the login link is shown and the logout link is hidden.
Thus, the first test purely deals with rendering and logic to ensure that the application
is correctly hooked up to the server and is capable of fetching and displaying the content
correctly.
The second test deals with user interaction by:
• Opening up the login page.
• Entering the username and password to the correct model.
• Clicking the login button by CSS selector.
• Ensuring that the login is successful by checking the URL of the redirected page.
• Checking that the login link is hidden and the logout link is shown.
250
| Chapter 14: End-to-End Testing
www.it-ebooks.info
Notice that we didn’t add any wait conditions in either of the tests. We wrote the tests
as if a user would be interacting with the application, and let AngularJS and Protractor
worry about when to proceed with the tests.
To execute these tests, execute the following in order:
1. If the server is not running, run node server.js from the appUnderTest folder.
You might need to run npm install from that folder first.
2. If Selenium is not up and running, run webdriver-manager start.
3. Run protractor protractor.conf.js from the folder containing the config file
and the specs.
We will see Protractor open the Chrome browser through Selenium, navigate to the
main page of our locally running application, and click through and run our tests as we
have defined them. At the end, it should print out whether they were successful, or the
reason for failure in case they failed.
Considerations
There are a few things we have to keep in mind, as well as some best practices that should
be followed, when working with and writing end-to-end tests in AngularJS. Let’s go over
them one by one:
Location of ng-app
When you write a simple Protractor test for AngularJS, and point it at any URL that
hosts an AngularJS application, Protractor’s default behavior is to look at the body
element of the HTML to find ng-app. It then kicks in and does its magic. But in
case ng-app is not on the body tag, but on a subelement, we need to manually tell
Protractor how to find it. This is done through the rootElement option of the
Protractor configuration, which takes a CSS selector to the element using ng-app.
For example, if ng-app was on the following element inside our body tag:
<div class="angular-app" ng-app="myApp"></div>
then we’d have to specify the following line in our Protractor configuration file:
rootElement: ".angular-app"
This would tell Protractor to find the element with the CSS class angular-app. This
is not needed if you have the ng-app on the body element.
Polling
If you have any kind of polling logic in your code, where you have to keep fetching
some information or doing some calculations every few seconds, make sure you’re
not using the $timeout service AngularJS provides for that. Protractor has issues
Considerations
www.it-ebooks.info
| 251
figuring out when AngularJS is done with its work. If you need polling, and need
to write end-to-end tests for it, make sure you use the $interval service instead.
Protractor understands and deals with the $interval service, and behaves like you
would expect it to.
Manual bootstrapping
Protractor currently does not support working with AngularJS applications that are
manually bootstrapped. Thus, if you need to write end-to-end tests for such an
application, you might have to work with the underlying WebDriver (by using
browser.driver.get instead of browser.get, and so on), and add wait conditions
to ensure that all the things are loaded before proceeding with the test. You would
not be able to leverage any of the benefits that Protractor offers.
Future execution
WebDriver commands that we write in our tests don’t return the actual values, but
rather promises that will execute later in the browser (in various browsers even).
Thus, console.log won’t actually print the values because it doesn’t have them at
the point the code is executed.
Debugging
Protractor has great built-in support for debugging, because it leverages the Web‐
Driver debugging. To debug any test, we can just add the following line at the point
where we want to start debugging:
browser.debugger();
This could be after any of the lines in the test. Then we run our tests using the
following command:
protractor debug path/to/conf.js
This opens up the Node debugger, which allows us to step through the various
breakpoints in our test. We now need to type “c” and click Enter, to tell Protractor
to continue running the tests. Protractor will run the tests like normal in the browser
up until the point it hits the debugger statement. At that point, it stops and waits
for further instructions to resume the test. This is a real, live application in the
browser that you can interact with and debug to see exactly what the Protractor
runner is seeing. You can actually click around and change the state of the test to
make it fail as well. When you are done debugging, you can type “c” and click Enter
to continue running the test until the next debug point or the end of the test,
whichever comes first.
The last thing to consider is how to organize your tests in such a way that makes them
easy to maintain and reuse. In the test that we wrote in the previous section, we used
element and by to find elements in the page and interact with it by clicking, entering
keys, and asserting the state of the UI. But when we write our tests, we want to create
252
| Chapter 14: End-to-End Testing
www.it-ebooks.info
an API that allows us to easily understand the intent of the test. This is useful because
it becomes easier to understand the test, as well as allow anyone to quickly build a set
of larger, more encompassing tests using the same API. To accomplish this, we use the
concept of PageObjects.
Let’s rewrite the Teams List page test to use PageObjects instead of directly working with
the WebDriver APIs at a test level:
// File: chapter14/routingSpecWithPageObjects.js
// The PageObjects are ideally in separate files
// to allow for reuse across all the tests,
// but are listed here together for ease of understanding
function TeamsListPage() {
this.open = function() {
browser.get('/');
};
this.getTeamsListRows = function() {
return element.all(by.repeater('team in teamListCtrl.teams'));
};
this.getRankForRow = function(row) {
return element(
by.repeater('team in teamListCtrl.teams')
.row(row).column('team.rank'));
};
this.getNameForRow = function(row) {
return element(
by.repeater('team in teamListCtrl.teams')
.row(row).column('team.name'));
};
this.isLoginLinkVisible = function() {
return element(by.css('.login-link')).isDisplayed();
};
this.isLogoutLinkVisible = function() {
return element(by.css('.logout-link')).isDisplayed();
};
}
describe('Routing Test With PageObjects', function() {
it('should show teams on the first page', function() {
var teamsListPage = new TeamsListPage();
teamsListPage.open();
expect(teamsListPage.getTeamsListRows().count()).toEqual(5);
Considerations
www.it-ebooks.info
| 253
expect(teamsListPage.getRankForRow(0).getText())
.toEqual('1');
expect(teamsListPage.getNameForRow(0).getText())
.toEqual('Spain');
expect(teamsListPage.getRankForRow(4).getText())
.toEqual('5');
expect(teamsListPage.getNameForRow(4).getText())
.toEqual('Uruguay');
// Check that login link is shown and
// logout link is hidden
expect(teamsListPage.isLoginLinkVisible()).toBe(true);
expect(teamsListPage.isLogoutLinkVisible()).toBe(false);
});
});
We created a JavaScript class called TeamsListPage, which exposes some APIs to open
the page, get all the rows, and get the individual name and rank for a given row. Then
in our test, we can work with an instance of the TeamsListPage object, which makes
the test much easier to read than before. We can do something similar for the Login
Page test as well.
PageObjects encapsulate abstractions on how to access certain elements and how to
interact with them in a single place, thus allowing for simple reuse as well as handling
change in a single place rather than making the change in multiple places.
Conclusion
We installed Protractor and set up our configuration for a very simple end-to-end test.
We then wrote our first two end-to-end tests, one of which checked the rendering and
display logic of the main page, and the other which tried the login flow in our application.
We also covered some considerations that should be kept in mind when writing these
tests.
With a suite of such tests (and Protractor allows you to define multiple suites for various
needs), you can quickly and confidently ensure that the application you’re developing
is working correctly without having to manually and repeatedly test each individual
flow. For a large-scale application, a smoke set of end-to-end tests is a must.
Now that we’ve covered and touched upon almost every single aspect of AngularJS, the
next chapter brings them all together and covers best practices and guidelines that
should be followed for any AngularJS project. We also cover some tools and applications
that can make your AngularJS development easier.
254
| Chapter 14: End-to-End Testing
www.it-ebooks.info
CHAPTER 15
Guidelines and Best Practices
We covered a whole lot of stuff about AngularJS, and dove deep into almost every part
of the AngularJS framework. We haven’t started scratching the surface of how deep and
complex AngularJS can be, but we do have a strong base on the moving parts to start
considering the bigger picture. In this chapter, our aim is to take a step back from
AngularJS and consider it in the larger picture of your end-to-end web-based applica‐
tion. To that extent, we look at:
• Testing
• File and directory structure
• Best practices
• Building your application
• Tools and libraries
We’ll talk about how to efficiently accomplish each one, and consider a way to perform
them such as to ensure long-term maintainability and project health without reducing
the velocity of development.
Testing
The first and foremost rule of web application development is that testing happens before
the application development starts, while you are developing the application, and af‐
ter you’ve finished development. We’ve tried to imbibe that mentality into this book by
bringing up unit testing and end-to-end testing whenever applicable.
Test-Driven Development
Writing your unit tests and specifications up front is by far the best approach to building
any large-scale, maintainable application using AngularJS. We covered some of the
255
www.it-ebooks.info
major reasons why you should unit test in AngularJS in Chapter 3. In this section, we
cover some of the best practices and thoughts to keep in mind when approaching testing
for your own projects.
Variety of Tests
There are schools of thought that say if you perform test-driven development (TDD),
you will never have a bug. Of course, in practice, TDD is great for hammering out the
nitty-gritty implementation details, but when it comes to a large project with complex
integrations, your unit tests are never going to catch all possible bugs and breakages.
This is why it’s essential to have a proper set of unit tests, integration tests, and scenario
tests to have any level of confidence in your application:
Unit tests
A unit test is concise and focused on testing only one piece (a controller, service,
filter, etc.) and in that, one function. We test (using Karma, which we first saw in
Chapter 3) whether given the right inputs and the right state, does it produce the
right output, or generate the right side effects? If at all possible, we mock out de‐
pendencies to ensure that we are testing a piece in isolation. For example, a con‐
troller unit test could mock out the service completely and assume it works
correctly.
With a unit test, that is the foremost thought. We ask if all the other pieces are
working correctly, does the part being tested work correctly? We are systematically
guaranteeing each individual piece’s correctness so that when we’re tracking down
a hard-to-find bug, we can immediately discount the parts that we know are fully
tested.
Integration tests
Unit tests are great, fast, and tiny—but they have a limitation. They make certain
assumptions about the other integration points for our controllers and services.
They assume they behave in a certain way or return a certain output. More often
than not, the assumptions might not be true. Integration tests (again, written using
Karma and Jasmine in AngularJS) are the ones that test whether the different parts
of your application are correctly configured and hooked up. These might check
whether the controller communicates with the service, and whether the service has
the right side effects and returns the correct values. We did something like this in
“Integration-Level Unit Tests” on page 118 where we tested the integration of our
controller with a service and ensured that the service made the right call and re‐
turned the right values.
In an integration test, you can test the controller-service interaction, and whether
interceptors are correctly configured. You can also test complex interactions by
checking what happens when two functions are called one after the other, or if a
polling feature is correctly fetching the data every few seconds.
256
| Chapter 15: Guidelines and Best Practices
www.it-ebooks.info
Integration tests are still mocking out the XHR calls though, so any tests we write
will not communicate with our server. Even trying to load a template for a directive
(like we saw in Chapter 12) will also be mocked out. Thus, even with an integration
test, while we can be assured that our frontend application logic might be correctly
hooked up and working, we’re still depending on the fact that our server responds
in a certain way, which is not tested.
Scenario tests
The final kinds of tests we should consider having for our application are end-toend, scenario tests. These, as described in Chapter 14, entail opening a browser,
loading our frontend application, and clicking through the various pages as a user
would. These are the truest level of user tests, and are testing the end-to-end flow
of our application. Still, we recommend writing a few of these, and not replacing
all your tests with scenario tests because:
• You need an exponential number of tests to cover all the cases that you can
otherwise split and catch with unit and integration tests.
• A scenario test failing still doesn’t give you the necessary information about
what’s broken, just that something is broken.
A good set of scenario tests will be small, stable, deterministic, and try to catch most
of the integration points. The aim of scenario tests is to ensure that, at the very least,
your most basic features and flows in your application are correctly hooked up and
working.
A common, accepted breakdown, in terms of the quantities of between the preceding
three types of tests in your application is 70:20:10, where 70% of your tests are unit tests,
20% are integration level tests, and 10% are end-to-end, scenario tests. That is the golden
ratio to shoot for.
When to Run Tests
You’ve written these amazing set of tests that are rock solid, and capture and prevent
every possible bug that you could have in your application. Now what? When do you
run these? Here are a few possible places where you should ideally be running them:
On every save
At the very least, your unit tests and integration tests should be run on every save.
Your scenario tests might be a bit too much, but both unit and integration tests are
ridiculously fast (if they are not, you are doing something wrong!). Karma has this
feature called autoWatch, which if enabled, automatically runs the tests in case a
source file changes. We will look at WebStorm as an IDE in a bit, which also has
Karma integrated into it and can automatically run tests every time you save. The
advantage of this is that it gives you instant feedback; when something goes wrong
you can just undo it to get back to a clean state.
Testing | 257
www.it-ebooks.info
Before pushing
If you’re not using a version control system, then stop and do so immediately. Any
significantly sized project (basically, anything that is not a throwaway) should be
hosted in a version control repository (like Git or Mercurial). Once you do that,
you want to ensure that any code getting pushed or commited to your repository
is valid, and that nothing is broken. Most of these systems give you hooks (Git
Hooks, for example) to allow you to execute certain scripts and commands when
a certain acitivity happens. In an ideal setup, you want to execute your unit, inte‐
gration, and scenario tests as a pre-commit or pre-push hook to ensure that any
code checked in to your codebase is valid and working.
Continuous integration
Continuous integration is the concept of a build machine picking up the changes
from your version control system and running a build and tests on the latest code,
whenever new code is checked in. This ensures that any breakages and failures are
caught immediately when something bad is checked in, versus only at the time of
releasing a version with a bunch of changes together. It allows us to trace a problem
to an exact change in the system. Both Karma and Protractor integrate easily with
most open source continuous integration systems like Jenkins and Travis, so if you
have a build system, make sure that it’s running all your AngularJS tests.
All these are action items that you need to do once and get it set up. Then it runs happily
in the background, and enforces code quality for your project. So do it up front, and
reap the benefits later.
Project Structure
One of the most important and often asked questions in an AngularJS project is how
to structure your project in terms of files and folders. This is both in terms of your own
code as well as importing third-party libraries, managing templates and partials, and
getting the build structure in place. In this section, we deal with some general best
practices before diving into directory structure and third-party library strategies and
finally finishing with some good starting points where you can get your project up and
running quickly.
Best Practices
We will keep this section short and concise and expand on it in the following sections.
Use this as a quick guide and refresher when you have any doubts or decision making
to do:
• Have one controller, service, directive, or filter per file. Don’t club them into large
single files.
258
| Chapter 15: Guidelines and Best Practices
www.it-ebooks.info
• Don’t have one giant module. Break up your applications into smaller modules. Let
your main application be composed of multiple smaller, reusable modules.
• Prefer to create modules and directories by functionality (authorization, admin‐
services, search, etc.), over type (controllers, services, directives). This makes your
code more reusable.
• Use the recommended syntax of using the module functions (angular.module('so
meModule').controller…, etc.) over any other syntax you might see online or any‐
where else.
• Use namespaces. Namespace your modules, controllers, services, directives, and
filters. Have mycompany-chart for directives, myProjectAuthService, and so on.
That way, anyone coming in can quickly distinguish your own code from thirdparty and core AngularJS code.
• Most of all, be consistent. This is not AngularJS-specific, but don’t change the way
you name files, folders, directives, or controllers from one place in your application
to another.
Directory Structure
When we talk about directory structure in this section, we are only talking about the
frontend application and not your server application. The AngularJS application would
just be a folder that would typically be statically served from your server. At the highest
level of your frontend application, you might have the following folders:
app
The app folder houses all the JavaScript code that you develop. We’ll talk about this
in more detail next.
tests
Houses all your unit tests and possibly the end-to-end scenario tests as well.
data
Any data that is common but not dynamic in your application can be stored here.
scripts
Build scripts and other common utility scripts can be stored in this folder.
Other files
The package.json, bower.json, and other files that don’t really need a directory can
be in the main folder.
Before we jump into what the app folder or the tests folder looks like, let’s get some highlevel concepts out of the way:
Project Structure | 259
www.it-ebooks.info
Group by functionality or component
A lot of examples and starter projects for AngularJS suggest a folder structure where
there is a folder for all the controllers, a folder for all the services, and so on. We’ve
seen this as a major bottleneck when a project starts growing. So instead, we suggest
that you group your code into folders based on components or functionality. So
login, authorization, and search could all be components that could have individual
folders. Your views or subsections of the app could similarly be grouped into folders,
like admin and view, within which there could be subfolders for search, list, and
so on.
Components and app sections
There are two concepts that we like to use when structuring our application. The
first one is called components, which are basically reusable widgets in your appli‐
cation that are not tied down to a certain page or section of the UI. These are things
like a datepicker directive or an authorization service—common across your ap‐
plication. The components directory contains folders that contain:
• Related services, directives, and related files.
• Dependent data like CSS, images, and others can be located here, or separately,
depending on the need.
• Each folder could be further divided into subfolders if the component is
complex.
The second is app sections, which reflect your application structure and can be
routes, pages (like search, listing, admin, view, etc.), or even small subsections of
the page (like a dashboard). The sections folders generally:
• Reflect views that are shown to the user.
• Contain only the template HTML (and CSS), and the controller that works
with it.
A component or a section (or a subcomponent or subsection) could have a module
definition of its own. The decision of whether a component or section needs a
module definition comes down to whether the module can be reused or selectively
included in different applications.
Tests should mirror the app
The tests folder would mostly contain the protractor-based, end-to-end, scenario
tests. The structure inside the tests folder would mimic your application and its
views. In that sense, each view or section of the page under test in the live running
application could have a subfolder with its own tests. The folder structure here
should reflect how your application is structured and flows through it, rather than
try to fit inside the existing directory structure.
260
| Chapter 15: Guidelines and Best Practices
www.it-ebooks.info
The unit tests, on the other hand, are colocated with your application code, in each
subfolder inside the app folder. Thus each controller, service, directive, or filter has
its unit test right alongside it in the same folder. This makes it easier to find and
manage, while giving the build tools the responsibility to ensure that tests don’t get
included in your application bundle.
Naming conventions
The Google JavaScript Style Guide is a great place to start, and we build on top of
that with a few AngularJS-specific recommendations. In particular:
• When it comes to filenames, the filename should be descriptive enough to be
able to figure out which section or component it’s in and what type of AngularJS
object it is. For example, if we have a section called adminsearch in our appli‐
cation, then the folder could be called adminsection, and the controller inside
it would be called adminsection-controller.js.
• Ideally, a controller file should end with -controller.js, a service with service.js, a directive with -directive.js, and a filter with -filter.js.
• Test files should have the filename, followed by test.js. Thus, a datepicker di‐
rective might have the filename datepicker-directive.js and the test named
datepicker-directive_test.js.
• Prefer to use lowercase when naming your files, instead of camelCase or each
word starting with uppercase.
With these thoughts and concepts in mind, let’s see how this might apply to an actual
project. Here we demonstrate how a simple CRUD application might look:
• app
— app.css
— app.js
— index.html
— components // Reusable common components
— datepicker
— datepicker-directive.js
— datepicker-directive_test.js
— authorization
— authorization.js
— authorization-service.js
— authorization-service_test.js
— ui-widgets
Project Structure | 261
www.it-ebooks.info
— ui-widgets.js
— grid
— grid.html
— grid-directive.js
— grid-directive_test.js
— dialog
— dialog-service.js
— dialog-service_test.js
— list
— list.html
— list.css
— list-controller.js
— list-controller_test.js
— login
— login.html
— login-controller.js
— search
— search.html
— search.css
— search-controller.js
— search-controller_test.js
— detail
— detail.html
— detail-controller.js
— detail-controller_test.js
— admin
— create
— create.html
— create-controller.js
— create-controller_test.js
— update
— update.html
262
| Chapter 15: Guidelines and Best Practices
www.it-ebooks.info
— update-controller.js
— update-controller_test.js
• vendors // third-party dependencies go here
— underscore
— jquery
— bootstrap
• e2e // end-to-end scenario tests
— runner.html
— login_scenario.js
— list_scenario.js
— search_scenario.js
— detail_scenario.js
— admin
— admin_create_scenario.js
— admin_update_scenario.js
Third-Party Libraries
We saw how your own application and its source code might be structured, but what
about the third-party dependencies and libraries that you can’t do without? We do pro‐
vide for a general vendors folder in the main application folder to host all the thirdparty dependencies, but that is just one part of the story.
To make managing, updating, and versioning of your third-party dependencies easy
and manageable, we suggest you use something like Bower, which is a package man‐
agement tool for web dependencies. Almost any and every third-party dependency you
use in an AngularJS project more than likely has a Bower package you can depend on.
Using Bower is a simple, three-step procedure:
1. Install Bower (Bower is a NodeJS Package, so you can simply install it by running
npm install bower).
2. List your dependencies in a Bower package definition file (usually stored as bow‐
er.json). This lists all your dependent libraries, and the version you depend on.
3. Run bower install in any project (or bower update) to get the latest and greatest
dependencies cloned right into your project folder with the correct versioning au‐
tomatically taken care of.
Project Structure | 263
www.it-ebooks.info
Bower helps by:
• Removing the necessity to check third-party libraries into your codebase.
• Easily knowing, managing, and upgrading the version of any third-party library by
just changing it in a single JSON file.
• Making it easy for anyone starting on your project to quickly figure out what thirdparty libraries you are using in your project.
In short, it makes it seamless to quickly integrate third-party libraries into your project.
AngularJS itself is available as a Bower package. So definitely give it a whirl in the next
project you start!
Starting Point
We covered a lot, and provided our opinion on how you should approach an AngularJS
project from the file and directory structure, naming conventions, and some general
good practices. But you don’t need to do this all from scratch. The AngularJS community
is awesome, and they have a lot of helpful tools and libraries to get you a starter project
from which you can start working. Some options for you to take a look at when starting
your project include:
Yeoman
Yeoman is a workflow management tool that automates a lot of the routine, chorelike tasks that are necessary in any project. It is not an AngularJS-specific tool, but
has plugins for AngularJS. AngularJS automates a lot of the boilerplate code you
might write, but the task of creating a route, adding the HTML and the controller,
creating the skeleton for the unit test, and so on still need to be done by you. Yeoman
goes one step further and automates these tasks for you. So you can write a com‐
mand such as yo angular:route myNewRoute, which generates route.html, adds a
controller skeleton, adds the test skeleton, adds the JavaScript to index.html, and
so on for you. And there are multiple AngularJS generators with slightly different
syntax and folder structures for you to find something that suits you and your needs.
Yeoman also gives you the build scripts and grunt tasks that you would need for
any medium to large project.
Angular seed projects
There are multiple seed projects for AngularJS that again, give you a starting base
set of directory structures, application skeleton, and build scripts. Some of the more
commonly used and well-known projects are ng-boilerplate and angularseed. Both of these give you the initial project structure and grunt tasks that you
need to get started.
264
| Chapter 15: Guidelines and Best Practices
www.it-ebooks.info
Mean.io
Finally, if you are in the market for an end-to-end solution, and are open to using
NodeJS for your application server needs, then do take a look at Mean.IO. The
MEAN stack, which has become famous in the past few years, stands for MongoDB,
Express, AngularJS, and NodeJS. Mean.IO provides a ready starting point for any
project using this stack, with all the necessary folder structure, build scripts, and
more. While it imposes its own structure and paradigm as well, it is well thought
out and worth a try.
Build
We talked about folder structure, naming conventions, and even bootstrapping our
AngularJS project. Some of the bootstrapping mechanisms come with build scripts and
grunt tasks already, but it is still essential that we understand what building our Angu‐
larJS project entails, and the components and key things to keep in mind when we create
one.
Grunt
Grunt is the de facto standard when it comes to build scripts for JavaScript-based
projects. Grunt is a task runner based on JavaScript and NodeJS, and comes with a whole
set of plugins to automate certain build step tasks, like:
• Concatenating files
• Copying/moving files
• Renaming files
• Minifying CSS
• Minifying JavaScript
• Running tests or shell scripts
With Grunt, we define a JavaScript file that imports all the necessary Grunt plugins, and
then we can use JavaScript to define our own tasks as well as the actions to perform
when a certain grunt task is executed. For example, we might define a build task in
Grunt, which does the following:
• Runs the Karma- and Protractor-based tests to validate the state of our application
• Globs the JavaScript files into a single JavaScript file
• Runs a JavaScript Compiler to remove spaces and reduce the size of our JavaScript
• Compiles the CSS into one small file
Build
www.it-ebooks.info
| 265
• Moves the HTML, JavaScript, and CSS into a separate directory for deployment
We could have another task to run all the unit and integration tests using Karma only
and so on. Grunt is extremely customizable and allows you to set up the tasks and flows
as you need for your application.
Serve a Single JavaScript File
With Grunt (or some other similar build tool in place), we need to keep track of a few
things and make sure they’re accomplished during our build process. First and foremost
is handling the JavaScript files (rather, the number of JavaScript files) we serve to in‐
dex.html (or any other HTML file really). When we are developing an AngularJS ap‐
plication, it makes it easier for us to develop and maintain when we have multiple files
and folders so that things are easy to read and debug. But serving 250 JavaScript files to
the production application is a sureshot recipe for a slow-loading website.
The browser usually has a restriction on the number of parallel GET requests it can
make to the same domain. So say a browser can only fetch 8 files at a time, then the 250
files will get broken down into chunks of 8 files and fetched, which will take forever on
any user’s computer.
The recommended way to serve JavaScript files in production is to glob (or combine)
all your JavaScript files into one single JavaScript file for production. This way, when
your application requests your application-specific JavaScript files, it is one request and
not 250 or even 50 separate requests.
If your application is being served on an intranet, or all the JavaScript files (including
your third-party dependencies) will be served only by your application, you can consider
combining your third-party JavaScript libraries into a single file and serving it as well.
But with third-party dependencies, it often makes sense to rely on Content Delivery
Neworks (CDNs) and get the file from those instead of serving your own version. That
way, if a user has previously loaded BootStrap from a CDN for another website, he gets
to avoid loading it again for your website and can use it directly from his cached copy.
Two great CDNs for JavaScript libraries are Google’s AJAX CDN and CDNJS. Between
the two of them, you should be able to find most core third-party dependencies.
By the way, globbing or combining files into a single file is applicable not just for Java‐
Script, but also for any CSS files you might have. The aim when you load your application
is that it should not make more than 3–5 parallel requests at any given time, to prevent
any browser from blocking and serializing the requests.
266
| Chapter 15: Guidelines and Best Practices
www.it-ebooks.info
Minification
While globbing or combining JavaScript files reduces the number of parallel requests
that are made, it still doesn’t reduce the total size or data that is transferred over the
wire. To ensure a fast, zippy application, you need to tackle both.
It is always good sense to both glob or combine your JavaScript (and CSS), as well as
run it through a minifier like UglifyJS or the Google Closure JavaScript Compiler. These
JavaScript compilers go through your JavaScript, removing unnecessary things like
spaces and comments to reduce the size of the JavaScript files. In addition, in certain
modes of the compiler, it can also rename variables and functions to smaller names,
because the browser doesn’t care what a function is named when it is executing it.
Don’t forget that you should be using the safe style of Dependency Injection before you
run your minification step. Otherwise, you might be left with a broken application. You
can also evaluate using ng-annotate, which is an open source library that converts your
nonminification-safe code into minification-safe code.
Similarly, CSS compilers also remove spaces and comments and can sometimes recog‐
nize optimal ways of rewriting CSS for it to be efficient.
Ensure that you do include minification as a build step for any application you create.
ng-templates
A grunt task that is available for online use is ng-templates, which allows you to preload
all the HTML templates that you use in your application instead of making an XHR
request for them when it is needed. The ng-templates grunt task reads all the HTML,
and generates an AngularJS JavaScript file that inlines all the HTML into AngularJS’s
templateCache when the app loads.
The ng-templates build task is great if you have a small number of templates, or are
willing to preload all your templates to speed up your runtime. But if you have a large
number of templates, you can consider preloading the most common templates and
views in your application, and let the others load asynchronously as needed.
Best Practices
With folder structure and build practices out of the way, let’s focus on some AngularJS
best practices. These are things you should do, or avoid religiously so that you aren’t
caught unawares at the worst possible time. Some of these might be pure common sense,
and others really specific, but all of them are worth keeping in mind.
Best Practices
www.it-ebooks.info
| 267
General
The following are some high-level things to keep in mind when writing your AngularJS
application:
• Prefer small files to large files. They are much easier to maintain, debug, and un‐
derstand than large files. An arbitrary rule of thumb for a large file is over 100 lines.
• Use the AngularJS version of setTimeout, which is the $timeout service, and the
AngularJS version of setInterval, which is the $interval service. You can easily
mock them out and write unit tests where you don’t actually have to wait for an
interval to finish to test your functionality. You can even control how many times
an interval function should be called from your tests. So use the AngularJS versions
instead.
• Any controller or directive, if it adds $timeout or $interval, should remember to
clean it up or cancel it when it is destroyed, to prevent it from unnecessarily exe‐
cuting in the background.
• If you are adding listeners outside of AngularJS, ensure that it is cleaned up correctly.
AngularJS manages its own scopes and listeners, but anything you manually add
might need to be managed and cleaned up. You can do this when the scope is
destroyed by adding a $scope.$on('$destroy', function() {}) listener.
• Try to avoid doing deep watches ($scope.$watch, with the third argument as true).
It is expensive, and overusing it can cause performance issues in your application.
Instead, prefer to have a simple Boolean that reflects when an object has changed,
and watch that instead.
• Try to follow the AngularJS paradigm of model-driven programming. Let the model
and data drive the UI, and if you need to update the UI, all you should have to do
is update the model. The UI should update itself automatically.
• Use HttpInterceptors when you have common tasks you need to do every time
the server returns an authorization error, or a “404 not found.” Let the services and
controllers take care of specific error handling only.
Services
Services are useful for common APIs and application-level stores. Here are some good
tips to get the most out of your services:
• Services are singletons for your application. Leverage this—use it as a service API,
as a data store, as a cache. Services are great for it.
• If you need to share state across the application, think of a service.
268
| Chapter 15: Guidelines and Best Practices
www.it-ebooks.info
• There is no performance difference between using service, factory, or provider. All
are implemented the same way. Use whichever one suits your coding style and
needs, and stick with it.
• Services are the only place where adding event listeners on the $rootScope is ac‐
ceptable. This is because services don’t have their own scope.
• Multilayered, composite services are great. Instead of having one giant service that
does everything, split it into smaller services. Then have one larger service that uses
each of the individual ones.
• XHR calls should be done in services using $http. This gives you one single place
to look at all your API calls, and change URLs in one single place. The service should
return a promise to ensure a consistent API.
• Integrations with third-party service libraries (think third-party non-UI libraries,
like SocketIO) should be done as a service. This allows you to integrate and replace
it seamlessly at any given point, as well as mock it out for unit testing.
Controllers
Here are some guidelines to keep in mind when creating your controllers:
• Prefer to use the newer syntax when working with controllers, or defining variables
and functions on the controller’s this. That is, use the controllerAs syntax and
avoid using the $scope syntax whenever possible. The newer syntax is more concise
and easier to understand.
• Watch out for using the this variable. Prefer to assign it to a local variable (like
self), and then use it.
• Controllers should not reference the DOM or reach out into the DOM. Do not use
jQuery directly in your controllers.
• Controllers should ideally just have the presentation logic of what data to fetch, how
to show it, and how to handle user interactions. And most of these should pass
through to a service when possible.
• Only put the variables and functions that need to be accessed from the HTML on
the controller’s this. Anything that the HTML does not need can and should be
local variables inside the controller. The exception, of course, are functions that you
want to unit test.
• If a controller is for a specific route that needs to be accessible via a URL, then ensure
that the controller loads all the data it needs when it is instantiated.
• If a controller needs to store some state for the entire application, it should be stored
in a service, and not $rootScope. Never $rootScope.
Best Practices
www.it-ebooks.info
| 269
• Controllers can $broadcast or $emit events on their own scope, or inject the
$rootScope and fire events on $rootScope. But a controller should never add a
listener on $rootScope. This is because a controller and its scope can get destroyed,
but the $rootScope remains across the application, along with its listeners, which
will keep triggering even if the controller is not present.
Directives
Directives are some of the most powerful parts of AngularJS. Here’s how you can ensure
you get the most out of your directives:
• If you’re bringing in a third-party UI component, bring it in as an AngularJS
directive.
• Try to isolate the scope if you want reusable components, because this ensures that
you don’t modify the parent scope, or depend on anything from any parent scope.
• Don’t forget to $scope.$apply() in case you’re responding to an event or callback
that is external to AngularJS and updating the AngularJS model. Otherwise, your
UI won’t get updated at the correct moment.
• If you add any event listeners on elements external to the directive, or any polling
functions, ensure that you clean it up when the directive gets destroyed.
• You should do your cleanup on the $scope $destroy event if you are creating a new
child scope or an isolated scope. But in case you’re inheriting the parent scope,
prefer to do your cleanup on the $element $destroy event.
• If a controller needs to share state with a directive, it should:
— Pass in the state using HTML attributes (and the isolated scope) if the component
is not specific to your project and can be reusable.
— Pass in the state using a service if it is a specific component.
• Pass in an object using the = binding on the scope, and add a $scope.$watch on it
if you need to perform an operation whenever the object changes.
• Never reassign the reference of any object passed in through the scope. That is, if
the scope.firstObject is passed in via = binding, you should never set or overwrite
the value of scope.firstObject in your directive. Updating a key on firstOb
ject is fine, but the firstObject itself should never be reassigned.
Filters
Filters are great for the last step formatting that we need to do, or data manipulation.
Here are two things to keep in mind:
270
| Chapter 15: Guidelines and Best Practices
www.it-ebooks.info
• Every filter used in the HTML gets evaluated in every digest cycle. If you know the
data is not going to change that often, you can optimize by using and applying the
filter directly in your controller as shown in Chapter 8.
• Filters should be fast, because each filter is expected to execute multiple times during
the life of an application. So don’t do any heavy processing, and definitely don’t do
any DOM manipulation inside your filter.
Tools and Libraries
In this final section, we’ll look at some tools and libraries that can make your life as an
AngularJS developer easier. These could be developer tools, or existing component li‐
braries and modules for common requirements in your projects.
Batarang
First and foremost, if you are working on AngularJS, you owe it to yourself to go get
Batarang, a Chrome extension to help debug and work with AngularJS projects. When
installed, it adds an AngularJS tab to the Developer Tools in Chrome as shown in
Figure 15-1.
Figure 15-1. Batarang extension in Chrome Developer Tools
The Batarang extension adds three very interesting and useful tabs to the Chrome De‐
veloper Tools:
Models
The Models tab contains a live view into the scope hierarchy of your AngularJS
application. The Models tab shows you all the scopes that are currently present in
your view, and allows you to click any scope to see the variables and functions that
are present and their current values. For any scope, you can click the < icon to jump
to the HTML element that the scope is present on, to see where in your HTML a
Tools and Libraries | 271
www.it-ebooks.info
particular scope is. As you navigate around in your page, the scope values also get
updated.
Performance
The Performance tabs gives you a live view of the performance of your AngularJS
application. It gives you a sorted list of the watchers triggered in a digest cycle, and
the time taken in milliseconds and as a percentage of the digest cycle. With this
information, if you are optimizing your application or figuring out your bottle‐
necks, you have an immediate action list of things you need to tackle. You can also
export the performance as JSON so that you can compare it after you have made
your changes and fixes to see how it stacks up.
Dependencies
The last tab gives you a visualization of the dependency structure of your applica‐
tion. It lists all the services that your application provides and creates, as well as
ones it depends on. It then tells you visually which services depend on which other
services, and so on. A great starting point for anyone new to the application, or
trying to figure out the core, most important, and used services in an application.
When Batarang is installed, you can actually dig into any live AngularJS application by
just opening the Chrome Developer Tools and enabling Batarang for the website.
WebStorm
An IDE, or lack thereof, can make a huge difference in the productivity of a develop‐
er. And with JavaScript, with its lack of compiler, type safety, and dynamic nature, it
becomes essential to have a solid working environment. WebStorm, a JavaScript IDE
from Jetbrains, is one of the most solid IDEs out there currently for JavaScript. And the
best part? It is superbly integrated with AngularJS and Karma, to make an AngularJS
developer’s life easy with features like:
• AngularJS autocomplete for HTML attributes like ng-click, ng-class, and so on.
• Ability to jump to external (online) documentation for any directive while using it.
• Control- (or Command-) click any directive, function, or controller in your HTML
to jump to its definition.
• Refactoring support, from renaming of variables and properties, to even directives.
• Live templates to create skeletons for common tasks like controller and directive
definitions.
• Karma integration, to run Karma unit tests right from within WebStorm.
And much more. We highly recommend WebStorm for AngularJS development, so give
the 30-day free trial a whirl. You can’t go wrong with it.
272
| Chapter 15: Guidelines and Best Practices
www.it-ebooks.info
Optional Modules
Finally, we will quickly cover some of the other optional modules that AngularJS core
provides, and when you would need to or want to use them:
ngCookies
Traditionally, cookies have been string-based for years on end. The ngCookies
module gives you a service to interact with browser cookies in an object form. You
can directly save objects with keys, and retrieve them as objects, instead of dealing
with strings and parsing them. A very useful module in case you are working with
cookies in your application.
ngSanitize
If you have the need to work with user-generated content, and need to display it in
the UI, you can possibly expose yourself to Cross Site Scripting (XSS) attacks, es‐
pecially if the user enters JavaScript and HTML where he should be entering text.
The ngSanitize module gives you APIs and directives to declare which inputs are
validated, and how to render them. You can opt to render some content as entered,
or render it as HTML, or even as HTML with JavaScript and CSS execution allowed.
A must-have module in case you are dealing with content that the user can enter
and manipulate.
ngResource
The $http library is a low-level resource, needing us as developers to be specific in
the URL we hit, the arguments, the method of the request, and so on. But to ensure
reusability and separation of concerns, we would wrap the $http service in our own
service, and provide methods like save, query, and update. If we have a RESTful
server, on the other hand, we can use the ngResource module and the $resource
service. The $resource service takes a URL regex, and knows how to automatically
translate that into RESTful calls. For example, if our server has a REST API for
projects at the following URL: /api/project/:pId, and we define a service called
project that returns $resource('/api/project/:pId'), then it creates a service
that allows us to make calls like Project.query() or Project.save({pId: 15},
data), and have AngularJS automatically translate the URLs to GET /api/project or
POST to /api/project/15 with data without us manually having to define the query
and save functions. The ngResource is a great module if you have RESTful APIs on
your server side.
ngTouch
AngularJS plays great on mobile as well, with its small footprint and minimal de‐
pendencies. The ngTouch module is a great add-on that adds directives like ngswipeleft and ng-swiperight for dealing with touch interfaces. In addition, it also
handles something called fastclick, which is necessary to ensure your mobile web
application responds instantly to touch events.
Tools and Libraries | 273
www.it-ebooks.info
ngAnimate
Finally, a new introduction with AngularJS 1.2 is the ngAnimate module. This com‐
pletely optional module allows you define animations for common transitions in
AngularJS. With the ngAnimate module, you can animate items hiding and showing
using ng-show and ng-hide, even complete views and addition and removal of
classes. When the ngAnimate module is included, most AngularJS directives give
you CSS-class based hooks to animate or transition different elements that you can
define your own animations on. Want your UI to slide in from the right? Want to
have an element in the repeater displace the other elements and then fade in? You
can do all this and more using ngAnimate.
Conclusion
In this final chapter, we covered testing at a high level and the best ways to get the most
out of your unit and integration tests in a project, including the best times to run them.
We then covered one of the most asked about questions in AngularJS, which is how to
structure your project and folders. We talked about some concepts to keep in mind and
saw how it might apply to a straightforward CRUD application.
We then jumped into the aspect after your application is done, which is building and
deploying it. We covered Grunt and how it could be integrated, along with some addi‐
tional things to keep in mind when deploying AngularJS projects. Our next topic was
best practices in AngularJS, both from a general standpoint, as well as specific to con‐
trollers, directives, services, and filters. Finally, we looked at tools and libraries like
Batarang and WebStorm, as well as the optional modules that AngularJS includes.
This brings us to the end of our journey together with this book. We tried to cover all
the various parts at a reasonable depth in an order that made sense. There is nowhere
near enough space or time for this book to cover each and every part of AngularJS, but
this gives you a very strong base on which to rapidly build amazing, sleek, and per‐
formant web applications. Keep trying new things, and join us in the journey of making
AngularJS a great framework!
274
| Chapter 15: Guidelines and Best Practices
www.it-ebooks.info
Index
Symbols
" (quote mark), 172
$ (dollar sign), 74
$$ (double dollar sign), 32
$apply, 242
$digest, 242
$even variable, 30
$first variable, 30
$http
advanced uses
best practices, 104
configuring defaults, 99
interceptors, 101
core functionality of, 78
fetching data using GET
$http API, 96
$q service, 94
configuration object, 97
making POST requests, 94
promises concept, 91
propagating sucess and error, 93
traditional method, 87
unit testing, 115
$httpBackend service, 116
$httpProvider.responseInterceptors, 101
$index variable, 30
$last variable, 30
$location service, 77
$middle variable, 30
$odd variable, 30
$q service, 94
$routeParams service, 148
$scope variable, 20
$window service, 77
& (ampersand sign), 184
= (equal sign), 183
@ (at sign), 184
[ ] (square brackets), 16
{ } (curly braces), 24
A
AJAX App indexing, 162
ampersand sign (&), 184
Angular Seed Projects, 264
angular-mocks.js file, 115
angular.copy function, 233
angular.equals, 233
angular.forEach function, 232
angular.fromJson function, 232
angular.isArra function, 233
angular.isDate function, 233
angular.isFunction, 233
angular.isNumber function, 233
angular.isObject function, 233
angular.isString function, 233
angular.toJson function, 232
We’d like to hear your suggestions for improving our indexes. Send email to
[email protected].
275
www.it-ebooks.info
AngularJS
basic application
bootstrapping, 12
downloading the code, 11
hello world application, 12
loading source code, 12
basic setup
application requirements, 11
backend requirements, 10, 11
benefits of, 2, 3
best practices, 267–271
convenience functions in, 232
core concept of, 2
core feature of, 5
life cycle of
digest cycle, 206
directive life cycle, 208
events executed, 203
flow diagram, 204, 207
need for, 1
philosophy of
data-driven, 4, 22
declarative, 6
dependency injection, 9
extensible, 9
separation of concerns, 8
testable, 10
routing in, 141
(see also routing)
SEO and, 162
AngularJS filters
best practices, 133, 271
commonly used, 124
creating, 131
examples of, 122
features of, 121
selecting/changing order with, 127
tasks handled by, 121
using in controllers and services, 130
AngularJS Scenario Runner, 246
AngularJS services
benefits of, 70
commonly used, 77
creating your own
criteria for, 78
example of, 78
factory vs. service, 82
with ngResource, 88
dependency injection, 73
276
functionality implemented by, 69
services vs. controllers, 72
unit testing, 107
using built-in, 74
AngularJS XHR API, 88
arguments
in controller definition, 18
in module definition, 16
arrays
displaying, 25
filtering, 122
working with, 22
at sign (@), 184
B
backend requirements, 10
basic application
bootstrapping, 12
downloading the code, 11
hello world, 12
loading source code, 12
Batarang, 271
behavior-driven testing, 42
best practices (see guidelines/best practices)
Bower, 263
C
checkboxes, 63
combo boxes, 66
compile step, 228–234
continuous integration, 258
controllerAs syntax, 20
controllers
benefits/drawbacks of, 70
best practices, 269
controllerAs syntax, 20
declaring, 18
defining, 18
dependencies and, 18
directive controllers
creating, 216
custom validators, 226
input with ng-model, 222
require options, 221
tabs directive, 219
examples of, 21
hello world application, 19
responsibilities of, 17
| Index
www.it-ebooks.info
unit testing for, 44
using filters in, 130
vs. services, 72
convenience functions, 232
CRUD (Create-Retrieve-Update-Delete) model,
4
CSS classes
form states, 58
input states, 58
curly braces ({ }), 24
currency filter, 124
D
data-binding
benefits of, 5
example of, 21
in forms, 52
ng-model directive, 49
one-way, 5, 12
two-way, 6, 49
data-driven programming, 4, 22
date filter, 124
declarative paradigm, 7
declarative programming, 6
dependencies, 15, 18, 263
dependency injection
benefits of, 9, 73
order of, 76
safe style of, 18, 75
syntax of, 18
unit testing, 107
digest cycle, 206
directive controllers
creating, 216
custom validators, 226
input directives with ng-model, 222
require options, 221
tabs directive, 219
directives
advanced
AngularJS life cycle, 203–208
best practices, 239–242
compile step, 228–234
directive controllers and require, 216–
228
priority and terminal, 234
third-party integration, 234–239
transclusions, 209–216
best practices for, 180, 270
clean up and destruction of, 240
creating
directive goals, 175
initial steps, 176
link keyword, 181
names for, 176
replace keyword, 192
restrict keyword, 179
scope, 182–192
template/template URL, 176
input directives, 222
major types of, 169
ng-app, 25
ng-bind, 24
ng-class, 27
ng-click, 21, 32
ng-cloak, 24
ng-controller, 18
ng-form, 60
ng-href, 155
ng-include, 170–173
ng-model, 49, 222
ng-repeat, 23, 27–33
ng-repeat-start, 32
ng-repeat-stop, 32
ng-show, 27
ng-src, 155
ng-switch, 173
ng-transclude, 211
purpose of, 7, 169
unit testing
bindings and, 202
challenges of, 195
dependent services and, 201
HTML rendering and, 201
set up for, 197
steps involved, 195
stock directive example, 196
directory structure, 259
dollar sign ($), 74
dollar sign, double ($$), 32
DOM elements
manipulating, 169
reusing, 30
double-curly notation, 24
dropdowns, 66
Index
www.it-ebooks.info
| 277
E
G
elements
hiding, 24
manipulating, 169
reusing, 30
end-to-end tests, 245
(see also Protractor)
equal sign (=), 183
error handling
built-in validators, 55
displaying error messages, 56
error handler argument, 90
styling forms and states, 58
event listeners, 241
extensibility
benefits of, 9
Promise objects and, 90
GET, fetching data with
$http API, 96
$q service, 94
configuration object, 97
POST requests, 94
promises concept, 91
propagating success and error, 93
traditional method, 87
Google Analytics, 164
Google indexing, 162
Grunt, 265
guidelines/best practices
$http, 104
build scripts, 265
controllers, 269
directives, 180, 239–242, 270
directory structure, 259
filters, 133, 270
JavaScript file handling, 266
minification, 267
ng-templates, 267
optional modules, 273
project structure, 258
services, 268
testing
test variety, 256
test-driven development, 256
timing of, 255, 257
third-party libraries, 263
tools, 271
F
filter filter, 128
filters
best practices, 133, 271
commonly used, 124
creating, 131
examples of, 122
features of, 121
selecting/changing order with, 127
tasks handled by, 121
unit testing, 135
using in controllers and services, 130
for each loops, 23
FormController, 55
forms
built-in validators, 55
checkboxes, 63
combo boxes/dropdowns, 66
data format in, 52
error handling with, 55–60
form states, 55
nested, 60
ng-model directive for, 49
radio buttons, 65
structure of model and bindings, 51
textareas, 62
validation of, 54
278
H
hello world application, 12, 19
helper variables, 29
HTML partials, 172
HTML rendering, unit testing of, 201
HTML5 mode, routing in, 160
I
imperative paradigm, 7
indexing, 162
inputs, 49
(see also forms)
integration-level tests, 118, 256
interceptors, 101
| Index
www.it-ebooks.info
J
M
Jasmine
behavior-driven style of, 42
matchers, 43
spies, 113
syntax, 42
test frameworks vs. test runners, 37
writing a unit test, 44
JavaScript
file handling, 266
this keyword in, 22
JSON filter, 124
Mean.io, 265
minification, 267
mock files, 111, 115
Model-View-Controller pattern (MVC), 2, 8
models
definition of, 2
structure for forms, 51
modules
arguments and, 16
common errors using, 16
defining, 16
dependencies and, 15
loading existing, 16
MVC architectural pattern and, 2
optional, 273
parts of, 15
purpose of, 15
K
Karma
benefits of, 37
configuration file generation, 41
configuration file options, 39
installing, 37
plugins
browser launchers, 38
installing, 38
integrations, 39
reporters, 39
testing frameworks, 39
running, 38, 47
test runners vs. testing frameworks, 37
unit testing of server calls, 115
unit testing of services, 107
karma-cli package, 38
karma.conf.js, 39
keys, displaying, 27
L
libraries, third-party, 263
life cycle
digest cycle, 206
directive life cycle, 208
events executed, 203
flow diagram, 204, 207
limitTo filter, 127
link function, 233
link keyword, 181
login/logout links, 152
lowercase filter, 124
N
ng-app directive, 25
ng-bind directive, 24
ng-class directive, 27
ng-click directive, 21, 32
ng-cloak directive, 24
ng-controller directive, 18
ng-form directive, 60
ng-href directive, 155
ng-include directive, 170–173
ng-model directive, 49, 222
ng-repeat directive
across multiple HTML elements, 32
exposing variables with, 29
repeating arrays with, 23
showing keys/values with, 27
track by ID with, 30
ng-repeat-start directive, 32
ng-repeat-stop directive, 32
ng-show directive, 27
ng-src directive, 155
ng-submit function, 53
ng-switch directive, 173
ng-transclude directive, 211
ngAnimate module, 274
ngCookies module, 273
ngResource module, 88, 273
ngRoute module
alternatives to, 165
Index
www.it-ebooks.info
| 279
benefits of, 139
Google Analytics and, 164
HTML5 mode and, 160
routing example, 150–159
routing in SPAs, 140
routing options
$routeParams service, 148
complex templates, 143
potential problems, 149
using resolves for pre-route checks, 146
search engine optimization and, 162
using, 141
ngSanitize module, 273
ngTouch module, 273
number filter, 124
objects
Promise objects, 90
showing keys/values of, 27
orderBy filter, 128
resolves
benefits of, 146
defining, 143
example of, 146
pre-route checks with, 146
response iterceptors, 101
restrict keyword, 179
RESTUL APIs, 88
routing
alternative choices for, 165
challenges of, 139
example of, 150–159
Google Analytics and, 164
HTML5 mode and, 160
in SPAs, 140
options in
$routeParams service, 148
complex templates, 143
potential problems, 149
using resolves for pre-route checks, 146
search engine optimization and, 162
using ngRoute module, 141
P
S
O
packages, 15
polling, 252
POST requests, 94
post-link function, 233
pre-route checks, 146
presenters, 2
priority option, 234
private variables, 32
Promise API, 88, 90, 92
Protractor
benefits of, 246
configuration, 247
end-to-end testing with, 248
initial setup, 246
key-points, 251
Q
quotes, single vs. double, 172
R
radio buttons, 65
replace keyword, 192
require keyword, 221
280
safe-style of Dependency Injection, 18, 75
scalability, 90
scenario tests, 245, 257
scope, 182–192, 240
search engine optimization (SEO), 162
Selenium WebDriver, 246
separation of concerns, 2, 8
server communication
advanced $http
best practices, 104
configuring defaults, 99
interceptors, 101
fetching data
$http API, 96
$q service, 94
configuration object, 97
making POST requests, 94
promises concept, 91
propagating success and error, 93
traditional method, 87
unit testing, 115
services
benefits of, 70
best practices, 268
commonly used, 77
| Index
www.it-ebooks.info
creating your own
criteria for, 78
example of, 78
factory vs. service, 82
with ngResource, 88
dependency injection, 73
functionality implemented by, 69
services vs. controllers, 72
unit testing, 107
using built-in, 74
using filters in, 130
Single Page Application (SPA)
benefits of AngularJS for, 3
challenges in testing, 246
multiple views in, 139
route contexts in, 148
routing in, 140
SEO and, 162
single vs. double quotes, 172
spies, 113
spyOn function, 114
square brackets ([ ]), 16
success handlers, 90
T
TDD (test-driven development), 36, 256
(see also unit testing)
templates, 23, 176, 267
terminal option, 234
test driven development (see TDD)
textareas, 62
then function, 90
third-party integration, 234–239, 263
this keyword, 22
track-by expression, 32
tracking expressions, 30
transclusions
advanced, 212–216
basic, 211
example of, 209
purpose of, 209
early error detection, 36
lack of compiler, 36
proof of correctness, 35
regression prevention, 36
specification, 36
directives
bindings and, 202
challenges of, 195
dependent services and, 201
HTML rendering and, 201
set up for, 197
steps involved, 195
stock directive example, 196
filters
optional arguments in, 135
timeAgo filter example, 136
Jasmine
alternatives to, 37
behavior-driven style of, 42
matchers, 43
syntax, 42
Karma
benefits of, 37
configuration file generation, 41
configuration file options, 39
installing, 37
plugins, 38
running, 38
test runners vs. testing frameworks, 37
running a test, 47
server calls
integration-level tests, 118
mocking larger system, 115
XHR calls, 116
services
Karma configuration for, 107
mocking, 111
spies, 113
state across unit tests, 109
writing a test, 44
uppercase filter, 124
V
U
UI widgets, 169
ui-router, 165
unit testing
benefits of
as component in overall testing, 256
validation, 36, 54, 55, 226
values, displaying, 27
variables
$ prefixed, 30
$routeParam variable type, 150
exposing, 29
Index
www.it-ebooks.info
| 281
private, 32
viewmodels, 2
WebStorm IDE, 272
widgets, 169
W
Y
watchers, 241
Yeoman, 264
282
| Index
www.it-ebooks.info
About the Authors
Shyam Seshadri is the owner/CEO of Fundoo Solutions, where he splits his time be‐
tween working on innovative and exciting new products for the Indian markets, and
consulting about and running workshops on AngularJS. Prior to Fundoo Solutions,
Shyam completed his MBA from the prestigious Indian School of Business in Hydera‐
bad. Shyam’s first job out of college was with Google, where he worked on multiple
projects, including Google Feedback (AngularJS’s first customer!), and various internal
tools and projects. Shyam currently operates from his office in Navi Mumbai, India.
Brad Green works at Google as an engineering manager. In addition to the AngularJS
project, Brad also directs Accessibility and Support Engineering. Prior to Google, Brad
worked on the early mobile web at AvantGo, founded and sold startups, and spent a few
hard years toiling as a caterer. Brad’s first job out of school was as lackey to Steve Jobs
at NeXT Computer writing demo software and designing his slide presentations. Brad
lives in Mountain View, CA, with his wife and two children.
Colophon
The animal on the cover of AngularJS: Up and Running is a thornback cowfish (Lactoria
fornasini). This fish of many names—thornback, thornback cow, backspine cowfish,
shortspined cowfish, blue-spotted cowfish—is usually found on rocky reefs or sandy
slopes in a tangle of sponge and weeds in the Western Indo-Pacific region. They feed
primarily on worms and other invertebrates.
These boxfish can grow up to 15 centimeters long and anywhere between 3 to 50 cen‐
timeters wide. Members of the boxfish family are recognizable by the hexagonal pattern
on their skin. Their bodies are shaped like a boxy triangle from which their fins, tail,
eyes, and mouth protrude, allowing them to swim with a rowing motion. As they age,
their shapes change from more rounded to more square-shaped, and their brighter
colors dim.
The thornback cowfish protects itself by secreting cationic surfactants through their
skin, a reaction triggered by stress. The toxins, usually secreted in the form of a mucus,
dissolve into the environment and irritate fish in the surrounding area.
Many of the animals on O’Reilly covers are endangered; all of them are important to
the world. To learn more about how you can help, go to animals.oreilly.com.
The cover image is from Johnson’s Natural History. The cover fonts are URW Typewriter
and Guardian Sans. The text font is Adobe Minion Pro; the heading font is Adobe
Myriad Condensed; and the code font is Dalton Maag’s Ubuntu Mono.
www.it-ebooks.info