AngularJS 教程


Brad Green and Shyam Seshadri AngularJS AngularJS by Brad Green and Shyam Seshadri Copyright © 2013 Brad Green and Shyam Seshadri. 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://my.safaribooksonline.com). For more information, contact our corporate/ institutional sales department: 800-998-9938 or corporate@oreilly.com. Editors: Simon St. Laurent and Meghan Blanchette Production Editor: Melanie Yarbrough Copyeditor: Rachel Leach Proofreader: Jilly Gagnon Indexer: J udith McConville Cover Designer: Randy Comer Interior Designer: David Futato Illustrator: Rebecca Demarest April 2013: First Edition Revision History for the First Edition: 2013-04-05: First release See http://oreilly.com/catalog/errata.csp?isbn=9781449344856 for release details. Nutshell Handbook, the Nutshell Handbook logo, and the O’Reilly logo are registered trademarks of O’Reilly Media, Inc. AngularJS, 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 trade‐ mark 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-449-34485-6 LSI Table of Contents Preface. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vii 1. Introduction to AngularJS. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Concepts 1 Client-Side Templates 2 Model View Controller (MVC) 3 Data Binding 3 Dependency Injection 5 Directives 5 An Example: Shopping Cart 6 Up Next 9 2. Anatomy of an AngularJS Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Invoking Angular 11 Loading the Script 11 Declaring Angular’s Boundaries with ng-app 12 Model View Controller 12 Templates and Data Binding 14 Displaying Text 15 Form Inputs 16 A Few Words on Unobtrusive JavaScript 19 Lists, Tables, and Other Repeated Elements 21 Hiding and Showing 23 CSS Classes and Styles 24 Considerations for src and href Attributes 26 Expressions 26 Separating UI Responsibilities with Controllers 27 Publishing Model Data with Scopes 28 Observing Model Changes with $watch 29 iii Performance Considerations in watch() 31 Organizing Dependencies with Modules 33 How Many Modules Do I Need? 36 Formatting Data with Filters 37 Changing Views with Routes and $location 38 index.html 39 list.html 39 detail.html 40 controllers.js 40 Talking to Servers 41 Changing the DOM with Directives 43 index.html 44 controllers.js 44 Validating User Input 45 Moving On 46 3. Developing in AngularJS. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 Project Organization 47 Tools 50 IDEs 50 Running Your Application 51 With Yeoman 51 Without Yeoman 51 Testing with AngularJS 52 Karma 52 Unit Tests 54 End-to-End/Integration Tests 55 Compilation 57 Other Awesome Tools 59 Debugging 59 Batarang 60 Yeoman: Optimizing Your Workflow 64 Installing Yeoman 65 Starting a Fresh AngularJS project 65 Running Your Server 65 Adding New Routes, Views, and Controllers 65 The Testing Story 66 Building Your Project 66 Integrating AngularJS with RequireJS 67 4. Analyzing an AngularJS App. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 The Application 77 iv | Table of Contents Relationship Between Model, Controller, and Template 78 The Model 79 Controllers, Directives, and Services, Oh My! 80 Services 80 Directives 84 Controllers 85 The Templates 89 The Tests 95 Unit Tests 96 Scenario Tests 99 5. Communicating with Servers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 Communicating Over $http 101 Configuring Your Request Further 103 Setting HTTP Headers 104 Caching Responses 105 Transformations on Requests and Responses 106 Unit Testing 107 Working with RESTful Resources 108 The Declaration 111 Custom Methods 111 No Callbacks! (Unless You Really Want Them) 112 Simplified Server-Side Operations 112 Unit Test the ngResource 112 The $q and the Promise 113 Response Interception 115 Security Considerations 115 JSON Vulnerability 116 XSRF 116 6. Directives. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 Directives and HTML Validation 119 API Overview 120 Naming Your Directive 121 The Directive Definition Object 122 Transclusion 126 Compile and Link Functions 126 Scopes 128 Manipulating DOM Elements 132 Controllers 133 Table of Contents | v Moving On 136 7. Other Concerns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 $location 137 HTML5 Mode and Hashbang Mode 140 AngularJS Module Methods 142 Where’s the Main Method? 142 Loading and Dependencies 143 Convenience Methods 144 Communicating Between Scopes with $on, $emit, and $broadcast 146 Cookies 148 Internationalization and Localization 148 What Can I Do in AngularJS? 149 How Do I Get It All Working? 149 Common Gotchas 150 Sanitizing HTML & the Sanitize Module 150 Linky 152 8. Cheatsheet and Recipes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 Wrapping a jQuery Datepicker 153 ng-model 155 Binding select 155 Calling select 155 The Rest of the Example 156 The Teams List App: Filtering and Controller Communication 157 The Search Box 161 The Combo Boxes 161 The Check Box 161 The Repeater 161 File Upload in AngularJS 162 Using Socket.IO 164 A Simple Pagination Service 167 Working with Servers and Login 171 Conclusion 174 Index. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 vi | Table of Contents Preface I can trace Angular’s beginnings to 2009, on a project called Google Feedback. We’d gone through months of frustration with our development speed and ability to write testable code. At around the six month mark, we had around 17,000 lines of front-end code. At that point, one of the team members, Misko Hevery, made a bold statement that he’d be able to rewrite the whole thing in two weeks using an open source library that he’d created as a hobby. I figured that a two week delay couldn’t hurt us that much and we’d at least be entertained by Misko scrambling to build something. Misko missed his time estimate. It took three weeks. We were all astounded, but even more astounding was that the line count for this new app had dropped from 17,000 to a mere 1,500. It seemed that Misko was onto something worth pursuing. Misko and I decided we’d built a team around the concepts he started with a simple charter: to simplify the web developer’s experience. Shyam Seshadri, this book’s co- author, went on to lead the Google Feedback team in developing Angular’s first shipping application. Since then, we’ve developed Angular with guidance both from teams at Google and from hundreds of open source contributors around the world. Thousands of developers rely on Angular in their daily work and contribute to an amazing support network. We’re excited to learn what you’ll teach us. 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. vii 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 icon signifies a tip, suggestion, or general note. This icon indicates a warning or caution. Using Code Examples This book is here to help you get your job done. In general, if this book includes code examples, you may use the code in this book 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 example code from this book into your product’s documentation does require per‐ mission. We appreciate, but do not require, attribution. An attribution usually includes the title, author, publisher, and ISBN. For example: “AngularJS by Brad Green and Shyam Se‐ shadri (O’Reilly). Copyright 2013 Brad Green and Shyam Seshadri, 978-1-449-34485-6.” If you feel your use of code examples falls outside fair use or the permission given above, feel free to contact us at permissions@oreilly.com. viii | Preface Safari® Books Online Safari Books Online is an on-demand digital library that delivers ex‐ pert 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 product mixes and pricing programs for organi‐ zations, government agencies, and individuals. Subscribers have access to thousands of books, training videos, and prepublication manuscripts in one fully searchable database from publishers like O’Reilly Media, Prentice Hall Professional, Addison-Wesley Pro‐ fessional, 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, Jones & Bartlett, Course Technol‐ ogy, and dozens 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://oreil.ly/angularJS. To comment or ask technical questions about this book, send email to bookques tions@oreilly.com. 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 Preface | ix Acknowledgments We’d like to give special thanks to Misko Hevery, father of Angular, for having the courage to think very differently about how we write web applications and to drive it into reality; to Igor Minar for bringing stability and structure to the Angular project and for building the roots of today’s awesome open source community; to Vojta Jina for creating many parts of Angular, and for giving us the fastest test runner the world has ever seen; to Naomi Black, John Lindquist, and Mathias Matias Niemelä for their expert editing assistance. And finally, thank you to the Angular community for their contri‐ butions, and for teaching us about making Angular great through feedback from build‐ ing real applications. x | Preface CHAPTER 1 Introduction to AngularJS Our ability to create amazing web-based apps is incredible, but the complexity involved in making these apps is similarly incredible. We on the Angular team wanted to relieve the pain involved with developing AJAX applications. At Google, we’d worked through the hard lessons of building large web applications like Gmail, Maps, Calendar, and several others. We thought we might be able to use these experiences to benefit everyone. We wanted writing web apps to feel more like the first time we wrote a few lines of code and stood back in amazement at what we’d made happen. We wanted the coding process to feel more like creating and less like trying to satisfy the strange inner workings of web browsers. At the same time, we wanted an environment that helped us make the design choices that make apps easy to create and understand from the start, but that continue to be the right choices to make our apps easy to test, extend, and maintain as they grow large. We’ve tried to do this in the Angular framework. We’re very excited about the results we’ve achieved. A lot of credit goes to the open source community around Angular who do a fantastic job supporting each other and who have taught us many things. We hope you’ll join our community and help us learn how Angular can be even better. Some of the larger and more involved examples and code snippets are available on a GitHub repository for you to look at, fork, and play with at our GitHub page. Concepts There are a few core ideas that you’ll use throughout an Angular app. As it turns out, we didn’t invent any of these. Instead, we’ve borrowed heavily from successful idioms in other development environments and implemented them in a way that embraces HTML, browsers, and many other familiar web standards. 1 Client-Side Templates Multi-page web applications create their HTML by assembling and joining it with data on the server, and then shipping the finished pages up to the browser. Most single-page applications—also known as AJAX apps—do this as well, to some extent. Angular is different in that the template and data get shipped to the browser to be assembled there. The role of the server then becomes only to serve as static resources for the templates and to properly serve the data required by those templates. Let’s see an example of what assembling this data and template on the browser looks like in Angular. We’ll take the obligatory Hello, World example, but instead of writing “Hello, World” as a single string, let’s structure the greeting “Hello” as data that we could change later. For it, we’ll create our template in hello.html:

{{greeting.text}}, World

And our logic in controllers.js: function HelloController($scope) { $scope.greeting = { text: 'Hello' }; } Loading hello.html into any browser will then produce what we see in Figure 1-1: Figure 1-1. Hello, World There are a few interesting things to note here in comparison with most methods in widespread use today: • There are no classes or IDs in the HTML to identify where to attach event listeners. • When HelloController set the greeting.text to Hello, we didn’t have to register any event listeners or write any callbacks. 2 | Chapter 1: Introduction to AngularJS • HelloController is a plain JavaScript class, and doesn’t inherit from anything that Angular provides. • HelloController got the $scope object that it needed without having to create it. • We didn’t have to call the HelloController’s constructor ourselves, or figure out when to call it. We’ll look at more differences soon, but it should be clear already that Angular appli‐ cations are structured very differently than similar applications were in the past. Why have we made these design choices and how does Angular work? Let’s look at some good ideas Angular stole from elsewhere. Model View Controller (MVC) MVC application structure was introduced in the 1970s as part of Smalltalk. From its start in Smalltalk, MVC became popular in nearly every desktop development envi‐ ronment where user interfaces were involved. Whether you were using C++, Java, or Objective-C, there was some flavor of MVC available. Until recently, however, MVC was all but foreign to web development. The core idea behind MVC is that you have clear separation in your code between managing its data (model), the application logic (controller), and presenting the data to the user (view). The view gets data from the model to display to the user. When a user interacts with the application by clicking or typing, the controller responds by changing data in the model. Finally, the model notifies the view that a change has occurred so that it can update what it displays. In Angular applications, the view is the Document Object Model (DOM), the controllers are JavaScript classes, and the model data is stored in object properties. We think MVC is neat for several reasons. First, it gives you a mental model for where to put what, so you don’t have to invent it every time. Other folks collaborating on your project will have an instant leg up on understanding what you’ve written, as they’ll know you’re using MVC structure to organize your code. Perhaps most importantly, we’ll claim that it delivers great benefits in making your app easier to extend, maintain, and test. Data Binding Before AJAX single-page apps were common, platforms like Rails, PHP, or JSP helped us create the user interface (UI) by merging strings of HTML with data before sending it to the users to display it. Concepts | 3 Libraries like jQuery extended this model to the client and let us follow a similar style, but with the ability to update, part of the DOM separately, rather than updating the whole page. Here, we merge template HTML strings with data, then insert the result where we want it in the DOM by setting innerHtml on a placeholder element. This all works pretty well, but when you want to insert fresher data into the UI, or change the data based on user input, you need to do quite a bit of non-trivial work to make sure you get the data into the correct state, both in the UI and in JavaScript properties. But what if we could have all this work done for us without writing code? What if we could just declare which parts of the UI map to which JavaScript properties and have them sync automatically? This style of programming is called data binding. We included it in Angular because it works great with MVC to eliminate code when writing your view and model. Most of the work in moving data from one to the other just happens automatically. To see this in action, let’s take the first example and make it dynamic. As is, the Hello Controller sets the model greeting.text once and it never changes from then on. To make it live, let’s change the example by adding a text input that can change the value of greeting.text as the user types. Here’s the new template:

{{greeting.text}}, World

The controller, HelloController, can stay exactly the same. Loading it in a browser, we’d see the screen captured in Figure 1-2. Figure 1-2. The default state of the greeting app If we replace Hello with Hi in the input field, we’d see the screen captured in Figure 1-3. 4 | Chapter 1: Introduction to AngularJS Figure 1-3. The Greeting App with input changed Without ever registering a change listener on the input field, we have a UI that will dynamically update. The same would be true for changes coming to and from the server. In our controller, we could make a request to our server, get the response, and set $scope.greeting.text to equal what it returns. Angular would automatically update both the input and the text in the curly braces to that value. Dependency Injection We mentioned it before, but it bears repeating that there’s a lot going on with Hello Controller that we didn’t have to write. For example, the $scope object that does our data binding is passed to us automatically; we didn’t have to create it by calling any function. We just asked for it by putting it in HelloController’s constructor. As we’ll find out in later chapters, $scope isn’t the only thing we can ask for. If we want to data bind to the location URL in the user’s browser, we can ask for an object that manages this by putting $location in our constructor, like so: function HelloController($scope, $location) { $scope.greeting = { text: 'Hello' }; // use $location for something good here... } We get this magical effect through Angular’s dependency injection system. Dependency injection lets us follow a development style in which, instead of creating dependencies, our classes just ask for what they need. This follows a design pattern called the Law of Demeter, also known as the principle of least knowledge. Since our HelloController’s job is to set up the initial state for the greeting model, this pattern would say that it shouldn’t worry about anything else, like how $scope gets created, or where to find it. This feature isn’t just for objects created by the Angular framework. You can write the rest of this code as well. Directives One of the best parts of Angular is that you can write your templates as HTML. You can do this because at the core of the framework we’ve included a powerful DOM trans‐ formation engine that lets you extend HTML’s syntax. Concepts | 5 We’ve already seen several new attributes in our templates that aren’t part of the HTML specification. Examples include the double-curly notation for data binding, ng- controller for specifying which controller oversees which part of the view, and ng- model, which binds an input to part of the model. We call these HTML extension directives. Angular comes with many directives that help you define the view for your app. We’ll see more of them soon. These directives can define what we commonly view as the template. They can declaratively set up how your application works or be used to create reusable components. And you’re not limited to the directives that Angular comes with. You can write your own to extend HTML’s template abilities to do anything you can dream of. An Example: Shopping Cart Let’s look at a slightly larger example that shows off a bit more of Angular. Let’s imagine that we’re going to build a shopping app. Somewhere in the app we’ll need to show the user’s shopping cart and let him edit it. Let’s skip straight to that part. Your Shopping Cart

Your Order

{{item.title}} {{item.price | currency}} {{item.price * item.quantity | currency}}
6 | Chapter 1: Introduction to AngularJS The resulting UI looks like the screenshot in Figure 1-4. Figure 1-4. The Shopping Cart UI The following is a brief tour of what’s going on here. The rest of the book is dedicated to a more in-depth explanation. Let’s start at the top: The ng-app attribute tells Angular which parts of the page it should manage. Since we’ve placed it on the element, we’re telling Angular that we want it to manage the whole page. This will often be what you want, but you might want to place it on a
within the app if you’re integrating Angular with an existing app that uses other methods to manage the page. In Angular, you manage areas of the page with JavaScript classes called controllers. By including a controller in the body tag, I’m declaring that CartController will manage everything between and .
The ng-repeat says to copy the DOM inside this
once for every element in an array called items. On every copy of the div, it will also set a property named item to the current element so we can use it in the template. As you can see, this results in three
s each, containing the product title, quantity, unit price, total price, and a button to remove the item entirely. {{item.title}} As we showed in the “Hello, World” example, data binding via {{ }} lets us insert the value of a variable into part of the page and keep it in sync. The full expression {{item.title}} retrieves the current item in the iteration and then inserts the contents of that item’s title property into the DOM. The ng-model definition creates data binding between the input field and the value of item.quantity. An Example: Shopping Cart | 7 The {{ }} in the sets up a one-way relationship that says “insert a value here.” We want that effect, but the application also needs to know when the user changes the quantity so it can change the total price. We’ll keep changes in sync with our model by using ng-model. The ng-model declaration inserts the value of item.quantity into the text field, but it also automatically updates item.quantity whenever the user types a new value. {{item.price | currency}} {{item.price * item.quantity | currency}} We want the unit price and total price to be formatted as dollars. Angular comes with a feature called filters that lets us transform text, and there’s a bundled filter called currency that will do this dollar formatting for us. We’ll look at filters more in the next chapter. This allows users to remove items from their carts by clicking a Remove button next to the product. We’ve set it up so that clicking this button calls a remove() function. We’ve also passed in $index, which contains the iteration number of the ng-repeat, so we know which item to remove. function CartController($scope) { This CartController manages the logic of the shopping cart. We’ll tell Angular that the controller needs something called $scope by putting it here. The $scope is what lets us bind data to elements in the UI. $scope.items = [ {title: 'Paint pots', quantity: 8, price: 3.95}, {title: 'Polka dots', quantity: 17, price: 12.95}, {title: 'Pebbles', quantity: 5, price: 6.95} ]; By defining $scope.items, I’ve created a dummy data hash to represent the collection of items in the user’s shopping cart. We want to make them available to data bind with the UI, so we’ll add them to $scope. Of course, a real version of this can’t just work in memory, and will need to talk to a server to properly persist the data. We’ll get to that in later chapters. $scope.remove = function(index) { $scope.items.splice(index, 1); } We want the remove() function available to bind in the UI, so we’ve added this to $scope as well. For the in-memory version of the shopping cart, the remove() function can just delete items from the array. Because the list of
s created by ng-repeat is data 8 | Chapter 1: Introduction to AngularJS bound, the list automatically shrinks when items disappear. Remember, this remove() function gets called from the UI whenever the user clicks on one of the Remove buttons. Up Next We’ve looked at just the most basic idioms in Angular and some very simple examples. The rest of the book is dedicated to showing off what the framework has to offer. Up Next | 9 CHAPTER 2 Anatomy of an AngularJS Application Unlike typical libraries where you pick and choose functions as you like, everything in Angular is designed to be used as a collaborative suite. In this chapter we’ll cover all of the basic building blocks in Angular so you can understand how they fit together. Many of these blocks will be covered in more detail in later chapters. Invoking Angular Any application must do two things to start Angular: 1. Load the angular.js library 2. Tell Angular which part of the DOM it should manage with the ng-app directive Loading the Script Loading the library is straightforward and follows the same rules as any other JavaScript library. You can load the script from Google’s content delivery network (CDN), like so: Using Google’s CDN is recommended. Google’s servers are fast, and the script is cache‐ able across applications. That is, if your user has multiple apps that use Angular, she’ll have to download only it once. Also, if the user has visited other sites that use the Google CDN link for Angular, she won’t need to download it again when visiting your site. If you prefer to host locally (or anywhere else), you can do that too. Just specify the correct location in the src. 11 Declaring Angular’s Boundaries with ng-app The ng-app directive lets you tell Angular which part of your page it should expect to manage. If you’re building an all-Angular application, you should include ng-app as part of the tag, like so: … This tells Angular to manage all DOM elements in the page. If you’ve got an existing app where some other technology expects to manage the DOM, such as Java or Rails, you can tell Angular to manage only a part of the page by placing it on some element like a
within the page. …
… Model View Controller In Chapter 1, we mentioned that Angular supports the Model View Controller style of application design. Though you have a lot of flexibility in designing your Angular app, you will always have some flavor of: • A model containing data that represents the current state of your application. • Views that display this data. • Controllers that manage the relationship between your model and your views. You’ll create your model using object attributes, or even just primitive types containing your data. There’s nothing special about model variables. If you want to display some text to the user, you could have a string, like so: var someText = 'You have started your journey.'; You create your views by writing a template as an HTML page and merging it with data from your model. As we’ve seen, you can insert a placeholder in the DOM and set its text like this:

{{someText}}

We call this double-curly syntax interpolation, as it inserts new content into an existing template. 12 | Chapter 2: Anatomy of an AngularJS Application The controllers are classes or types you write to tell Angular which objects or primitives make up your model by assigning them to the $scope object passed into your controller: function TextController($scope) { $scope.someText = someText; } Bringing it all together, we have:

{{someText}}

Loading this in a browser, you would see: You have started your journey. Though this primitive-style model works in simple cases, for most applications you’ll want to create a model object to contain your data. We’ll create a messages model object and use it to store our someText. So instead of: var someText = 'You have started your journey.'; you would write: var messages = {}; messages.someText = 'You have started your journey.'; function TextController($scope) { $scope.messages = messages; } and use it in your template as:

{{messages.someText}}

As we’ll see later when we discuss the $scope object, creating a model object like this will prevent unexpected behavior that could be caused by the prototypal inheritance in $scope objects. While we’re discussing practices that will save you in the long run, in the previous example, we’ve created TextController in the global scope. While this is fine for ex‐ amples, the right way to define a controller is as part of something called a module, Model View Controller | 13 which provides a namespace for related parts of your application. The updated code would look like the following:

{{someText.message}}

In this version, we told our ng-app element about the name of our module, myApp. We then called the Angular object to create a module named myApp and pass our controller’s function to a call to that module’s controller function. We’ll get to all the whys and hows of modules in a bit. For now, just remember that keeping things out of the global namespace is a good thing and that modules are the mechanism we use to do so. Templates and Data Binding Templates in Angular applications are just HTML documents that we load from the server or define in a

A-Mail

As our view templates will be inserted into the shell we just created, we can write them as partial HTML documents. For the email list, we’ll use ng-repeat to iterate through a list of messages and render them into a table. list.html Changing Views with Routes and $location | 39
Sender Subject Date
{{message.sender}} {{message.subject}} {{message.date}}
Notice here that we’re going to let the user navigate to a particular message by clicking on the subject. We’ve data bound the URL to message.id, so clicking on a message with id=1 will take the user to /#/view/1. We’ll use this navigation-by-url, also known as deep- linking, in the message detail view’s controller, to make a particular message available to the detail view. To create this message detail view, we’ll create a template that displays properties from a single message object. detail.html
Subject: {{message.subject}}
Sender: {{message.sender}}
Date: {{message.date}}
To: {{recipient}}
{{message.message}}
Back to message list Now, to associate these templates with some controllers, we’ll configure the $routePro vider with the URLs that invoke our controllers and templates. controllers.js // Create a module for our core AMail services var aMailServices = angular.module('AMail', []); // Set up our mappings between URLs, templates, and controllers function emailRouteConfig($routeProvider) { $routeProvider. when('/', { controller: ListController, templateUrl: 'list.html' }). // Notice that for the detail view, we specify a parameterized URL component // by placing a colon in front of the id when('/view/:id', { controller: DetailController, templateUrl: 'detail.html' }). otherwise({ redirectTo: '/' 40 | Chapter 2: Anatomy of an AngularJS Application }); } // Set up our route so the AMail service can find it aMailServices.config(emailRouteConfig); // Some fake emails messages = [{ id: 0, sender: 'jean@somecompany.com', subject: 'Hi there, old friend', date: 'Dec 7, 2013 12:32:00', recipients: ['greg@somecompany.com'], message: 'Hey, we should get together for lunch sometime and catch up.' +'There are many things we should collaborate on this year.' }, { id: 1, sender: 'maria@somecompany.com', subject: 'Where did you leave my laptop?', date: 'Dec 7, 2013 8:15:12', recipients: ['greg@somecompany.com'], message: 'I thought you were going to put it in my desk drawer.' +'But it does not seem to be there.' }, { id: 2, sender: 'bill@somecompany.com', subject: 'Lost python', date: 'Dec 6, 2013 20:35:02', recipients: ['greg@somecompany.com'], message: "Nobody panic, but my pet python is missing from her cage.' +'She doesn't move too fast, so just call me if you see her." }, ]; // Publish our messages for the list template function ListController($scope) { $scope.messages = messages; } // Get the message id from the route (parsed from the URL) and use it to // find the right message object. function DetailController($scope, $routeParams) { $scope.message = messages[$routeParams.id]; } We’ve created the basic structure for an app with many views. We switch views by changing the URL. This means that the forward and back buttons just work for users. Users are able to bookmark and email links to views within the app, even though there is only one real HTML page. Talking to Servers Okay, enough messing around. Real apps generally talk to real servers. Mobile apps and the emerging Chrome desktop apps may be exceptions, but for everything else, whether you want persistence in the cloud or real-time interactions with other users, you prob‐ ably want your app to talk to a server. For this, Angular provides a service called $http. It has an extensive list of abstractions that make it easier to talk to servers. It supports vanilla HTTP, JSONP, and CORS. It Talking to Servers | 41 includes security provisions to protect from both JSON vulnerabilities and XSRF. It lets you easily transform the request and response data, and it even implements simple caching. Let’s say we want to retrieve products for our shopping site from a server instead of from our silly in-memory mocks. Writing the server bits is beyond the scope of this book, so let’s just imagine that we’ve created a service that will return a list of products as JSON when you make a query to /products. Given a response that looks like this: [ { "id": 0, "title": "Paint pots", "description": "Pots full of paint", "price": 3.95 }, { "id": 1, "title": "Polka dots", "description": "Dots with that polka groove", "price": 12.95 }, { "id": 2, "title": "Pebbles", "description": "Just little rocks, really", "price": 6.95 } ...etc... ] we could write the query like so: function ShoppingController($scope, $http) { $http.get('/products').success(function(data, status, headers, config) { $scope.items = data; }); } and use it in a template like this:

Shop!

{{item.title}} {{item.description}} {{item.price | currency}}
42 | Chapter 2: Anatomy of an AngularJS Application
As we learned previously, we would be better off in the long run by delegating the work of talking to the server to a service that could be shared across controllers. We’ll take a look at this structure and the full range of $http functions in Chapter 5. Changing the DOM with Directives Directives extend HTML syntax, and are the way to associate behavior and DOM trans‐ formations with custom elements and attributes. Through them, you can create reusable UI components, configure your application, and do almost anything else you can imag‐ ine wanting to do in your UI template. You can write apps with the built-in directives that come with Angular, but you’ll likely run into situations where you want to write your own. You’ll know it’s time to break into directives when you want to deal with browser events or modify the DOM in a way that isn’t already supported by the built-in directives. This code of yours belongs in a directive that you write, and not in a controller, service, or any other place in your app. As with services, you define directives through the module object’s API by calling its directive() function, where directiveFunction is a factory function that defines your directive’s features. var appModule = angular.module('appModule', [...]); appModule.directive('directiveName', directiveFunction); Writing the directive factory function is a deep area, and we’ve dedicated an entire chapter to it in this book. To whet your appetite, though, let’s look at a simple example. HTML5 has a great new attribute called autofocus that will place keyboard focus on an input element. You’d use it to let the user start interacting with the element via his keyboard without having to click on it first. This is great, as it lets you declaratively specify what you want the browser to do without having to write any JavaScript. But what if you wanted to place focus on some non-input element, like a link or any div? And what if you wanted it to work on non-HTML5 browsers as well? We could do it with a directive. var appModule = angular.module('app', []); appModule.directive('ngbkFocus', function() { return { link: function(scope, element, attrs, controller) { element[0].focus(); } }; }); Changing the DOM with Directives | 43 Here, we’re returning the directive configuration object with its link function specified. The link function gets a reference to the enclosing scope, the DOM element it lives on, an array of any attributes passed to the directive, and the controller on the DOM element, if it exists. Here, we only need to get at the element and call its focus() method. We can then use it in an example like so: index.html ...include angular and other scripts...
{{message.text}}
controllers.js function SomeController($scope) { $scope.message = { text: 'nothing clicked yet' }; $scope.clickUnfocused = function() { $scope.message.text = 'unfocused button clicked'; }; $scope.clickFocused = function { $scope.message.text = 'focus button clicked'; } } var appModule = angular.module('app', ['directives']); When the page loads, the user will see the button labeled “I’m very focused!” with the focus highlight. Hitting the spacebar or the enter key will cause a click and invoke the ng-click, which will set the div text to ‘focus button clicked’. Opening this example in a browser, we’d see something that looks like Figure 2-4. Figure 2-4. Focus directive 44 | Chapter 2: Anatomy of an AngularJS Application Validating User Input Angular automatically augments
elements with several nice features suitable for single-page applications. One of these nice features is that Angular lets you declare valid states for inputs within the form and allow submission only when the entire set of elements is valid. For example, if we’re creating a signup form where we require entering a name and email, but have an optional age field, we can validate several user entries before they are submitted to the server. Loading the example that follows into a browser will display what is shown in Figure 2-5. Figure 2-5. Form validation We’d want to make sure the user had entered text in the name fields, that he had entered a properly formed email address, and that if he entered an age, it was valid. We can do this all in the template, using Angular’s extensions to and the various input elements, like this:

Sign Up

First name:
Last name:
Email:
Age:
Notice that we’re using the required attribute and input types for email and number from HTML5 to do our validation on some of the fields. This works great with Angular, and in older non-HTML5 browsers, Angular will polyfill these with directives that per‐ form the same jobs. We can then add a controller to this to handle the submission by changing the form to reference it. Validating User Input | 45
Inside the controller, we can access the validation state of the form through a property called $valid. Angular will set this to true when all the inputs in the form are valid. We can use this $valid property to do nifty things such as disabling the Submit button when the form isn’t completed yet. We can prevent form submission in an invalid state by adding ng-disabled to the Submit button: Finally, we might want the controller to tell the user she’s been successfully added. Our final template would look like:

Sign Up

{{message}}
First name:
Last name:
Email:
Age:
with controller: function AddUserController($scope) { $scope.message = ''; $scope.addUser = function () { // TODO for the reader: actually save user to database... $scope.message = 'Thanks, ' + $scope.user.first + ', we added you!'; }; } Moving On In the last two chapters, we looked at all the most commonly used features in the Angular framework. For each feature discussed, there are many additional details we have yet to cover. In the next chapter, we’ll get you going by examining a typical development workflow. 46 | Chapter 2: Anatomy of an AngularJS Application CHAPTER 3 Developing in AngularJS By now we have delved a little bit into the cogs that make up AngularJS. We now know how to get data from the user into our application, how to display text, and how to do some funky stuff with validation, filtering, and even changing the DOM. But how do we put it all together? In this chapter, we will cover: • How to lay out your AngularJS app for rapid development • Starting your server to see your AngularJS app in action • Writing and running your unit and scenario tests using Karma • Compiling and minifying your AngularJS app for production deployment • Debugging your AngularJS app using Batarang • Simplifying your development workflow (from creating new files to running your application and tests) • Integrating your AngularJS project with RequireJS, a dependency management li‐ brary This chapter aims to give you a 20,000-foot view of how to possibly lay out your An‐ gularJS app. We won’t go into the actual app itself. That is for Chapter 4, which dives into a sample application that uses and shows off various AngularJS features. Project Organization We recommend seeding your project using Yeoman, which will create all the necessary files to bootstrap your AngularJS application. Yeoman is a robust tool comprised of multiple frameworks and client-side libraries. It provides a rapid development environment by automating some routine tasks needed 47 to bootstrap and develop your application. We’ll go through a whole section on how to install and use Yeoman this chapter, but until then, we will briefly touch upon Yeoman commands as alternatives to manually performing those operations. We will also detail the various pieces involved in case you decide not to use Yeoman because Yeoman does have some issues on Windows computers, and getting it set up can be slightly challenging. For those not using Yeoman, we will take a look at a sample application structure (which can be found in the chapter3/sample-app folder in our GitHub examples repository), which follows the recommended structure, as well as the structure generated by Yeoman. The files in the application can be broken into the following categories: JS source files Take a look at the app/scripts folder. This is where all your JS source code lives. One main file (app/scripts/app.js) will set up the the Angular module and the routes for your application. In addition, there is a separate folder—app/scripts/controller—which houses the individual controllers. Controllers provide the action and publish data to the scope which will then be displayed in the view. Usually, they correspond one to one with the view. Directives, filters, and services can also be found under app/scripts, either as com‐ plete files (directives.js, filters.js, services.js), or individually, if they are nice and complex. HTML Angular template files Now, every AngularJS partial template that Yeoman creates can be found in the app/views folder. This will mirror our app/scripts/controller folder for the most part. There is one other important Angular template file, which is the main app/ index.html. This is responsible for sourcing the AngularJS source files, as well as any source files you create for your application. If you end up creating a new JS file, ensure that you add it to the index.html, and also update the main module and the routes (Yeoman does this for you as well!). JS library dependencies Yeoman provides you the app/scripts/vendor folder for all JS source dependencies. Want to use Underscore or SocketIO in your application? No problem—add the dependency to the vendor folder (and your index.html!) and start referencing it in your application. Static resources You are creating an HTML application in the end, and it is a given that you will have CSS and image dependencies that you need served as part of your application. 48 | Chapter 3: Developing in AngularJS The app/styles and app/img folders are for this very purpose. Just add what you need and start referring to them (with the correct relative paths, of course!) in your application. Yeoman does not create the app/img path by default. Unit tests Testing is super important, and totally effortless when it comes to AngularJS. The test/spec folder should mirror your app/scripts in terms of tests. Each file should have a mirror spec file which has its unit tests. The seed creates a stub for each controller file, under test/spec/controllers, with the same name as the original con‐ troller. These are Jasmine-style specs, which describe a specification for each ex‐ pected behavior of the controller. Integration tests AngularJS comes with end-to-end testing support built right into the library. All your E2E tests, in the form of Jasmine specs, are saved under the folder tests/e2e. Yeoman does not create the tests/folder by default. While the E2E tests might look like Jasmine, they are not. They are functions that are executed asynchronously, in the future, by the Angular Scenario Runner. So don’t expect to be able to do stuff like you would in a normal Jasmine test (like console.log on the value of a repeater). There is also a simple HTML file generated that can be opened by itself in a browser to run the tests manually. Yeoman doesn’t generate the stubs for these yet, but they follow a similar style to the unit tests. Configuration files There are two configuration files needed. The first one, karma.conf.js, is generated by Yeoman for you and is used to run the unit tests. The second one, which Yeoman does not generate yet, is the karma.e2e.conf.js. This is used to run the scenario tests. There is a sample file at the end of this chapter in the RequireJS integration section. The config details the dependencies and the files to use when running the unit tests using Karma. By default, it runs the Karma server at port 9876. You might ask: how do I run my application? What about unit tests? How do I even write these various pieces that you are talking about? Project Organization | 49 Don’t worry, young grasshopper, all in due time. In this chapter, we will deal with setting up your project and development environment so that things can move along at a rapid pace once we do start churning out some awesome code. What code you write, and how it hooks together to form your final awesome application, will come in the next few chapters. Tools AngularJS is just one part of your toolkit that allows you to actually develop your web pages. In this section, we will take a look at various tools that you would use to ensure efficient and fast development, from IDEs to test runners to debuggers. IDEs Let’s start with how you actually edit your source code. There is a whole slew of JavaScript editors out there, both free and paid. Things have come a long way from the days when Emacs or Vi was the best option to develop in JS. Nowadays, IDEs come with syntax highlighting, auto-completion, and so much more, and it might be worth your while to give one a whirl. So which one should you use? WebStorm. If you don’t mind shelling out a few bucks (though there is a free 30-day trial!), then WebStorm by JetBrains offers one of the most comprehensive web devel‐ opment platforms in recent times. It has features that were only previously available for typed languages, including code-completion (browser specific at that, as shown in Figure 3-1), code navigation, syntax, error highlighting, and out-of-the-box support for multiple libraries and frameworks. In addition, there is some very nice integration for debugging JavaScript right from the IDE while it is executing in Chrome. Figure 3-1. Browser specific code completion in WebStorm The biggest reason you should seriously consider WebStorm for AngularJS develop‐ ment is that it is one of the only IDEs that has an AngularJS plug-in. The plug-in gives you auto-complete support for AngularJS HTML tags right in your HTML templates. In addition, one of the coolest things it supports is the concept of live templates. These are pre-formed templates for common code snippets that you would otherwise type from scratch every time. So instead of typing the following: directive('$directiveName$', function factory($injectables$) { var directiveDefinitionObject = { 50 | Chapter 3: Developing in AngularJS $directiveAttrs$ compile: function compile(tElement, tAttrs, transclude) { $END$ return function (scope, element, attrs) { } } }; return directiveDefinitionObject; }); in WebStorm, you can just type: ngdc and press the tab key to get the same thing. This is just one of the many code-completions the plug-in provides. Running Your Application Now let’s talk about how we get to the payload of all that we do—seeing your application live, in the browser. To really get a feel for how the application would work, we need to have a web server serving our HTML and JavaScript code. I will explore two ways: one very simple way of running your application with Yeoman, and another not so easy, but just as good, method without Yeoman. With Yeoman Yeoman makes it simple for you to start a web server and serve all your static and AngularJS-related files. Just execute the following command: yeoman server and it will start up a server and open your browser with the main page of your AngularJS application. It will even refresh the browser whenever you make changes to your source code. How cool is that? Without Yeoman Without Yeoman, you would need to configure a web server to serve all the files in your main directory. If you don’t know an easy way to do that, or don’t want to waste time creating your own web server, you can quickly write a simple web server using ExpressJS (as simple as npm install -g express to get it) in Node. It might look something like the following: // available at chapter3/sample-app/web-server.js var express = require("express"), app = express(), port = parseInt(process.env.PORT, 10) || 8080; Running Your Application | 51 app.configure(function(){ app.use(express.methodOverride()); app.use(express.bodyParser()); app.use(express.static(__dirname + '/')); app.use(app.router); }); app.listen(port); console.log('Now serving the app at http://localhost:' + port + '/app'); Once you have the file, you can run the file using Node, by executing the following command: node web-server.js and it will start up the server on port 8080 (or one of your own choosing). Alternatively, with Python in the folder with your application you could run: python -m SimpleHTTPServer Whichever way you decide to proceed, once you have the server configured, up and running, navigate to the following: http://localhost:[port-number]/app/index.html in your browser to see the application you have just created. Do note that you will have to manually refresh your browser to see the changes, unlike with Yeoman. Testing with AngularJS We have said it before (even right in this chapter), and we will say it again: testing is essential, and AngularJS makes it simple to write the right kind of unit and integration tests. While AngularJS plays nicely with multiple test runners, we strongly believe that Karma trumps most of them providing the most robust, solid, and insanely fast test runner for all your needs. Karma Karma’s main reason for existence is to make your test-driven development (TDD) workflow simple, fast, and fun. It uses NodeJS and SocketIO (you don’t need to know what they are, just assume that they are awesome, cool libraries) to allow running your 52 | Chapter 3: Developing in AngularJS code, and tests in multiple browsers at insanely fast speeds. Go find out more at https:// github.com/vojtajina/karma/. TDD: An Intro Test-driven development, or TDD, is an AGILE methodology that flips the development lifecycle by ensuring that tests are written first, 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 • Once all tests are passing, the next failing test is added for the next required func‐ tionality. These simple rules ensure that: • Your code develops organically, and that every line of code written is purposeful. • Your code remains highly modular, cohesive, and reusable (as you need to be able to test it). • You 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 this to be true, and the entire AngularJS codebase has been developed using TDD. For an uncompiled, dynamic language like JavaScript, we strong‐ ly believe that having a good set of unit tests will reduce headaches in the future! So how do we get this awesomeness that is Karma? Well, first ensure that NodeJS is installed on your machine. This comes with NPM (Node Package Manager), which makes it easy to manage and install the thousands of libraries available for NodeJS. Once you have NodeJS and NPM installed, installing Karma is as easy as running: sudo npm install -g karma There you go. You are ready to start Karmaing (I just made that up, please don’t go about using it in real life) in three easy steps! Testing with AngularJS | 53 Getting your config file up If you used Yeoman to create your app skeleton, then you already have a ready- made Karma config file waiting for you to use. If not, just go ahead and execute the following command from the base folder of your application directory: karma init in your terminal console, and it will generate a dummy config file (karma.conf.js) for you to edit to your liking, with some pretty standard defaults. You can use that. Starting the Karma server Just run the following command: karma start [optionalPathToConfigFile] This will start the Karma server on port 9876 (the default, which you can change by editing the karma.conf.js file from the previous step). While Karma should open up a browser and capture it automatically, it will print all the instructions needed to capture another browser in the console. If you are too lazy to do that, just go to http://localhost:9876 in another browser or device, and you are good to start run‐ ning tests in multiple browsers. While Karma can capture the usual browsers automatically, on start (Firefox, Chrome, IE, Opera, and even PhantomJS), it is not limited to just those browsers. Any device on which you can browse to a URL can possibly be a runner for Karma. So if you open up the browser of your iPhone or Android device and browse to http://machinename:9876 (provided it is accessible!), you could potentially run your tests on mo‐ bile devices as well. Running the tests Execute the following command: karma run That’s it. You should get your results printed right in the console where you ran the command. Easy, isn’t it? Unit Tests AngularJS makes it easy to write your unit tests, and supports the Jasmine style of writing tests by default (as does Karma). Jasmine is what we call a behavior-driven development framework, which allows you to write specifications that denote how your code should behave. A sample test in Jasmine might look something like this. 54 | Chapter 3: Developing in AngularJS describe("MyController:", function() { it("to work correctly", function() { var a = 12; var b = a; expect(a).toBe(b); expect(a).not.toBe(null); }); }); As you can see, it lends itself to a very readable format, as most of the code that could be read in plain English. It also provides a very diverse and powerful set of matchers (like the expect clauses), and of course has the xUnit staples of setUp and tearDowns (functions that are executed before and after each individual test case). AngularJS provides some nice mockups, as well as testing functions, to allow you to create services, controllers, and filters right in your unit tests, as well as mock out HttpRequests and the like. We will cover this in Chapter 5. Karma can be integrated with your development workflow to make it easier, as well as to get faster feedback on the code you have written. Integration with IDEs Karma does not have plug-ins (yet!) for all the latest and greatest IDEs, but you don’t really need any. All you need to do is add a shortcut command to execute “karma start” and “karma run” from within your IDE. This can usually be done by adding a simple script to execute, or the actual shell command, depending on your choice of editor. You should see the results every time it finishes running, of course. Running tests on every change This is utopia for many TDD developers: being able to run all their tests, every time they press save, within a few milliseconds, and get results back quickly. And this can be done with AngularJS + Karma pretty easily. Turns out, the Karma config file (remember the karma.conf.js file from before?) has an innocuous-looking flag named “autoWatch”. Setting it to true tells Karma to run your tests every time the file it watches (which is your source and test code) changes. And if you do “karma start” from within your IDE, guess what? The results from the Karma run will be available right within your IDE. You won’t even need to switch to console or terminal to figure out what broke! End-to-End/Integration Tests As applications grow (and they tend to, really fast, before you even realize it), testing whether they work as intended manually just doesn’t cut it anymore. After all, every time you add a new feature, you have to not only verify that the new feature works, but also that your old features still work, and that there are no bugs or regressions. If you End-to-End/Integration Tests | 55 start adding multiple browsers, you can easily see how this can become a combinatorial explosion! AngularJS tries to ease that by providing a Scenario Runner that simulates user inter‐ actions with your application. The Scenario Runner allows you to describe your application in a Jasmine-like syntax. Just as with the unit tests before, we will have a series of describes (for the feature), and individual its (to describe each individual functionality of the feature). As always, you can have some common actions, to be performed before and after each spec (as we call a test). A sample test that looks at an application that filters a list of results might look something like the following: describe('Search Results', function() { beforeEach(function() { browser().navigateTo('http://localhost:8000/app/index.html'); }); it('should filter results', function() { input('searchBox').enter('jacksparrow'); element(':button').click(); expect(repeater('ul li').count()).toEqual(10); input('filterText').enter('Bees'); expect(repeater('ul li').count()).toEqual(1); }); }); There are two ways of running these tests. Either way you run them, though, you must have a web server started that serves your application (refer to previous section for more information on how to do that). Once that is done, use one of the following methods: 1. Automated: Karma now supports running of Angular scenario tests. Create a Kar‐ ma config file with the following changes: a. Add ANGULAR_SCENARIO & ANGULAR_SCENARIO_ADAPTER to the files section of the config. b. Add a proxies section that redirects requests to the server to the correct folder where your test files are located, for example: proxies = {'/': 'http://localhost:8000/test/e2e/'}; c. Add a Karma root to ensure that Karma’s source files don’t interfere with your tests, like so: urlRoot = '/_karma_/'; Then just remember to capture your Karma server by browsing to http://local‐ host:9876/_karma_, and you should be free to run your tests using Karma. 56 | Chapter 3: Developing in AngularJS 2. Manual: The manual method allows you to open a simple page from your web server and run (and see) all the tests. To do so, you must: a. Create a simple runner.html file, which sources the angular-scenario.js file from the Angular library. b. Source all your JS files which hold the specifications that you have written as part of your Scenario suite. c. Start your web server, and browse to the runner.html file. Why should you use the Angular Scenario Runner over, say, an external third party integration or end-to-end test runner? There are some amazing benefits that you get from using the Scenario Runner, including: AngularJS aware The Angular Scenario Runner, as the name suggests, is made by and for Angular. Thus, it is AngularJS aware, and knows and understands the various AngularJS elements, like bindings. Need to input some text? Check the value of a binding? Verify the state of a repeater? All can be done easily through the use of the scenario runner. No more random waits The Angular awareness also means that Angular is aware of all XHRs being made to the server, and thus can avoid waiting for random intervals of time for pages to load. The Scenario Runner knows when a page has loaded, and thus is much more deterministic than a Selenium test, for example, where tests can fail by timing out while waiting for a page to load. Debugging capabilities Wouldn’t it be nice if you could look at your code, dig into the JavaScript, and pause and resume the test when you wanted to, all while the Scenario tests were running? With the Angular Scenario Runner, all this is possible, and much more. Compilation Compilation in the JavaScript world usually means minification of the code, though there is some amount of actual compilation possible using the Google Closure Library. But why would you want to convert all that glorious, well-written, and easily under‐ standable code to almost pure gibberish? One reason is the goal of making applications that are quick and responsive for the user. That is a major reason why client-side applications took off like they did a few years ago. And the sooner you can get your application up and running, the sooner it will be responsive. Compilation | 57 That responsiveness is the motivation of minification of JS code. The smaller the code, the smaller the payload, and the faster the transmission of the file to the user’s browser. This becomes especially important in mobile apps, where size becomes the bottleneck. There are a few ways you can minify the AngularJS code that you have written for your app, each with varying levels of effectiveness. Basic and simple optimization This involves minifying all the variables that you use in your code, but avoiding minifying the properties. This is known as the Simple optimization pass in Closure Compiler. This will not give you a great reduction in file size, but you’ll still get a substantial one, for minimal overhead. The reason this works is that the compiler (Closure or UglifyJS) avoids renaming properties that are referenced from the templates. Thus, your templates continue to work, and only local variables and parameters are renamed. With Google Closure, this is as simple as calling: java -jar closure_compiler.jar --compilation_level SIMPLE_OPTIMIZATIONS --js path/to/file.js Advanced optimization Advanced optimization is a bit more tricky, as it tries to rename pretty much any and every function possible. To get this level of optimization to work, you will need to handhold the compiler a bit by telling it explicitly (through the use of an ex terns file) which functions, variables, and properties should not be renamed. These are generally the functions and properties accessed by the templates. The compiler will use this externs file and then rename everything else. If done properly, this can result in a substantial reduction in the size of your JavaScript, but it does require a significant amount of work, including updating the externs file every time your code changes. One thing to keep in mind: you have to use the declared form of dependency in‐ jection (specifying the $inject property on the controller) when you want to minify your code. This will not work: function MyController($scope, $resource) { // Stuff here } You will need to do one of the following instead: function MyController($scope, $resource) { // Same stuff here 58 | Chapter 3: Developing in AngularJS } MyController.$inject = [‘$scope’, ‘$resource’]; or use the module, like so: myAppModule.controller(‘MyController’, [‘$scope’, ‘$resource’, function($scope, $resource) { // Same stuff here }]); This is the only way AngularJS can figure out which service or variable you were originally asking for once all the variables are obfuscated or compressed. It is generally good practice to use the array-style injection all the time, to avoid bugs later when you start compiling the code. Scratching your head later and trying to figure out why the provider of the $e variable (the minified, obfuscated version of some service) is suddenly missing is just not worth it. Other Awesome Tools In this section, we will take a look at some other tools that will help ease your develop‐ ment flow and make you that much more productive. These range from debugging with Batarang to actual coding and development with Yeoman. Debugging When you work with JavaScript, debugging your code in the browser is going to become second nature. The sooner you accept that, the better off you will be. Thankfully, things have come a long way since the old days when there was no Firebug. Now, regardless of the choice of browser, there is generally something you can use to step in to your code, analyze your errors, and figure out the state of the application. Get to know the Developer Tools in Chrome and Internet Explorer; Firebug works across Firefox and Chrome. A few further tips to help you out when debugging your application: • Always, always switch to the non-minified version of all your source code and dependencies when you want to debug. Not only will you get better variable names, you’ll also get line numbers and actual useful information and debugging capabil‐ ities. • Try to keep your source code in individual JS files, not inlined in HTML. Other Awesome Tools | 59 • Breakpoints are useful! They allow you to check the state of your application, its models, and everything in between at a given point in time. • “Pause on all exceptions” is a very useful option that is built in to most developer tools nowadays. The debugger will halt when an exception occurs, and highlight the line causing it. Batarang And then, of course, we have Batarang. Batarang is a Chrome extension that adds An‐ gularJS knowledge to the built-in Developer Tools suite in Google Chrome. Once in‐ stalled (you can get it from http://bit.ly/batarangjs), it adds another tab to the Developer Tools panel of Chrome called AngularJS. Have you ever wondered what the current state of your AngularJS application is? What each model, each scope, and each variable currently contains? How is the performance of your application? if you haven’t yet, trust me, you will! And when you do, Batarang is there for you! There are four main useful additions in Batarang. Model tab Batarang allows you to dig into the scope, from the root downwards. You can then see how scopes are nested and how models are attached to them (as shown in Figure 3-2). You can even change them in real time and see the changes reflected in your application. How cool is that? 60 | Chapter 3: Developing in AngularJS Figure 3-2. Model tree in Batarang Performance tab The performance tab must be enabled separately, as it injects some special JavaScript juice into your application. Once you enable it, you can look at various scopes and models, and evaluate the performance of all the watch expressions in each scope (as shown in Figure 3-3). The performance also gets updated as you use the app, so it works in real time as well! Other Awesome Tools | 61 Figure 3-3. Performance tab in Batarang Service dependencies For a simple application, you won’t have more than one or two dependencies for your controllers and services. But in a real, full-scale application, service dependency man‐ agement can become nightmarish without the proper tool support. Batarang is there for you, filling this very hole, as it gives you a clean, simple way of visualizing your service dependency chart (as shown in Figure 3-4). 62 | Chapter 3: Developing in AngularJS Figure 3-4. Charting dependencies in Batarang Elements properties and console access When you dig through the HTML template code of an AngularJS application, there is now an additional AngularJS Properties section in the Properties pane of the Elements tab. This allows you to inspect the models attached to a given element’s scope. It also exposes the scope of the element to the console, so that you can access it through the $scope variable in the console. This is shown in Figure 3-5. Other Awesome Tools | 63 Figure 3-5. AngularJS properties within Batarang Yeoman: Optimizing Your Workflow There are quite a few tools that have sprung up to help optimize your workflow when developing web applications. Yeoman, which we touched upon in previous sections, is one such tool that boasts an impressive set of features, including: • Lightning-fast scaffolding • Built-in preview server • Integrated package management • An awesome build process • Unit testing using PhantomJS It also integrates nicely and extensively with AngularJS, which is one of the foremost reasons why we strongly recommend using it for any AngularJS project. Let’s walk through the various ways that Yeoman makes your life easier: 64 | Chapter 3: Developing in AngularJS Installing Yeoman Installing Yeoman is quite an involved process, but there are scripts to help you through it. On a Mac/Linux machine, run the following command: curl -L get.yeoman.io | bash and just follow the instructions it prints to get Yeoman. For Windows, or if you run into any issues, go to https://github.com/yeoman/yeoman/ wiki/Manual-Install and follow the instructions there to get you unblocked. Starting a Fresh AngularJS project As previously mentioned, even a simple AngularJS project has quite a bit of seeding that needs to be done, from the templates, the basic controllers, and the library dependencies, to everything else that needs to be structured. You could do it yourself manually, or use Yeoman to do it for you. Simply create a folder for your project (the name of the folder will be taken as the project name by Yeoman), and then run: yeoman init angular This will create the entire structure detailed in the Project Organization part of this chapter for you, including the skeletons for rendering your routes, your unit tests, and more. Running Your Server If you don’t use Yeoman, you will have to create an HTTP server that serves your front- end code. But with Yeoman, you get a built-in server that is pre-configured and has some nice added benefits. You can start the server using: yeoman server This not only starts a web server that serves your code, but it also automatically opens your web browser and refreshes your browser when you make changes to your appli‐ cation. Adding New Routes, Views, and Controllers Adding a new route to Angular involves multiple steps, including: • Sourcing the New Controller JS file in the index.html • Adding the correct route to the AngularJS module Yeoman: Optimizing Your Workflow | 65 • Creating the template HTML • Adding unit tests All of this can be accomplished in a single step in Yeoman with the following command: yeoman init angular:route routeName So if you ended up running yeoman init angular:route home, it would: • Create a home.js controller skeleton in the app/scripts/controllers folder • Create a home.js test spec skeleton in the test/specs/controllers folder • Add the home.html template to the app/views folder • Hook up the home route in the main app module (app/scripts/app.js file) All of this from a single command! The Testing Story We’ve already seen how ridiculously easy it is to start and run tests using Karma. In the end, just two commands were needed to run all your unit tests. Yeoman makes it easier (if you can believe it). Anytime you generate a file using Yeoman, it also creates a testing stub for you to fill out. Once you’ve installed Karma, running tests with Yeoman is as simple as executing the following command: yeoman test Building Your Project Building the production-ready version of your app can be a pain, or at least involve many steps. Yeoman alleviates some of this by allowing you to: • Concatenate all your JS Scripts into one file • Version your files • Optimize images • Generate Application Cache manifests All these benefits come from just one command: yeoman build Yeoman does not support minification yet, but it is coming soon, according to the developers. 66 | Chapter 3: Developing in AngularJS Integrating AngularJS with RequireJS Getting your development environment just right is much easier if you get more done early. Modifying your development environment at a later stage will require modifica‐ tions to a larger number of files. Dependency management and creating deployment packages are top worries for any sizable project. With JavaScript, setting up your development environment used to be quite difficult, as it involved maintaining Ant builds, building scripts to concatenate your files, mini‐ fying them, and more. Thankfully, in the recent past, tools like RequireJS have emerged, which allow you to define and manage your JS dependencies, as well as hook them into a simpler build process. With these asynchronous load-management tools, which en‐ sure that all dependencies are loaded before the code is executed, focusing on developing the actual features has never been easier. Thankfully, AngularJS can and does play nice with RequireJS, so you can have the best of both worlds. For the purpose of this example, we will provide a sample setup that we have found to work nicely, and in a systematic, easy-to-follow way. Let us take a look at the project organization (similar to the skeletons previously de‐ scribed, with minor changes): 1. app: This folder hosts all the app code that is displayed to the user. This includes HTML, JS, CSS, images, and dependent libraries. a. /styles: Con tains all the CSS/LESS files b. /images: Contains images for our project c. /scripts: The main AngularJS codebase. This folder also includes our bootstrap‐ ping code, and the main integration with RequireJS i. /controllers: AngularJS controllers go here ii. /directives: AngularJS Directives go here iii. /filters: AngularJS filters go here iv. /services: AngularJS services go here d. /vendor: The libraries we depend on (Bootstrap, RequireJS, jQuery) e. /views: The HTML partials for the views and the components used in our project 2. config: Con tains Karma configs for unit and scenario tests 3. test: Con tains the unit and scenario (integration) tests for the app a. /spec: Con tains the unit tests, mirroring the structure of the JS folder in the app directory b. /e2e: Con tains the end-to-end scenario specs Integrating AngularJS with RequireJS | 67 The first thing we need is the main.js file (in the app folder) that RequireJS loads, which then triggers loading of all the other dependencies. In this example, our JS project will depend on jQuery and Twitter Bootstrap in addition to our code. // the app/scripts/main.js file, which defines our RequireJS config require.config({ paths: { angular: 'vendor/angular.min', jquery: 'vendor/jquery', domReady: 'vendor/require/domReady', twitter: 'vendor/bootstrap', angularResource: 'vendor/angular-resource.min', }, shim: { 'twitter/js/bootstrap': { deps: ['jquery/jquery'] }, angular: { deps: [ 'jquery/jquery', 'twitter/js/bootstrap'], exports: 'angular' }, angularResource: { deps:['angular'] } } }); require([ 'app', // Note this is not Twitter Bootstrap // but our AngularJS bootstrap 'bootstrap', 'controllers/mainControllers', 'services/searchServices', 'directives/ngbkFocus' // Any individual controller, service, directive or filter file // that you add will need to be pulled in here. // This will have to be maintained by hand. ], function (angular, app) { 'use strict'; app.config(['$routeProvider', function($routeProvider) { // Define your Routes here } ]); } ); We then define an app.js file. This defines our AngularJS app, and tells it that it depends on all the controllers, services, filters, and directives we define. We’ll look at the files that are mentioned in the RequireJS dependency list in just a bit. 68 | Chapter 3: Developing in AngularJS You can think of the RequireJS dependency list as a blocking import statement for JavaScript. That is, the function within the block will not execute until all the depen‐ dencies listed are satisfied or loaded. Also notice that we don’t individually tell RequireJS what directive, service, or filter to pull in, because that is not how this project is structured. There is one module each for controllers, services, filters, and directives, and thus it is sufficient to just define those as our dependencies. // The app/scripts/app.js file, which defines our AngularJS app define(['angular', 'angularResource', 'controllers/controllers', 'services/services', 'filters/filters', 'directives/directives'], function (angular) { return angular.module(‘MyApp’, ['ngResource', 'controllers', 'services', 'filters', 'directives']); }); We also have a bootstrap.js file, which waits for the DOM to be ready (using RequireJS’s plug-in, domReady), and then tells AngularJS to go forth and be awesome. // The app/scripts/bootstrap.js file which tells AngularJS // to go ahead and bootstrap when the DOM is loaded define(['angular', 'domReady'], function(angular, domReady) { domReady(function() { angular.bootstrap(document, [‘MyApp’]); }); }); There is another advantage to splitting the bootstrap from the app, which is that we could potentially replace our mainApp with a fake or a mockApp for the purpose of testing. For example, if the servers you depend on are flaky, you could just create a fakeApp that replaces all $http requests with fake data to allow you to develop in peace. That way, you can just slip in a fakeBootstrap and a fakeApp into your application. Now, your main index.html (which is in the app folder) could look something like: My AngularJS App Integrating AngularJS with RequireJS | 69
Now, we’ll take a look at the js/controllers/controllers.js file, which will look almost ex‐ actly the same as js/directives/directives.js, js/filters/filters.js, and js/services/services.js: define(['angular'], function(angular) { 'use strict'; return angular.module('controllers', []); }); Because of the way we have our RequireJS dependencies structured, all these are guar‐ anteed to run only after the Angular dependency has been satisfied and loaded. Each of these files defines an AngularJS module, which will then be used by the indi‐ vidual controllers, directives, filters, and services to add on to the definition. Let’s take a look at a directive definition (such as our focus directive from Chapter 2): // File: ngbkFocus.js define(['directives/directives'], function(directives) { directives.directive(ngbkFocus, ['$rootScope', function($rootScope) { return { restrict: 'A', scope: true, link: function(scope, element, attrs) { element[0].focus(); } }; }]); }); The directive itself is quite trivial, but let us take a closer look at what’s happening. The RequireJS shim around the file says that my ngbkFocus.js depends on the module dec‐ laration file directives/directives.js. It then uses the injected directives module to add on its own directive declaration. You could choose to have multiple directives, or a single one per file. It is completely up to you. One major note: if you have a controller that pulls in a service (say your RootControl ler depends on your UserService, and gets the UserService injected in), then you have to make sure that you define the file dependency to RequireJS as well, like so: define(['controllers/controllers', 'services/userService'], function(controllers) { controllers.controller('RootController', ['$scope', 'UserService', function($scope, UserService) { // Do what's needed 70 | Chapter 3: Developing in AngularJS }; }]); }); That is basically how your entire source folder structure is set up. But how does this affect my tests, you ask? We’re glad you asked that question, because you are going to get the answer now! The good news is that Karma does support RequireJS. Just install the latest and greatest version of Karma (using npm install -g karma). Once you have done that, the Karma config for the unit tests also changes slightly. The following is how we would set up the unit tests to run for the project structure we have previously defined: // This file is config/karma.conf.js. // Base path, that will be used to resolve files // (in this case is the root of the project) basePath = '../'; // list files/patterns to load in the browser files = [ JASMINE, JASMINE_ADAPTER, REQUIRE, REQUIRE_ADAPTER, // !! Put all libs in RequireJS 'paths' config here (included: false). // All these files are files that are needed for the tests to run, // but Karma is being told explicitly to avoid loading them, as they // will be loaded by RequireJS when the main module is loaded. {pattern: 'app/scripts/vendor/**/*.js', included: false}, // all the sources, tests // !! all src and test modules (included: false) {pattern: 'app/scripts/**/*.js', included: false}, {pattern: 'app/scripts/*.js', included: false}, {pattern: 'test/spec/*.js', included: false}, {pattern: 'test/spec/**/*.js', included: false}, // !! test main require module last 'test/spec/main.js' ]; // list of files to exclude exclude = []; // test results reporter to use // possible values: dots || progress reporter = 'progress'; // web server port Integrating AngularJS with RequireJS | 71 port = 8989; // cli runner port runnerPort = 9898; // enable/disable colors in the output (reporters and logs) colors = true; // level of logging logLevel = 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 // - PhantomJS // - IE if you have a windows box browsers = ['Chrome']; // Continuous Integration mode // if true, it captures browsers, runs tests, and exits singleRun = false; We use a slightly different format to define our dependencies (the included: false is quite important). We also add the dependency on REQUIRE_JS and its adapter. The final thing to get all this working is main.js, which triggers our tests. // This file is test/spec/main.js require.config({ // !! Karma serves files from '/base' // (in this case, it is the root of the project /your-project/app/js) baseUrl: '/base/app/scripts', paths: { angular: 'vendor/angular/angular.min', jquery: 'vendor/jquery', domReady: 'vendor/require/domReady', twitter: 'vendor/bootstrap', angularMocks: 'vendor/angular-mocks', angularResource: 'vendor/angular-resource.min', unitTest: '../../../base/test/spec' }, // example of using shim, to load non-AMD libraries // (such as Backbone, jQuery) shim: { angular: { exports: 'angular' 72 | Chapter 3: Developing in AngularJS }, angularResource: { deps:['angular']}, angularMocks: { deps:['angularResource']} } }); // Start karma once the dom is ready. require([ 'domReady', // Each individual test file will have to be added to this list to ensure // that it gets run. Again, this will have to be maintained manually. 'unitTest/controllers/mainControllersSpec', 'unitTest/directives/ngbkFocusSpec', 'unitTest/services/userServiceSpec' ], function(domReady) { domReady(function() { window.__karma__.start(); }); }); So with this setup, we can run the following: karma start config/karma.conf.js Then we can run the tests. Of course there is a slight change when it comes to writing your unit tests. They need to be RequireJS-supported modules as well, so let’s take a look at a sample test: // This is test/spec/directives/ngbkFocus.js define(['angularMocks', 'directives/directives', 'directives/ngbkFocus'], function() { describe('ngbkFocus Directive', function() { beforeEach(module('directives')); // These will be initialized before each spec (each it(), that is), // and reused var elem; beforeEach(inject(function($rootScope, $compile) { elem = $compile('')($rootScope); })); it('should have focus immediately', function() { expect(elem.hasClass('focus')).toBeTruthy(); }); }); }); Every test of ours will do the following: 1. Pull in angularMocks, which gets us angular, angularResource, and of course, angularMocks. Integrating AngularJS with RequireJS | 73 2. Pull in the high-level module (directives for directives, controllers for control‐ lers, and so on), then the individual file it is actually testing (the loadingIndicator). 3. If your test depends on some other service or controller, make sure you also define the RequireJS dependency, in addition to telling AngularJS about it. This kind of approach can be used with any test, and you should be good to go. Thankfully, the RequireJS approach doesn’t affect our end-to-end tests at all, so they can simply be done the way we have seen so far. A sample config follows, assuming that the server that runs your app is running on http://localhost:8000. // base path, that will be used to resolve files // (in this case is the root of the project basePath = '../'; // list of files / patterns to load in the browser files = [ ANGULAR_SCENARIO, ANGULAR_SCENARIO_ADAPTER, 'test/e2e/*.js' ]; // list of files to exclude exclude = []; // test results reporter to use // possible values: dots || progress reporter = 'progress'; // web server port port = 8989; // cli runner port runnerPort = 9898; // enable / disable colors in the output (reporters and logs) colors = true; // level of logging logLevel = LOG_INFO; // enable / disable watching file and executing tests whenever any file changes autoWatch = true; urlRoot = '/_karma_/'; proxies = { '/': 'http://localhost:8000/' }; // Start these browsers, currently available: 74 | Chapter 3: Developing in AngularJS browsers = ['Chrome']; // Continuous Integration mode // if true, it capture browsers, run tests and exit singleRun = false; Integrating AngularJS with RequireJS | 75 CHAPTER 4 Analyzing an AngularJS App We talked about some of the commonly used features of AngularJS in Chapter 2, and then dived into how your development should be structured in Chapter 3. Rather than continuing with similarly deep dives into individual features, Chapter 4 will look at a small, real-life application. We will get a feel for how all the pieces that we have been talking about (with toy examples) actually come together to form a real, working ap‐ plication. Rather than putting the entire application front and center, we will introduce one por‐ tion of it at a time, then talk about the interesting and relevant parts, slowly building up to the entire application by the end of this chapter. The Application GutHub is a simple recipe management application, which we designed both to store our super tasty recipes and to show off various pieces of an AngularJS application. The application: • has a two-column layout. • has a navigation bar on the left. • allows you to create a new recipe. • allows you to browse the list of existing recipes. The main view is on the right, which gets changed—depending on the URL—to either the list of recipes, the details of a single recipe, or an editable form to add to or edit existing recipes. We can see a screenshot of the application in Figure 4-1. 77 Figure 4-1. GutHub: A simple recipe management application This entire application is available on our GitHub repo in chapter4/guthub. Relationship Between Model, Controller, and Template Before we dive into the application, let us spend a paragraph or two talking about how the three pieces of our application work together, and how to think about each of them. The model is the truth. Just repeat that sentence a few times. Your entire application is driven off the model—what views are displayed, what to display in the views, what gets saved, everything! So spend some extra time thinking about your model, what the at‐ tributes of your object are going to be, and how you are going to retrieve it from the server and save it. The view will get updated automatically through the use of data bindings, so the focus should always be on the model. The controller holds the business logic: how you retrieve your model, what kinds of operations you perform on it, what kind of information your view needs from the model, and how you transform the model to get what you want. The responsibility of validation, making server calls, bootstrapping your view with the right data, and mostly everything in between belongs on your controller. Finally, the template represents how your model will be displayed, and how the user will interact with your application. It should mostly be restricted to the following: • Displaying your model • Defining the ways the user can interact with your application (clicks, input fields, and so on) 78 | Chapter 4: Analyzing an AngularJS App • Styling the app, and figuring out how and when some elements are displayed (show or hide, hover, and so on) • Filtering and formatting your data (both input and output) Realize that the template in Angular is not necessarily the view part of the Model View Controller design paradigm. Instead, the view is the compiled version of the template that gets executed. It is a combination of the template and the model. What should not go into the template is any kind of business logic or behavior; this information should be restricted to the controller. Keeping the template simple allows a proper separation of concerns, and also ensures that you can get the most code under test using only unit tests. Templates will have to be tested with scenario tests. But, you might ask, where does DOM manipulation go? DOM manipulation doesn’t really go into the controllers or the template. It goes into AngularJS directives (but can sometimes be used via services, which house DOM manipulation to avoid duplication of code). We’ll cover an example of that in our GutHub example as well. Without further ado, let’s dive right in. The Model We are going to keep the model dead simple for this application. There are recipes. They’re about the only model object in this entire application. Everything else builds off of it. Each recipe has the following properties: • An ID if it is persisted to our server • A name • A short description • Cooking instructions • Whether it is a featured recipe or not • An array of ingredients, each with an amount, a unit, and a name That’s it. Dead simple. Everything in the app is based around this simple model. Here’s a sample recipe for you to devour (the same one referenced in Figure 4-1): { "id": "1", "title": "Cookies", "description": "Delicious, crisp on the outside, chewy" + " on the outside, oozing with chocolatey goodness " + "cookies. The best kind", "ingredients": [ The Model | 79 { "amount": "1", "amountUnits": "packet", "ingredientName": "Chips Ahoy" } ], "instructions": "1. Go buy a packet of Chips Ahoy\n" + "2. Heat it up in an oven\n" + "3. Enjoy warm cookies\n" + "4. Learn how to bake cookies from somewhere else" } We will go on to see how more complicated UI features can be built around this simple model. Controllers, Directives, and Services, Oh My! Now we finally get to sink our teeth into the meat of this delicious application. First, we will look at the directives and services code and talk a little bit about what it is doing, then we’ll take a look at the multiple controllers needed for this application. Services // This file is app/scripts/services/services.js var services = angular.module('guthub.services', ['ngResource']); services.factory('Recipe', ['$resource', function($resource) { return $resource('/recipes/:id', {id: '@id'}); }]); services.factory('MultiRecipeLoader', ['Recipe', '$q', function(Recipe, $q) { return function() { var delay = $q.defer(); Recipe.query(function(recipes) { delay.resolve(recipes); }, function() { delay.reject('Unable to fetch recipes'); }); return delay.promise; }; }]); services.factory('RecipeLoader', ['Recipe', '$route', '$q', function(Recipe, $route, $q) { return function() { var delay = $q.defer(); Recipe.get({id: $route.current.params.recipeId}, function(recipe) { delay.resolve(recipe); 80 | Chapter 4: Analyzing an AngularJS App }, function() { delay.reject('Unable to fetch recipe ' + $route.current.params.recipeId); }); return delay.promise; }; }]); Let’s take a look at our services first. We touched upon services in “Organizing Depen‐ dencies with Modules” on page 33. Here, we’ll dig a little bit deeper. In this file, we instantiate three AngularJS services. There is a recipe service, which returns what we call an Angular Resource. These are RESTful resources, which point at a RESTful server. The Angular Resource encapsulates the lower level $http service, so that you can just deal with objects in your code. With just that single line of code—return $resource—(and of course, a dependency on the guthub.services module), we can now put recipe as an argument in any of our controllers, and it will be injected into the controller. Furthermore, each recipe object has the following methods built in: • Recipe.get() • Recipe.save() • Recipe.query() • Recipe.remove() • Recipe.delete() If you are going to use Recipe.delete, and want your application to work in IE, you will have to call it like so: Recipe[delete](). This is because delete is a keyword in IE. Of the the previous methods, all but query work with a single recipe; query() returns an array of recipes by default. The line of code that declares the resource—return $resource—also does a few more nice things for us: 1. Notice the :id in the URL specified for the RESTful resource. It basically says that when you make any query (say, Recipe.get()), if you pass in an object with an id field, then the value of that field will be added to the end of the URL. That is, calling Recipe.get({id: 15}) will make a call to /recipe/15. Controllers, Directives, and Services, Oh My! | 81 2. What about that second object? The {id: @id}? Well, as they say, a line of code is worth a thousand explanations, so let’s take a simple example. Say we have a recipe object, which has the necessary information already stored within it, including an id. Then, we can save it by simply doing the following: // Assuming existingRecipeObj has all the necessary fields, // including id (say 13) var recipe = new Recipe(existingRecipeObj); recipe.$save(); This will make a POST request to /recipe/13. The @id tells it to pick the id field from its object and use that as the id parameter. It’s an added convenience that can save a few lines of code. There are two other services in apps/scripts/services/services.js. Both of them are Load‐ ers; one loads a single recipe (RecipeLoader), and the other loads all recipes (MultiRe cipeLoader). These are used when we hook up our routes. At their cores, both of them behave very similarly. The flow of both these services is as follows: 1. Create a $q deferred object (these are AngularJS promises, used for chaining asyn‐ chronous functions). 2. Make a call to the server. 3. Resolve the deferred object when the server returns the value. 4. Return the promise that will be used by the routing mechanism of AngularJS. Promises in an AngularJS land A promise is an interface that deals with objects that are returned or get filled in at a future point in time (basically, asynchronous actions). At its core, a promise is an object with a then() function. To showcase the advantages, let us take an example where we need to fetch the current profile of a user: var currentProfile = null; var username = 'something'; fetchServerConfig(function(serverConfig) { fetchUserProfiles(serverConfig.USER_PROFILES, username, function(profiles) { currentProfile = profiles.currentProfile; 82 | Chapter 4: Analyzing an AngularJS App }); }); There are a few problems with this approach: 1. The resultant code is an indentation nightmare, especially if you have to chain multiple calls. 2. Errors reported in between callbacks and functions have a tendency to be lost, unless you handle them manually at each step. 3. You have to encapsulate the logic of what you want to do with currentProfile in the innermost callback, either directly, or through a separate function. Promises solve these issues. Before we go into the how, let’s take a look at the same problem implemented with promises: var currentProfile = fetchServerConfig().then(function(serverConfig) { return fetchUserProfiles(serverConfig.USER_PROFILES, username); }).then(function(profiles) { return profiles.currentProfile; }, function(error) { // Handle errors in either fetchServerConfig or // fetchUserProfiles here }); Notice the advantages: 1. You can chain function calls, so you don’t get into an indentation nightmare. 2. You are assured that the previous function call is finished before the next function in the chain is called. 3. Each then() call takes two arguments (both functions). The first one is the success callback and the second one is the error handler. 4. In case of errors in the chain, the error will get propagated through to the rest of the error handlers. So any error in any of the callbacks can be handled in the end. What about resolve and reject, you ask? Well, deferred in AngularJS is a way of creating promises. Calling resolve on it fulfills the promise (calls the success handler), while calling reject on it calls the error handler of the promise. We’ll come back to this again when we hook up our routes. Controllers, Directives, and Services, Oh My! | 83 Directives We can now move to the directives we will be using in our application. There will be two directives in the app: butterbar This directive will be shown and hidden when the routes change and while the page is still loading information. It will hook into the route-changing mechanism and automatically hide and show whatever is within its tag ,based on the state of the page. focus The focus directive is used to ensure that specific input fields (or elements) have the focus. Let’s look at the code: // This file is app/scripts/directives/directives.js var directives = angular.module('guthub.directives', []); directives.directive('butterbar', ['$rootScope', function($rootScope) { return { link: function(scope, element, attrs) { element.addClass('hide'); $rootScope.$on('$routeChangeStart', function() { element.removeClass('hide'); }); $rootScope.$on('$routeChangeSuccess', function() { element.addClass('hide'); }); } }; }]); directives.directive('focus', function() { return { link: function(scope, element, attrs) { element[0].focus(); } }; }); The preceding directive returns an object with a single property, link. We will dive deeper into how you can create your own directives in Chapter 6, but for now, all you need to know is the following: 84 | Chapter 4: Analyzing an AngularJS App 1. Directives go through a two-step process. In the first step (the compile phase), all directives attached to a DOM element are found, and then processed. Any DOM manipulation also happens during the compile step. At the end of this phase, a linking function is produced. 2. In the second step, the link phase (the phase we used previously), the preceding DOM template produced is linked to the scope. Also, any watchers or listeners are added as needed, resulting in a live binding between the scope and the element. Thus, anything related to the scope happens in the linking phase. So what’s happening in our directive? Let’s take a look, shall we? The butterbar directive can be used as follows:
My loading text...
It basically hides the element right up front, then adds two watches on the root scope. Every time a route change begins, it shows the element (by changing its class), and every time the route has successfully finished changing, it hides the butterbar again. Another interesting thing to note is how we inject the $rootScope into the directive. All directives directly hook into the AngularJS dependency injection system, so you can inject your services and whatever else you need into them. The final thing of note is the API for working with the element. jQuery veterans will be glad to know that it follows a jQuery-like syntax (addClass, removeClass). AngularJS implements a subset of the calls of jQuery so that jQuery is an optional dependency for any AngularJS project. In case you do end up using the full jQuery library in your project, you should know that AngularJS uses that instead of the jQlite implementation it has built-in. The second directive (focus) is much simpler. It just calls the focus() method on the current element. You can call it by adding the focus attribute on any input element, like so: When the page loads, that element immediately gets the focus. Controllers With directives and services covered, we can finally get into the controllers, of which we have five. All these controllers are located in a single file (app/scripts/controllers/ controllers.js), but we’ll go over them one at a time. Let’s go over the first controller, which is the List Controller, responsible for displaying the list of all recipes in the system. app.controller('ListCtrl', ['$scope', 'recipes', function($scope, recipes) { Controllers, Directives, and Services, Oh My! | 85 $scope.recipes = recipes; }]); Notice one very important thing with the List Controller: in the constructor, it does no work of going to the server and fetching the recipes. Instead, it is handed a list of recipes already fetched. You might wonder how that’s done. We’ll answer that in the routing section of the chapter, but it has to do with the MultiRecipeLoader service we saw previously. Just keep that in the back of your mind. With the List Controller under our belts, the other controllers are pretty similar in nature, but we will still cover them one by one to point out the interesting aspects: app.controller('ViewCtrl', ['$scope', '$location', 'recipe', function($scope, $location, recipe) { $scope.recipe = recipe; $scope.edit = function() { $location.path('/edit/' + recipe.id); }; }]); The interesting aspect about the View Controller is the edit function it exposes on the scope. Instead of showing and hiding fields or something similar, this controller relies on AngularJS to do the heavy lifting (as should you!). The edit function simply changes the URL to the edit equivalent for the recipe, and lo and behold, AngularJS does the rest. AngularJS recognizes that the URL has changed and loads the corresponding view (which is the same recipe in edit mode). Voila! Next, let’s take a look at the Edit Controller: app.controller('EditCtrl', ['$scope', '$location', 'recipe', function($scope, $location, recipe) { $scope.recipe = recipe; $scope.save = function() { $scope.recipe.$save(function(recipe) { $location.path('/view/' + recipe.id); }); }; $scope.remove = function() { delete $scope.recipe; $location.path('/'); }; }]); What’s new here are the save and remove methods that the Edit Controller exposes on the scope. The save function on the scope does what you would expect it to. It saves the current recipe, and once it is done saving, redirects the user to the view screen with the same 86 | Chapter 4: Analyzing an AngularJS App recipe. The callback function is useful in these scenarios to execute or perform some action once you are done. There are two ways we could have saved the recipe here. One is to do it as shown in the code, by executing $scope.recipe.$save(). This is only possible because recipe is a resource object that was returned by the RecipeLoader in the first place. Otherwise, the way you would save the recipe would be: Recipe.save(recipe); The remove function is also straightforward, in that it removes the recipe from the scope, and redirects users to the main landing page. Note that it doesn’t actually remove it from our server, though it shouldn’t be very hard to make that additional call. Next, we have the New Controller: app.controller('NewCtrl', ['$scope', '$location', 'Recipe', function($scope, $location, Recipe) { $scope.recipe = new Recipe({ ingredients: [ {} ] }); $scope.save = function() { $scope.recipe.$save(function(recipe) { $location.path('/view/' + recipe.id); }); }; }]); The New Controller is almost exactly the same as the Edit Controller. In fact, you could look at combining the two into a single controller as an exercise. The only major dif‐ ference is that the New Controller creates a new recipe (which is a resource, so that it has the save function) as the first step. Everything else remains unchanged. Finally, we have the Ingredients Controller. This is a special controller, but before we get into why or how, let’s take a look: app.controller('IngredientsCtrl', ['$scope', function($scope) { $scope.addIngredient = function() { var ingredients = $scope.recipe.ingredients; ingredients[ingredients.length] = {}; }; $scope.removeIngredient = function(index) { $scope.recipe.ingredients.splice(index, 1); }; }]); All the other controllers that we saw so far are linked to particular views on the UI. But the Ingredients Controller is special. It’s a child controller that is used on the edit pages to encapsulate certain functionality that is not needed at the higher level. The interesting Controllers, Directives, and Services, Oh My! | 87 thing to note is that since it is a child controller, it inherits the scope from the parent controller (the Edit/New controllers in this case). Thus, it has access to the $scope.recipe from the parent. The controller itself does nothing too interesting or unique. It just adds a new ingredient to the array of ingredients present on the recipe, or removes a specific ingredient from the list of ingredients on the recipe. With that, we finish the last of the controllers. The only JavaScript piece that remains is how the routing is set up: // This file is app/scripts/controllers/controllers.js var app = angular.module('guthub', ['guthub.directives', 'guthub.services']); app.config(['$routeProvider', function($routeProvider) { $routeProvider. when('/', { controller: 'ListCtrl', resolve: { recipes: function(MultiRecipeLoader) { return MultiRecipeLoader(); } }, templateUrl:'/views/list.html' }).when('/edit/:recipeId', { controller: 'EditCtrl', resolve: { recipe: function(RecipeLoader) { return RecipeLoader(); } }, templateUrl:'/views/recipeForm.html' }).when('/view/:recipeId', { controller: 'ViewCtrl', resolve: { recipe: function(RecipeLoader) { return RecipeLoader(); } }, templateUrl:'/views/viewRecipe.html' }).when('/new', { controller: 'NewCtrl', templateUrl:'/views/recipeForm.html' }).otherwise({redirectTo:'/'}); }]); As promised, we finally reached the point where the resolve functions are used. The previous piece of code sets up the Guthub AngularJS module, as well as the routes and templates involved in the application. 88 | Chapter 4: Analyzing an AngularJS App It hooks up the directives and the services that we created, and then specifies the various routes we will have in our application. For each route, we specify the URL, the controller that backs it up, the template to load, and finally (optionally), a resolve object. This resolve object tells AngularJS that each of these resolve keys needs to be satisfied before the route can be displayed to the user. For us, we want to load all the recipes, or an individual recipe, and make sure we have the server response before we display the page. So we tell the route provider that we have recipes (or a recipe), and then tell it how to fetch it. This links back to the two services we defined in the first section, the MultiRecipeLoad er and the RecipeLoader. If the resolve function returns an AngularJS promise, then AngularJS is smart enough to wait for the promise to get resolved before it proceeds. That means that it will wait until the server responds. The results are then passed into the constructor as arguments (with the names of the parameters being the object’s fields). Finally, the otherwise function denotes the default URL redirect that needs to happen when no routes are matched. You might notice that both the Edit and the New controller routes lead to the same template URL, views/recipeForm.html. What’s happening here? We reused the edit template. Depending on which controller is associated, different elements are shown in the edit recipe template. With this done, we can now move on to the templates, how these controllers hook up to them, and manage what is shown to the end user. The Templates Let us start by taking a look at the outermost, main template, which is the index.html. This is the base of our single-page application, and all the other views are loaded within the context of this template: GutHub - Create and Share The Templates | 89

GutHub

Loading...
There are five interesting elements to note in the preceding template, most of which you already encountered in Chapter 2. Let’s go over them one by one: ng-app We set the ng-app module to be GutHub. This is the same module name we gave in our angular.module function. This is how AngularJS knows to hook the two together. script tag This is where AngularJS is loaded for the application. It has to be done before all your JS files that use AngularJS are loaded. Ideally, this should be done at the bottom of the body. Butterbar Aha! Our first usage of a custom directive. When we defined our butterbar di‐ rective before, we wanted to use it on an element so that it would be shown when the routes were changing, and hidden on success. The highlighted element’s text is shown (a very boring “Loading…” in this case) as needed. Link href Values The hrefs link to the various pages of our single-page application. Notice how they use the # character to ensure that the page doesn’t reload, and are relative to the current page. AngularJS watches the URL (as long as the page isn’t reloaded), and 90 | Chapter 4: Analyzing an AngularJS App works it magic (or actually, the very boring route management we defined as part of our routes) when needed. ng-view This is where the last piece of magic happens. In our controllers section, we defined our routes. As part of that definition, we denoted the URL for each route, the con‐ troller associated with the route, and a template. When AngularJS detects a route change, it loads the template, attaches the controller to it, and replaces the ng- view with the contents of the template. One thing that is conspicuous in its absence is the ng-controller tag. Most applications would have some sort of a MainController associated with the outer template. Its most common location would be on the body tag. In this case, we didn’t use it, because the entire outer template has no AngularJS content that needs to refer to a scope. Now let’s look at the individual templates associated with each controller, starting with the “list of recipes” template:

Recipe List

Really, it’s a very boring template. There are only two points of interest here. The first one is a very standard usage of the ng-repeat tag. It picks up all the recipes from the scope, and repeats over them. The second is the usage of the ng-href tag instead of href. This is purely to avoid having a bad link during the time that AngularJS is loading up. The ng-href ensures that at no time is a malformed link presented to the user. Always use this whenever your URLs are dynamic instead of static. Of course you might wonder: where is the controller? There is no ng-controller defined, and there really was no Main Controller defined. This is where route mapping comes into play. If you remember (or peek back a few pages), the / route redirected to the list template and had the List Controller associated with it. Thus, when any references are made to variables and the like, it is within the scope of the List Controller. Now we move on to something with a little bit more meat: the view form.

{{recipe.title}}

{{recipe.description}}

Ingredients

The Templates | 91
  • {{ingredient.amount}} {{ingredient.amountUnits}} {{ingredient.ingredientName}}

Instructions

{{recipe.instructions}}
Another nice, small, contained template. We’ll draw your attention to three things, though not necessarily in the order they are shown! The first is the pretty standard ng-repeat. The recipes are again in the scope of the View Controller, which is loaded by the resolve function before this page is displayed to the user. This ensures that the page is not in a broken, unloaded state when the user sees it. The next interesting usage is that of ng-show and ng-class to style the template. The ng-show tag has been added to the tag, which is used to display a starred icon. Now, the starred icon is shown only when the recipe is a featured recipe (as denoted by the recipe.featured boolean value). Ideally, to ensure proper spacing, you would have another empty spacer icon, with an ng-hide directive on it, with the exact same Angu‐ larJS expression as shown in the ng-show. That is a very common usage, to display one thing and hide another on a given condition. The ng-class is used to add a class to the

tag (“featured” in this case) when the recipe is a featured recipe. That adds some special highlighting to make the title stand out even more. The final thing to note is the ng-submit directive on the form. The directive states that the edit() function on the scope is called in case the form is submitted. The form submission happens when any button without an explicit function attached (in this case, the Edit button) is clicked. Again, AngularJS is smart enough to figure out the scope that is being referred to (from the module, the route, and the controller) and call the right method at the right time. Now we can move on to our final template (and possibly the most complicated one yet), the recipe form template.

Edit Recipe

92 | Chapter 4: Analyzing an AngularJS App
The Templates | 93
Don’t panic. It looks like a lot of code, and it is a lot of code, but if you actually dig into it, it’s not very complicated. In fact, a lot of it is simple, repetitive boilerplate to show editable input fields for editing recipes: • The focus directive is added on the very first input field (the title input field). This ensures that when the user navigates to this page, the title field has focus so the user can immediately start typing in the title. • The ng-submit directive is used very similarly to the previous example, so we won’t dive into it much, other than to say that it saves the state of the recipe and signals the end of the editing process. It hooks up to the save() function in the Edit Con‐ troller. • The ng-model directive is used to bind the various input boxes and text areas on the field to the model. • One of the more interesting aspects on this page, and one we recommend you spend some time trying to understand, is the ng-controller tag on the ingredients list portion. Let’s take a minute to understand what is happening here. We see a list of ingredients being displayed, and the container tag is associated with an ng-controller. That means that the whole
还剩195页未读

继续阅读

pdf贡献者

pw8c

贡献于2015-06-24

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!