• No results found

AngularJS Tutorial

N/A
N/A
Protected

Academic year: 2021

Share "AngularJS Tutorial"

Copied!
41
0
0

Loading.... (view fulltext now)

Full text

(1)

You are now ready to build the AngularJS phonecat app. In this step, you will become familiar with the most important source code files, learn how to start the development servers bundled with angular-seed, and run the application in the browser.

• Git on Mac/Linux • Git on Windows

1. In angular-phonecat directory, run this command: 1. git checkout -f step-0

This resets your workspace to step 0 of the tutorial app.

You must repeat this for every future step in the tutorial and change the number to the number of the step you are on. This will cause any changes you made within your working directory to be lost.

2. To see the app running in a browser, do one of the following: • For node.js users:

1. In a separate terminal tab or window, run ./scripts/web-server.js to start the web server.

2. Open a browser window for the app and navigate to http://localhost:8000/app/index.html

For other http servers:

1. Configure the server to serve the files in the angular-phonecat directory. 2. Navigate in your browser to

http://localhost:[port-number]/[context-path]/app/index.html.

You can now see the page in your browser. It's not very exciting, but that's OK.

The HTML page that displays "Nothing here yet!" was constructed with the HTML code shown below. The code contains some key Angular elements that we will need going forward.

app/index.html:

1. <!doctype html>

2. <html lang="en" ng-app>

3. <head>

4. <meta charset="utf-8">

5. <title>My HTML File</title>

6. <link rel="stylesheet" href="css/app.css">

7. <link rel="stylesheet" href="css/bootstrap.css">

8. <script src="lib/angular/angular.js"></script>

9. </head>

10. <body>

11.

(2)

13. 14. </body>

15. </html>

What is the code doing?

• ng-app directive:

<html ng-app>

The ng-app attribute represents an Angular directive (named ngApp; Angular uses name-with-dashes for attribute names and camelCase for the corresponding directive name) used to flag an element which Angular should consider to be the root element of our application. This gives application developers the freedom to tell Angular if the entire html page or only a portion of it should be treated as the Angular application.

• AngularJS script tag:

<script src="lib/angular/angular.js">

This code downloads the angular.js script and registers a callback that will be executed by the browser when the containing HTML page is fully downloaded. When the callback is executed, Angular looks for the ngAppdirective. If Angular finds the directive, it will

bootstrap the application with the root of the application DOM being the element on which the ngApp directive was defined.

• Double-curly binding with an expression: Nothing here {{'yet' + '!'}}

This line demonstrates the core feature of Angular's templating capabilities – a binding, denoted by double-curlies{{ }} as well as a simple expression 'yet' + '!' used in this binding.

The binding tells Angular that it should evaluate an expression and insert the result into the DOM in place of the binding. Rather than a one-time insert, as we'll see in the next steps, a binding will result in efficient continuous updates whenever the result of the expression evaluation changes.

Angular expression is a JavaScript-like code snippet that is evaluated by Angular in the context of the current model scope, rather than within the scope of the global context (window).

As expected, once this template is processed by Angular, the html page contains the text: "Nothing here yet!".

Bootstrapping AngularJS apps

Bootstrapping AngularJS apps automatically using the ngApp directive is very easy and suitable for most cases. In advanced cases, such as when using script loaders, you can use imperative / manual way to bootstrap the app.

(3)

1. The injector that will be used for dependency injection within this app is created. 2. The injector will then create the root scope that will become the context for the model of

our application.

3. Angular will then "compile" the DOM starting at the ngApp root element, processing any directives and bindings found along the way.

Once an application is bootstrapped, it will then wait for incoming browser events (such as mouse click, key press or incoming HTTP response) that might change the model. Once such an event occurs, Angular detects if it caused any model changes and if changes are found, Angular will reflect them in the view by updating all of the affected bindings.

The structure of our application is currently very simple. The template contains just one directive and one static binding, and our model is empty. That will soon change!

What are all these files in my working directory?

Most of the files in your working directory come from the angular-seed project which is typically used to bootstrap new Angular projects. The seed project includes the latest Angular libraries, test libraries, scripts and a simple example app, all pre-configured for developing a typical web app.

For the purposes of this tutorial, we modified the angular-seed with the following changes:

• Removed the example app

• Added phone images to app/img/phones/ • Added phone data files (JSON) to app/phones/

(4)

• Added Bootstrap files to app/css/ and app/img/

Experiments

• Try adding a new expression to the index.html that will do some math: <p>1 + 2 = {{ 1 + 2 }}</p>

Summary

Now let's go to step 1 and add some content to the web app.

 

 

In order to illustrate how Angular enhances standard HTML, you will create a purely static HTML page and then examine how we can turn this HTML code into a template that Angular will use to dynamically display the same result with any set of data.

In this step you will add some basic information about two cell phones to an HTML page. Workspace Reset Instructions ➤

The page now contains a list with information about two phones.

The most important changes are listed below. You can see the full diff on GitHub: app/index.html:

1. <ul>

2. <li>

3. <span>Nexus S</span>

4. <p>

5. Fast just got faster with Nexus S. 6. </p>

7. </li>

8. <li>

9. <span>Motorola XOOM™ with Wi-Fi</span>

10. <p>

11. The Next, Next Generation tablet. 12. </p>

13. </li>

14. </ul>

Experiments

• Try adding more static HTML to index.html. For example: <p>Total number of phones: 2</p>

(5)

This addition to your app uses static HTML to display the list. Now, let's go to step 2 to learn how to use AngularJS to dynamically generate the same list.

 

Now it's time to make the web page dynamic — with AngularJS. We'll also add a test that verifies the code for the controller we are going to add.

There are many ways to structure the code for an application. For Angular apps, we encourage the use of the Model-View-Controller (MVC) design pattern to decouple the code and to separate concerns. With that in mind, let's use a little Angular and JavaScript to add model, view, and controller components to our app.

Workspace Reset Instructions ➤

The app now contains a list with three phones.

The most important changes are listed below. You can see the full diff on GitHub:

View and Template

In Angular, the view is a projection of the model through the HTML template. This means that whenever the model changes, Angular refreshes the appropriate binding points, which updates the view.

The view component is constructed by Angular from this template: app/index.html:

1. <html ng-app>

2. <head>

3. ...

4. <script src="lib/angular/angular.js"></script>

5. <script src="js/controllers.js"></script>

6. </head>

7. <body ng-controller="PhoneListCtrl">

8. 9. <ul>

10. <li ng-repeat="phone in phones">

11. {{phone.name}} 12. <p>{{phone.snippet}}</p> 13. </li> 14. </ul> 15. </body> 16. </html>

We replaced the hard-coded phone list with the ngRepeat directive and two Angular expressions enclosed in curly braces: {{phone.name}} and {{phone.snippet}}:

• The ng-repeat="phone in phones" statement in the <li> tag is an Angular repeater. The repeater tells Angular to create a <li> element for each phone in the list using the first <li> tag as the template.

(6)

• As we've learned in step 0, the curly braces

around phone.name and phone.snippet denote bindings. As opposed to evaluating constants, these expressions are referring to our application model, which was set up in ourPhoneListCtrl controller.

Model and Controller

The data model (a simple array of phones in object literal notation) is instantiated within the PhoneListCtrlcontroller:

app/js/controllers.js:

1. function PhoneListCtrl($scope) { 2. $scope.phones = [

3. {"name": "Nexus S",

4. "snippet": "Fast just got faster with Nexus S."}, 5. {"name": "Motorola XOOM™ with Wi-Fi",

(7)

7. {"name": "MOTOROLA XOOM™",

8. "snippet": "The Next, Next Generation tablet."} 9. ];

10. }

Although the controller is not yet doing very much controlling, it is playing a crucial role. By providing context for our data model, the controller allows us to establish data-binding between the model and the view. We connected the dots between the presentation, data, and logic components as follows:

• PhoneListCtrl — the name of our controller function (located in the JavaScript file controllers.js), matches the value of the ngController directive located on the <body> tag.

The phone data is then attached to the scope ($scope) that was injected into our controller function. The controller scope is a prototypical descendant of the root scope that was created when the application bootstrapped. This controller scope is available to all bindings located within the <body ng-controller="PhoneListCtrl"> tag.

The concept of a scope in Angular is crucial; a scope can be seen as the glue which allows the template, model and controller to work together. Angular uses scopes, along with the information contained in the template, data model, and controller, to keep models and views separate, but in sync. Any changes made to the model are reflected in the view; any

changes that occur in the view are reflected in the model.

To learn more about Angular scopes, see the angular scope documentation.

Tests

The "Angular way" makes it easy to test code as it is being developed. Take a look at the following unit test for your newly created controller:

test/unit/controllersSpec.js:

1. describe('PhoneCat controllers', function() { 2.

3. describe('PhoneListCtrl', function(){ 4.

5. it('should create "phones" model with 3 phones', function() { 6. var scope = {},

7. ctrl = new PhoneListCtrl(scope); 8.

9. expect(scope.phones.length).toBe(3); 10. });

11. }); 12. });

The test verifies that we have three records in the phones array and the example demonstrates how easy it is to create a unit test for code in Angular. Since testing is such a critical part of

(8)

software development, we make it easy to create tests in Angular so that developers are encouraged to write them.

Angular developers prefer the syntax of Jasmine's Behavior-driven Development (BDD)

framework when writing tests. Although Angular does not require you to use Jasmine, we wrote all of the tests in this tutorial in Jasmine. You can learn about Jasmine on the Jasmine home page and on the Jasmine wiki.

The angular-seed project is pre-configured to run all unit tests using Testacular. To run the test, do the following:

1. In a separate terminal window or tab, go to the angular-phonecat directory and run ./scripts/test.sh to start the Testacular server.

2. Testacular will start a new instance of Chrome browser automatically. Just ignore it and let it run in the background. Testacular will use this browser for test execution.

3. You should see the following or similar output in the terminal:

info: Testacular server started at http://localhost:9876/ info (launcher): Starting browser "Chrome" info (Chrome 22.0): Connected on socket id tPUm9DXcLHtZTKbAEO-n Chrome 22.0: Execu ted 1 of 1 SUCCESS (0.093 secs / 0.004 secs)

Yay! The test passed! Or not...

4. To rerun the tests, just change any of the source or test files. Testacular will notice the change and will rerun the tests for you. Now isn't that sweet?

Experiments

• Add another binding to index.html. For example:

<p>Total number of phones: {{phones.length}}</p>

• Create a new model property in the controller and bind to it from the template. For example: $scope.hello = "Hello, World!"

Refresh your browser to make sure it says, "Hello, World!" • Create a repeater that constructs a simple table:

<table> <tr><th>row number</th></tr> <tr ng-repeat="i in [ 0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}</td></tr> </table>

Now, make the list 1-based by incrementing i by one in the binding:

<table> <tr><th>row number</th></tr> <tr ng-repeat="i in [ 0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr> </table>

• Make the unit test fail by changing the toBe(3) statement to toBe(4).

(9)

You now have a dynamic app that features separate model, view, and controller components, and you are testing as you go. Now, let's go to step 3 to learn how to add full text search to the app.

 

 

We did a lot of work in laying a foundation for the app in the last step, so now we'll do

something simple; we will add full text search (yes, it will be simple!). We will also write an end-to-end test, because a good end-end-to-end test is a good friend. It stays with your app, keeps an eye on it, and quickly detects regressions.

Workspace Reset Instructions ➤

The app now has a search box. Notice that the phone list on the page changes depending on what a user types into the search box.

The most important differences between Steps 2 and 3 are listed below. You can see the full diff on GitHub:

Controller

We made no changes to the controller.

Template

app/index.html:

1. <div class="container-fluid">

2. <div class="row-fluid">

3. <div class="span2">

4. <!--Sidebar content--> 5.

6. Search: <input ng-model="query">

7.

8. </div>

9. <div class="span10">

10. <!--Body content--> 11.

12. <ul class="phones">

13. <li ng-repeat="phone in phones | filter:query">

14. {{phone.name}} 15. <p>{{phone.snippet}}</p> 16. </li> 17. </ul> 18. 19. </div> 20. </div> 21. </div>

(10)

We added a standard HTML <input> tag and used Angular's $filter function to process the input for thengRepeat directive.

This lets a user enter search criteria and immediately see the effects of their search on the phone list. This new code demonstrates the following:

• Data-binding: This is one of the core features in Angular. When the page loads, Angular binds the name of the input box to a variable of the same name in the data model and keeps the two in sync.

In this code, the data that a user types into the input box (named query) is immediately available as a filter input in the list repeater (phone in phones | filter:query). When changes to the data model cause the repeater's input to change, the repeater efficiently updates the DOM to reflect the current state of the model.

• Use of the filter filter: The filter function uses the query value to create a new array that contains only those records that match the query.

(11)

ngRepeat automatically updates the view in response to the changing number of phones returned by the filterfilter. The process is completely transparent to the developer.

Test

In Step 2, we learned how to write and run unit tests. Unit tests are perfect for testing controllers and other components of our application written in JavaScript, but they can't easily test DOM manipulation or the wiring of our application. For these, an end-to-end test is a much better choice.

The search feature was fully implemented via templates and data-binding, so we'll write our first end-to-end test, to verify that the feature works.

test/e2e/scenarios.js:

1. describe('PhoneCat App', function() { 2.

3. describe('Phone list view', function() { 4. 5. beforeEach(function() { 6. browser().navigateTo('../../app/index.html'); 7. }); 8. 9.

10. it('should filter the phone list as user types into the search box', fu nction() {

11. expect(repeater('.phones li').count()).toBe(3); 12.

13. input('query').enter('nexus');

14. expect(repeater('.phones li').count()).toBe(1); 15.

16. input('query').enter('motorola');

17. expect(repeater('.phones li').count()).toBe(2); 18. });

19. }); 20. });

Even though the syntax of this test looks very much like our controller unit test written with Jasmine, the end-to-end test uses APIs of Angular's end-to-end test runner.

To run the end-to-end test, open one of the following in a new browser tab:

node.js users:

http://localhost:8000/test/e2e/runner.html

users with other http servers:

http://localhost:[port-number]/[context-path]/test/e2e/runner.html

casual reader:

http://angular.github.com/angular-phonecat/step-3/test/e2e/runner.html

(12)

Previously we've seen how Testacular can be used to execute unit tests. Well, it can also run the end-to-end tests! Use./scripts/e2e-test.sh script for that. End-to-end tests are slow, so unlike with unit tests, Testacular will exit after the test run and will not automatically rerun the test suite on every file change. To rerun the test suite, execute thee2e-test.sh script again. This test verifies that the search box and the repeater are correctly wired together. Notice how easy it is to write end-to-end tests in Angular. Although this example is for a simple test, it really is that easy to set up any functional, readable, end-to-end test.

Experiments

• Display the current value of the query model by adding a {{query}} binding into the index.html template, and see how it changes when you type in the input box.

• Let's see how we can get the current value of the query model to appear in the HTML page title.

You might think you could just add the to the title tag element as follows: <title>Google Phone Gallery: {{query}}</title>

However, when you reload the page, you won't see the expected result. This is because the "query" model lives in the scope defined by the body element:

<body ng-controller="PhoneListCtrl">

If you want to bind to the query model from the <title> element, you

must move the ngController declaration to the HTML element because it is the common parent of both the body and title elements:

<html ng-app ng-controller="PhoneListCtrl">

Be sure to remove the ng-controller declaration from the body element.

While using double curlies works fine within the title element, you might have noticed that for a split second they are actually displayed to the user while the page is loading. A better solution would be to use the ngBind orngBindTemplate directives, which are invisible to the user while the page is loading:

<title ng-bind-template="Google Phone Gallery: {{query}}">Google P hone Gallery</title>

• Add the following end-to-end test into the describe block within test/e2e/scenarios.js:

1. it('should display the current filter value within an element with id " status"',

2. function() {

3. expect(element('#status').text()).toMatch(/Current filter: \s*$/); 4.

5. input('query').enter('nexus'); 6.

(13)

7. expect(element('#status').text()).toMatch(/Current filter: nexus\s*$/

); 8.

9. //alternative version of the last assertion that tests just the value of the binding

10. using('#status').expect(binding('query')).toBe('nexus'); 11. });

Refresh the browser tab with the end-to-end test runner to see the test fail. To make the test pass, edit theindex.html template to add a div or p element with id "status" and content with the query binding, prefixed by "Current filter:". For instance:

<div id="status">Current filter: {{query}}</div>

• Add a pause() statement inside of an end-to-end test and rerun it. You'll see the runner pause; this gives you the opportunity to explore the state of your application while it is displayed in the browser. The app is live! You can change the search query to prove it. Notice how useful this is for troubleshooting end-to-end tests.

Summary

We have now added full text search and included a test to verify that search works! Now let's go on to step 4 to learn how to add sorting capability to the phone app.

 

 

In this step, you will add a feature to let your users control the order of the items in the phone list. The dynamic ordering is implemented by creating a new model property, wiring it together with the repeater, and letting the data binding magic do the rest of the work.

Workspace Reset Instructions ➤

You should see that in addition to the search box, the app displays a drop down menu that allows users to control the order in which the phones are listed.

The most important differences between Steps 3 and 4 are listed below. You can see the full diff on GitHub:

Template

app/index.html:

1. Search: <input ng-model="query"> 2. Sort by:

3. <select ng-model="orderProp">

4. <option value="name">Alphabetical</option>

5. <option value="age">Newest</option> 6. </select>

(14)

8.

9. <ul class="phones">

10. <li ng-repeat="phone in phones | filter:query | orderBy:orderProp">

11. {{phone.name}}

12. <p>{{phone.snippet}}</p> 13. </li>

14. </ul>

We made the following changes to the index.html template:

• First, we added a <select> html element named orderProp, so that our users can pick from the two provided sorting options.

(15)

• We then chained the filter filter with orderBy filter to further process the input into the repeater. orderBy is a filter that takes an input array, copies it and reorders the copy which is then returned.

Angular creates a two way data-binding between the select element and

the orderProp model. orderProp is then used as the input for the orderBy filter.

As we discussed in the section about data-binding and the repeater in step 3, whenever the model changes (for example because a user changes the order with the select drop down menu), Angular's data-binding will cause the view to automatically update. No bloated DOM manipulation code is necessary!

Controller

app/js/controllers.js:

1. function PhoneListCtrl($scope) { 2. $scope.phones = [

3. {"name": "Nexus S",

4. "snippet": "Fast just got faster with Nexus S.", 5. "age": 0},

6. {"name": "Motorola XOOM™ with Wi-Fi",

7. "snippet": "The Next, Next Generation tablet.", 8. "age": 1},

9. {"name": "MOTOROLA XOOM™",

10. "snippet": "The Next, Next Generation tablet.", 11. "age": 2}

12. ]; 13.

14. $scope.orderProp = 'age'; 15. }

• We modified the phones model - the array of phones - and added an age property to each phone record. This property is used to order phones by age.

• We added a line to the controller that sets the default value of orderProp to age. If we had not set the default value here, the model would stay uninitialized until our user would pick an option from the drop down menu.

This is a good time to talk about two-way data-binding. Notice that when the app is loaded in the browser, "Newest" is selected in the drop down menu. This is because we

set orderProp to 'age' in the controller. So the binding works in the direction from our model to the UI. Now if you select "Alphabetically" in the drop down menu, the model will be updated as well and the phones will be reordered. That is the data-binding doing its job in the opposite direction — from the UI to the model.

Test

The changes we made should be verified with both a unit test and an end-to-end test. Let's look at the unit test first.

(16)

test/unit/controllersSpec.js:

1. describe('PhoneCat controllers', function() { 2.

3. describe('PhoneListCtrl', function(){ 4. var scope, ctrl;

5.

6. beforeEach(function() { 7. scope = {},

8. ctrl = new PhoneListCtrl(scope); 9. });

10. 11.

12. it('should create "phones" model with 3 phones', function() { 13. expect(scope.phones.length).toBe(3);

14. }); 15. 16.

17. it('should set the default value of orderProp model', function() { 18. expect(scope.orderProp).toBe('age');

19. }); 20. }); 21. });

The unit test now verifies that the default ordering property is set.

We used Jasmine's API to extract the controller construction into a beforeEach block, which is shared by all tests in the parent describe block.

You should now see the following output in the Testacular tab:

Chrome 22.0: Executed 2 of 2 SUCCESS (0.021 secs / 0.001 secs) Let's turn our attention to the end-to-end test.

test/e2e/scenarios.js:

1. ...

2. it('should be possible to control phone order via the drop down select box',

3. function() {

4. //let's narrow the dataset to make the test assertions shorter 5. input('query').enter('tablet');

6.

7. expect(repeater('.phones li', 'Phone List').column('phone.name')). 8. toEqual(["Motorola XOOM\u2122 with Wi-Fi",

9. "MOTOROLA XOOM\u2122"]); 10.

(17)

11. select('orderProp').option('Alphabetical'); 12.

13. expect(repeater('.phones li', 'Phone List').column('phone.name')). 14. toEqual(["MOTOROLA XOOM\u2122",

15. "Motorola XOOM\u2122 with Wi-Fi"]); 16. });

17. ...

The end-to-end test verifies that the ordering mechanism of the select box is working correctly. You can now rerun ./scripts/e2e-test.sh or refresh the browser tab with the end-to-end test runner.html to see the tests run, or you can see them running on Angular's server.

Experiments

• In the PhoneListCtrl controller, remove the statement that sets the orderProp value and you'll see that Angular will temporarily add a new "unknown" option to the drop-down list and the ordering will default to unordered/natural order.

• Add an {{orderProp}} binding into the index.html template to display its current value as text.

Summary

Now that you have added list sorting and tested the app, go to step 5 to learn about Angular services and how Angular uses dependency injection.

 

 

Enough of building an app with three phones in a hard-coded dataset! Let's fetch a larger dataset from our server using one of angular's built-in services called $http. We will use angular's dependency injection (DI) to provide the service to the PhoneListCtrl controller. Workspace Reset Instructions ➤

You should now see a list of 20 phones.

The most important changes are listed below. You can see the full diff on GitHub:

Data

The app/phones/phones.json file in your project is a dataset that contains a larger list of phones stored in the JSON format.

Following is a sample of the file:

1. [ 2. {

3. "age": 13,

(18)

5. "name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",

6. "snippet": "Are you ready for everything life throws your way?"

7. ... 8. }, 9. ... 10. ]

Controller

We'll use angular's $http service in our controller to make an HTTP request to your web server to fetch the data in theapp/phones/phones.json file. $http is just one of several built-in angular services that handle common operations in web apps. Angular injects these services for you where you need them.

Services are managed by angular's DI subsystem. Dependency injection helps to make your web apps both well-structured (e.g., separate components for presentation, data, and control) and loosely coupled (dependencies between components are not resolved by the components themselves, but by the DI subsystem).

app/js/controllers.js:

1. function PhoneListCtrl($scope, $http) {

2. $http.get('phones/phones.json').success(function(data) { 3. $scope.phones = data; 4. }); 5. 6. $scope.orderProp = 'age'; 7. } 8. 9. //PhoneListCtrl.$inject = ['$scope', '$http'];

$http makes an HTTP GET request to our web server, asking for phone/phones.json (the url is relative to ourindex.html file). The server responds by providing the data in the json file. (The response might just as well have been dynamically generated by a backend server. To the browser and our app they both look the same. For the sake of simplicity we used a json file in this tutorial.)

The $http service returns a promise object with a success method. We call this method to handle the asynchronous response and assign the phone data to the scope controlled by this controller, as a model calledphones. Notice that angular detected the json response and parsed it for us!

To use a service in angular, you simply declare the names of the dependencies you need as arguments to the controller's constructor function, as follows:

(19)

Angular's dependency injector provides services to your controller when the controller is being constructed. The dependency injector also takes care of creating any transitive dependencies the service may have (services often depend upon other services).

Note that the names of arguments are significant, because the injector uses these to look up the dependencies.

'$' Prefix Naming Convention

You can create your own services, and in fact we will do exactly that in step 11. As a naming convention, angular's built-in services, Scope methods and a few other angular APIs have a '$' prefix in front of the name. Don't use a '$' prefix when naming your services and models, in order to avoid any possible naming collisions.

A Note on Minification

Since angular infers the controller's dependencies from the names of arguments to the controller's constructor function, if you were to minify the JavaScript code

for PhoneListCtrl controller, all of its function arguments would be minified as well, and the dependency injector would not be able to identify services correctly.

To overcome issues caused by minification, just assign an array with service identifier strings into the $injectproperty of the controller function, just like the last line in the snippet (commented out) suggests:

(20)

PhoneListCtrl.$inject = ['$scope', '$http'];

There is also one more way to specify this dependency list and avoid minification issues — using the bracket notation which wraps the function to be injected into an array of strings (representing the dependency names) followed by the function to be injected:

var PhoneListCtrl = ['$scope', '$http', function($scope, $http) { /* co nstructor body */ }];

Both of these methods work with any function that can be injected by Angular, so it's up to your project's style guide to decide which one you use.

Test

test/unit/controllersSpec.js:

Because we started using dependency injection and our controller has dependencies,

constructing the controller in our tests is a bit more complicated. We could use the new operator and provide the constructor with some kind of fake$http implementation. However, the

recommended (and easier) way is to create a controller in the test environment in the same way that angular does it in the production code behind the scenes, as follows:

1. describe('PhoneCat controllers', function() { 2.

3. describe('PhoneListCtrl', function(){ 4. var scope, ctrl, $httpBackend; 5.

6. beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) { 7. $httpBackend = _$httpBackend_;

8. $httpBackend.expectGET('phones/phones.json').

9. respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]); 10.

11. scope = $rootScope.$new();

12. ctrl = $controller(PhoneListCtrl, {$scope: scope}); 13. }));

Note: Because we loaded Jasmine and angular-mocks.js in our test environment, we got two helper methodsmodule and inject that we'll use to access and configure the injector. We created the controller in the test environment, as follows:

• We used the inject helper method to inject instances

of $rootScope, $controller and $httpBackendservices into the

Jasmine's beforeEach function. These instances come from an injector which is recreated from scratch for every single test. This guarantees that each test starts from a well known starting point and each test is isolated from the work done in other tests.

• We created a new scope for our controller by calling $rootScope.$new()

• We called the injected $controller function passing the PhoneListCtrl function and the created scope as parameters.

(21)

Because our code now uses the $http service to fetch the phone list data in our controller, before we create thePhoneListCtrl child scope, we need to tell the testing harness to expect an incoming request from the controller. To do this we:

• Request $httpBackend service to be injected into our beforeEach function. This is a mock version of the service that in a production environment facilitates all XHR and JSONP requests. The mock version of this service allows you to write tests without having to deal with native APIs and the global state associated with them — both of which make testing a nightmare.

• Use the $httpBackend.expectGET method to train the $httpBackend service to expect an incoming HTTP request and tell it what to respond with. Note that the responses are not returned until we call the$httpBackend.flush method.

Now, we will make assertions to verify that the phones model doesn't exist on scope before the response is received:

1. it('should create "phones" model with 2 phones fetched from xhr', function( ) {

2. expect(scope.phones).toBeUndefined(); 3. $httpBackend.flush();

4.

5. expect(scope.phones).toEqual([{name: 'Nexus S'},

6. {name: 'Motorola DROID'}]); 7. });

• We flush the request queue in the browser by calling $httpBackend.flush(). This causes the promise returned by the $http service to be resolved with the trained response. • We make the assertions, verifying that the phone model now exists on the scope.

Finally, we verify that the default value of orderProp is set correctly:

1. it('should set the default value of orderProp model', function() { 2. expect(scope.orderProp).toBe('age');

3. }); 4. ; 5.

You should now see the following output in the Testacular tab:

Chrome 22.0: Executed 2 of 2 SUCCESS (0.028 secs / 0.007 secs)

Experiments

• At the bottom of index.html, add a {{phones | json}} binding to see the list of phones displayed in json format.

(22)

• In the PhoneListCtrl controller, pre-process the http response by limiting the number of phones to the first 5 in the list. Use the following code in the $http callback:

$scope.phones = data.splice(0, 5);

Summary

Now that you have learned how easy it is to use angular services (thanks to Angular's

dependency injection), go to step 6, where you will add some thumbnail images of phones and some links.

 

 

In this step, you will add thumbnail images for the phones in the phone list, and links that, for now, will go nowhere. In subsequent steps you will use the links to display additional information about the phones in the catalog.

Workspace Reset Instructions ➤

You should now see links and images of the phones in the list.

The most important changes are listed below. You can see the full diff on GitHub:

Data

Note that the phones.json file contains unique ids and image urls for each of the phones. The urls point to theapp/img/phones/ directory.

app/phones/phones.json (sample snippet):

1. [ 2. { 3. ...

4. "id": "motorola-defy-with-motoblur",

5. "imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg", 6. "name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",

7. ... 8. }, 9. ... 10. ]

Template

app/index.html: 1. ...

2. <ul class="phones">

3. <li ng-repeat="phone in phones | filter:query | orderBy:orderProp " class="thumbnail">

(23)

4. <a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{ph one.imageUrl}}"></a>

5. <a href="#/phones/{{phone.id}}">{{phone.name}}</a> 6. <p>{{phone.snippet}}</p>

7. </li>

8. </ul> 9. ...

To dynamically generate links that will in the future lead to phone detail pages, we used the now-familiar double-curly brace binding in the href attribute values. In step 2, we added the {{phone.name}} binding as the element content. In this step the {{phone.id}} binding is used in the element attribute.

We also added phone images next to each record using an image tag with the ngSrc directive. That directive prevents the browser from treating the angular {{ expression }} markup literally, and initiating a request to invalid

urlhttp://localhost:8000/app/{{phone.imageUrl}}, which it would have done if we had only specified an attribute binding in a regular src attribute (<img class="diagram" src="{{phone.imageUrl}}">). Using thengSrc directive prevents the browser from making an http request to an invalid location.

Test

test/e2e/scenarios.js:

1. ...

2. it('should render phone specific links', function() { 3. input('query').enter('nexus');

4. element('.phones li a').click();

5. expect(browser().location().url()).toBe('/phones/nexus-s'); 6. });

7. ...

We added a new end-to-end test to verify that the app is generating correct links to the phone views that we will implement in the upcoming steps.

You can now rerun ./scripts/e2e-test.sh or refresh the browser tab with the end-to-end test runner to see the tests run, or you can see them running on Angular's server.

Experiments

• Replace the ng-src directive with a plain old src attribute. Using tools such as Firebug, or Chrome's Web Inspector, or inspecting the webserver access logs, confirm that the app is indeed making an extraneous request

(24)

The issue here is that the browser will fire a request for that invalid image address as soon as it hits the img tag, which is before Angular has a chance to evaluate the expression and inject the valid address.

Summary

Now that you have added phone images and links, go to step 7 to learn about Angular layout templates and how Angular makes it easy to create applications that have multiple views.

 

 

In this step, you will learn how to create a layout template and how to build an app that has multiple views by adding routing.

Workspace Reset Instructions ➤

Note that when you now navigate to app/index.html, you are redirected

to app/index.html#/phones and the same phone list appears in the browser. When you click on a phone link the stub of a phone detail page is displayed.

The most important changes are listed below. You can see the full diff on GitHub.

Multiple Views, Routing and Layout Template

Our app is slowly growing and becoming more complex. Before step 7, the app provided our users with a single view (the list of all phones), and all of the template code was located in the index.html file. The next step in building the app is to add a view that will show detailed information about each of the devices in our list.

To add the detailed view, we could expand the index.html file to contain template code for both views, but that would get messy very quickly. Instead, we are going to turn

the index.html template into what we call a "layout template". This is a template that is common for all views in our application. Other "partial templates" are then included into this layout template depending on the current "route" — the view that is currently displayed to the user.

Application routes in Angular are declared via the $routeProvider, which is the provider of the $route service. This service makes it easy to wire together controllers, view templates, and the current URL location in the browser. Using this feature we can implement deep linking, which lets us utilize the browser's history (back and forward navigation) and bookmarks. A Note About DI, Injector and Providers

As you noticed, dependency injection (DI) is the core feature of AngularJS, so it's important for you to understand a thing or two about how it works.

When the application bootstraps, Angular creates an injector that will be used for all DI stuff in this app. The injector itself doesn't know anything about what $http or $route services do, in fact it doesn't even know about the existence of these services unless it is configured with proper module definitions. The sole responsibilities of the injector are to load specified module definition(s), register all service providers defined in these modules and when asked inject a specified function with dependencies (services) that it lazily instantiates via their providers.

(25)

Providers are objects that provide (create) instances of services and expose configuration APIs that can be used to control the creation and runtime behavior of a service. In case of

the $route service, the $routeProvider exposes APIs that allow you to define routes for your application.

Angular modules solve the problem of removing global state from the application and provide a way of configuring the injector. As opposed to AMD or require.js modules, Angular modules don't try to solve the problem of script load ordering or lazy script fetching. These goals are orthogonal and both module systems can live side by side and fulfil their goals.

The App Module

app/js/app.js:

1. angular.module('phonecat', []).

2. config(['$routeProvider', function($routeProvider) { 3. $routeProvider.

4. when('/phones', {templateUrl: 'partials/phone-list.html', controlle r: PhoneListCtrl}).

5. when('/phones/:phoneId', {templateUrl: 'partials/phone-detail.html', controller: PhoneDetailCtrl}).

6. otherwise({redirectTo: '/phones'}); 7. }]);

In order to configure our application with routes, we need to create a module for our application. We call this modulephonecat and using the config API we request the $routeProvider to be injected into our config function and use$routeProvider.when API to define our routes. Note that during the injector configuration phase, the providers can be injected as well, but they will not be available for injection once the injector is created and starts creating service

instances.

Our application routes were defined as follows:

• The phone list view will be shown when the URL hash fragment is /phones. To construct this view, Angular will use the phone-list.html template and

the PhoneListCtrl controller.

• The phone details view will be shown when the URL hash fragment matches

'/phone/:phoneId', where :phoneIdis a variable part of the URL. To construct the phone details view, angular will use the phone-detail.htmltemplate and

the PhoneDetailCtrl controller.

We reused the PhoneListCtrl controller that we constructed in previous steps and we added a new, emptyPhoneDetailCtrl controller to the app/js/controllers.js file for the phone details view.

The statement $route.otherwise({redirectTo: '/phones'}) triggers a redirection to /phones when the browser address doesn't match either of our routes.

(26)

Note the use of the :phoneId parameter in the second route declaration. The $route service uses the route declaration — '/phones/:phoneId' — as a template that is matched against the current URL. All variables defined with the : notation are extracted into

the $routeParams object.

In order for our application to bootstrap with our newly created module we'll also need to specify the module name as the value of the ngApp directive:

app/index.html:

1. <!doctype html>

2. <html lang="en" ng-app="phonecat">

3. ...

Controllers

app/js/controllers.js:

1. ...

2. function PhoneDetailCtrl($scope, $routeParams) { 3. $scope.phoneId = $routeParams.phoneId;

4. } 5.

6. //PhoneDetailCtrl.$inject = ['$scope', '$routeParams'];

Template

The $route service is usually used in conjunction with the ngView directive. The role of the ngView directive is to include the view template for the current route into the layout template, which makes it a perfect fit for ourindex.html template.

app/index.html:

1. <html lang="en" ng-app="phonecat">

2. <head>

3. ...

4. <script src="lib/angular/angular.js"></script>

5. <script src="js/app.js"></script>

6. <script src="js/controllers.js"></script>

7. </head>

8. <body>

9.

10. <div ng-view></div>

11. 12. </body>

(27)

Note that we removed most of the code in the index.html template and replaced it with a single line containing a div with the ng-view attribute. The code that we removed was placed into the phone-list.html template:

app/partials/phone-list.html:

1. <div class="container-fluid">

2. <div class="row-fluid">

3. <div class="span2">

4. <!--Sidebar content--> 5.

6. Search: <input ng-model="query">

7. Sort by:

8. <select ng-model="orderProp">

9. <option value="name">Alphabetical</option>

10. <option value="age">Newest</option>

11. </select>

12.

13. </div>

14. <div class="span10">

15. <!--Body content--> 16.

17. <ul class="phones">

18. <li ng-repeat="phone in phones | filter:query | orderBy:orderProp"

class="thumbnail">

19. <a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{ph one.imageUrl}}"></a>

20. <a href="#/phones/{{phone.id}}">{{phone.name}}</a>

21. <p>{{phone.snippet}}</p> 22. </li> 23. </ul> 24. 25. </div> 26. </div> 27. </div>

We also added a placeholder template for the phone details view: app/partials/phone-detail.html:

1. TBD: detail view for {{phoneId}}

Note how we are using phoneId model defined in the PhoneDetailCtrl controller.

Test

(28)

To automatically verify that everything is wired properly, we wrote end-to-end tests that navigate to various URLs and verify that the correct view was rendered.

1. ...

2. it('should redirect index.html to index.html#/phones', function() { 3. browser().navigateTo('../../app/index.html');

4. expect(browser().location().url()).toBe('/phones'); 5. });

6. ... 7.

8. describe('Phone detail view', function() { 9. 10. beforeEach(function() { 11. browser().navigateTo('../../app/index.html#/phones/nexus-s'); 12. }); 13. 14.

15. it('should display placeholder page with phoneId', function() { 16. expect(binding('phoneId')).toBe('nexus-s');

17. }); 18. });

You can now rerun ./scripts/e2e-test.sh or refresh the browser tab with the end-to-end test runner to see the tests run, or you can see them running on Angular's server.

Experiments

Try to add an

{{orderProp}}

binding to

index.html

, and you'll see that

nothing happens even when you are in the phone list view. This is because

the

orderProp

model is visible only in the scope managed by

PhoneListCtrl

,

which is associated with the

<div ng-view>

element. If you add the same

binding into the

phone-list.html

template, the binding will work as expected.

Summary

With the routing set up and the phone list view implemented, we're ready to go to step 8 to implement the phone details view.

 

In this step, you will implement the phone details view, which is displayed when a user clicks on a phone in the phone list.

Workspace Reset Instructions ➤

Now when you click on a phone on the list, the phone details page with phone-specific information is displayed.

(29)

To implement the phone details view we will use $http to fetch our data, and we'll flesh out the phone-detail.html view template.

The most important changes are listed below. You can see the full diff on GitHub:

Data

In addition to phones.json, the app/phones/ directory also contains one json file for each phone:

app/phones/nexus-s.json: (sample snippet)

1. {

2. "additionalFeatures": "Contour Display, Near Field Communications (NFC),. ..", 3. "android": { 4. "os": "Android 2.3", 5. "ui": "Android" 6. }, 7. ... 8. "images": [ 9. "img/phones/nexus-s.0.jpg", 10. "img/phones/nexus-s.1.jpg", 11. "img/phones/nexus-s.2.jpg", 12. "img/phones/nexus-s.3.jpg" 13. ], 14. "storage": { 15. "flash": "16384MB", 16. "ram": "512MB" 17. } 18. }

Each of these files describes various properties of the phone using the same data structure. We'll show this data in the phone detail view.

Controller

We'll expand the PhoneDetailCtrl by using the $http service to fetch the json files. This works the same way as the phone list controller.

app/js/controllers.js:

1. function PhoneDetailCtrl($scope, $routeParams, $http) {

2. $http.get('phones/' + $routeParams.phoneId + '.json').success(function(da ta) {

3. $scope.phone = data; 4. });

(30)

6.

7. //PhoneDetailCtrl.$inject = ['$scope', '$routeParams', '$http'];

To construct the URL for the HTTP request, we use $routeParams.phoneId extracted from the current route by the$route service.

Template

The TBD placeholder line has been replaced with lists and bindings that comprise the phone details. Note where we use the angular {{expression}} markup and ngRepeat to project phone data from our model into the view.

app/partials/phone-detail.html:

1. <img ng-src="{{phone.images[0]}}" class="phone">

2.

3. <h1>{{phone.name}}</h1>

4.

5. <p>{{phone.description}}</p>

6.

7. <ul class="phone-thumbs">

8. <li ng-repeat="img in phone.images">

9. <img ng-src="{{img}}">

10. </li>

11. </ul>

12.

13. <ul class="specs">

14. <li>

15. <span>Availability and Networks</span>

16. <dl>

17. <dt>Availability</dt>

18. <dd ng-repeat="availability in phone.availability">{{availability }}</dd>

19. </dl>

20. </li>

21. ... 22. </li>

23. <span>Additional Features</span>

24. <dd>{{phone.additionalFeatures}}</dd>

25. </li>

26. </ul>

Test

We wrote a new unit test that is similar to the one we wrote for the PhoneListCtrl controller in step 5.

(31)

1. ...

2. describe('PhoneDetailCtrl', function(){ 3. var scope, $httpBackend, ctrl;

4.

5. beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $c ontroller) {

6. $httpBackend = _$httpBackend_;

7. $httpBackend.expectGET('phones/xyz.json').respond({name:'phone xyz'}) ;

8.

9. $routeParams.phoneId = 'xyz'; 10. scope = $rootScope.$new();

11. ctrl = $controller(PhoneDetailCtrl, {$scope: scope}); 12. }));

13. 14.

15. it('should fetch phone detail', function() { 16. expect(scope.phone).toBeUndefined(); 17. $httpBackend.flush();

18.

19. expect(scope.phone).toEqual({name:'phone xyz'}); 20. });

21. }); 22. ...

You should now see the following output in the Testacular tab:

Chrome 22.0: Executed 3 of 3 SUCCESS (0.039 secs / 0.012 secs)

We also added a new end-to-end test that navigates to the Nexus S detail page and verifies that the heading on the page is "Nexus S".

test/e2e/scenarios.js:

1. ...

2. describe('Phone detail view', function() { 3. 4. beforeEach(function() { 5. browser().navigateTo('../../app/index.html#/phones/nexus-s'); 6. }); 7. 8.

9. it('should display nexus-s page', function() { 10. expect(binding('phone.name')).toBe('Nexus S'); 11. });

12. }); 13. ...

(32)

You can now rerun ./scripts/e2e-test.sh or refresh the browser tab with the end-to-end test runner to see the tests run, or you can see them running on Angular's server.

Experiments

Using the

Angular's end-to-end test runner API

, write a test that verifies that

we display 4 thumbnail images on the Nexus S details page.

Summary

Now that the phone details view is in place, proceed to step 9 to learn how to write your own custom display filter.

 

 

In this step you will learn how to create your own custom display filter. Workspace Reset Instructions ➤

Navigate to one of the detail pages.

In the previous step, the details page displayed either "true" or "false" to indicate whether certain phone features were present or not. We have used a custom filter to convert those text strings into glyphs: ✓ for "true", and ✘ for "false". Let's see what the filter code looks like. The most important changes are listed below. You can see the full diff on GitHub:

Custom Filter

In order to create a new filter, you are going to create a phonecatFilters module and register your custom filter with this module:

app/js/filters.js:

1. angular.module('phonecatFilters', []).filter('checkmark', function() { 2. return function(input) {

3. return input ? '\u2713' : '\u2718'; 4. };

5. });

The name of our filter is "checkmark". The input evaluates to either true or false, and we return one of two unicode characters we have chosen to represent true or false

(\u2713 and \u2718).

Now that our filter is ready, we need to register the phonecatFilters module as a dependency for our mainphonecat module.

(33)

1. ...

2. angular.module('phonecat', ['phonecatFilters']). 3. ...

Template

Since the filter code lives in the app/js/filters.js file, we need to include this file in our layout template.

app/index.html:

1. ...

2. <script src="js/controllers.js"></script> 3. <script src="js/filters.js"></script> 4. ...

The syntax for using filters in Angular templates is as follows: {{ expression | filter }}

Let's employ the filter in the phone details template: app/partials/phone-detail.html: 1. ... 2. <dl> 3. <dt>Infrared</dt> 4. <dd>{{phone.connectivity.infrared | checkmark}}</dd> 5. <dt>GPS</dt> 6. <dd>{{phone.connectivity.gps | checkmark}}</dd> 7. </dl> 8. ...

Test

Filters, like any other component, should be tested and these tests are very easy to write. test/unit/filtersSpec.js:

1. describe('filter', function() { 2.

3. beforeEach(module('phonecatFilters')); 4.

5.

6. describe('checkmark', function() { 7.

(34)

9. inject(function(checkmarkFilter) {

10. expect(checkmarkFilter(true)).toBe('\u2713'); 11. expect(checkmarkFilter(false)).toBe('\u2718'); 12. }));

13. }); 14. });

Note that you need to configure our test injector with the phonecatFilters module before any of our filter tests execute.

You should now see the following output in the Testacular tab:

Chrome 22.0: Executed 4 of 4 SUCCESS (0.034 secs / 0.012 secs)

Experiments

• Let's experiment with some of the built-in Angular filters and add the following bindings toindex.html:

• {{ "lower cap string" | uppercase }} • {{ {foo: "bar", baz: 23} | json }} • {{ 1304375948024 | date }}

• {{ 1304375948024 | date:"MM/dd/yyyy @ h:mma" }}

• We can also create a model with an input element, and combine it with a filtered binding. Add the following to index.html:

<input ng-model="userInput"> Uppercased: {{ userInput | uppercase }}

Summary

Now that you have learned how to write and test a custom filter, go to step 10 to learn how we can use Angular to enhance the phone details page further.

 

 

In this step, you will add a clickable phone image swapper to the phone details page. Workspace Reset Instructions ➤

The phone details view displays one large image of the current phone and several smaller thumbnail images. It would be great if we could replace the large image with any of the

thumbnails just by clicking on the desired thumbnail image. Let's have a look at how we can do this with Angular.

The most important changes are listed below. You can see the full diff on GitHub:

Controller

(35)

1. ...

2. function PhoneDetailCtrl($scope, $routeParams, $http) {

3. $http.get('phones/' + $routeParams.phoneId + '.json').success(function(da ta) {

4. $scope.phone = data;

5. $scope.mainImageUrl = data.images[0]; 6. });

7.

8. $scope.setImage = function(imageUrl) { 9. $scope.mainImageUrl = imageUrl; 10. }

11. } 12.

13. //PhoneDetailCtrl.$inject = ['$scope', '$routeParams', '$http'];

In the PhoneDetailCtrl controller, we created the mainImageUrl model property and set its default value to the first phone image URL.

We also created a setImage event handler function that will change the value of mainImageUrl.

Template

app/partials/phone-detail.html:

1. <img ng-src="{{mainImageUrl}}" class="phone">

2. 3. ... 4.

5. <ul class="phone-thumbs">

6. <li ng-repeat="img in phone.images">

7. <img ng-src="{{img}}" ng-click="setImage(img)">

8. </li>

9. </ul>

10. ...

We bound the ngSrc directive of the large image to the mainImageUrl property.

We also registered an ngClick handler with thumbnail images. When a user clicks on one of the thumbnail images, the handler will use the setImage event handler function to change the value of the mainImageUrl property to the URL of the thumbnail image.

Test

To verify this new feature, we added two end-to-end tests. One verifies that the main image is set to the first phone image by default. The second test clicks on several thumbnail images and verifies that the main image changed appropriately.

(36)

test/e2e/scenarios.js:

1. ...

2. describe('Phone detail view', function() { 3.

4. ... 5.

6. it('should display the first phone image as the main phone image', func tion() {

7. expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.0.j pg');

8. }); 9. 10.

11. it('should swap main image if a thumbnail image is clicked on', functio n() {

12. element('.phone-thumbs li:nth-child(3) img').click();

13. expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.2.j pg');

14.

15. element('.phone-thumbs li:nth-child(1) img').click();

16. expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.0.j pg');

17. }); 18. }); 19. });

You can now rerun ./scripts/e2e-test.sh or refresh the browser tab with the end-to-end test runner to see the tests run, or you can see them running on Angular's server.

Experiments

• Let's add a new controller method to PhoneDetailCtrl:

$scope.hello = function(name) { alert('Hello ' + (name || 'w orld') + '!'); }

and add:

<button ng-click="hello('Elmo')">Hello</button> to the phone-details.html template.

Summary

With the phone image swapper in place, we're ready for step 11 (the last step!) to learn an even better way to fetch data.

(37)

 

In this step, you will improve the way our app fetches data. Workspace Reset Instructions ➤

The last improvement we will make to our app is to define a custom service that represents a RESTful client. Using this client we can make XHR requests for data in an easier way, without having to deal with the lower-level $http API, HTTP methods and URLs.

The most important changes are listed below. You can see the full diff on GitHub:

Template

The custom service is defined in app/js/services.js so we need to include this file in our layout template. Additionally, we also need to load the angular-resource.js file, which contains the ngResource module and in it the $resource service, that we'll soon use: app/index.html.

1. ...

2. <script src="js/services.js"></script>

3. <script src="lib/angular/angular-resource.js"></script> 4. ...

Service

app/js/services.js.

1. angular.module('phonecatServices', ['ngResource']). 2. factory('Phone', function($resource){

3. return $resource('phones/:phoneId.json', {}, {

4. query: {method:'GET', params:{phoneId:'phones'}, isArray:true} 5. });

6. });

We used the module API to register a custom service using a factory function. We passed in the name of the service - 'Phone' - and the factory function. The factory function is similar to a controller's constructor in that both can declare dependencies via function arguments. The Phone service declared a dependency on the $resource service.

The $resource service makes it easy to create a RESTful client with just a few lines of code. This client can then be used in our application, instead of the lower-level $http service. app/js/app.js.

1. ...

2. angular.module('phonecat', ['phonecatFilters', 'phonecatServices']). 3. ...

(38)

We need to add 'phonecatServices' to 'phonecat' application's requires array.

Controller

We simplified our sub-controllers (PhoneListCtrl and PhoneDetailCtrl) by factoring out the lower-level $httpservice, replacing it with a new service called Phone.

Angular's $resource service is easier to use than $http for interacting with data sources exposed as RESTful resources. It is also easier now to understand what the code in our controllers is doing.

app/js/controllers.js.

1. ... 2.

3. function PhoneListCtrl($scope, Phone) { 4. $scope.phones = Phone.query();

5. $scope.orderProp = 'age'; 6. }

7.

8. //PhoneListCtrl.$inject = ['$scope', 'Phone']; 9.

10. 11.

12. function PhoneDetailCtrl($scope, $routeParams, Phone) {

13. $scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {

14. $scope.mainImageUrl = phone.images[0]; 15. });

16.

17. $scope.setImage = function(imageUrl) { 18. $scope.mainImageUrl = imageUrl; 19. }

20. } 21.

22. //PhoneDetailCtrl.$inject = ['$scope', '$routeParams', 'Phone']; Notice how in PhoneListCtrl we replaced:

$http.get('phones/phones.json').success(function(data) { $scope.phone s = data; });

with:

$scope.phones = Phone.query();

This is a simple statement that we want to query for all phones.

An important thing to notice in the code above is that we don't pass any callback functions when invoking methods of our Phone service. Although it looks as if the result were returned synchronously, that is not the case at all. What is returned synchronously is a "future" — an

(39)

object, which will be filled with data when the XHR response returns. Because of the data-binding in Angular, we can use this future and bind it to our template. Then, when the data arrives, the view will automatically update.

Sometimes, relying on the future object and data-binding alone is not sufficient to do everything we require, so in these cases, we can add a callback to process the server response.

The PhoneDetailCtrl controller illustrates this by setting the mainImageUrl in a callback.

Test

We have modified our unit tests to verify that our new service is issuing HTTP requests and processing them as expected. The tests also check that our controllers are interacting with the service correctly.

The $resource service augments the response object with methods for updating and deleting the resource. If we were to use the standard toEqual matcher, our tests would fail because the test values would not match the responses exactly. To solve the problem, we use a newly-defined toEqualData Jasmine matcher. When the toEqualDatamatcher compares two objects, it takes only object properties into account and ignores methods.

test/unit/controllersSpec.js:

1. describe('PhoneCat controllers', function() { 2.

3. beforeEach(function(){ 4. this.addMatchers({

5. toEqualData: function(expected) {

6. return angular.equals(this.actual, expected); 7. }

8. }); 9. }); 10. 11.

12. beforeEach(module('phonecatServices')); 13.

14.

15. describe('PhoneListCtrl', function(){ 16. var scope, ctrl, $httpBackend; 17.

18. beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) { 19. $httpBackend = _$httpBackend_;

20. $httpBackend.expectGET('phones/phones.json').

21. respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]); 22.

23. scope = $rootScope.$new();

24. ctrl = $controller(PhoneListCtrl, {$scope: scope}); 25. }));

26. 27.

(40)

28. it('should create "phones" model with 2 phones fetched from xhr', funct ion() { 29. expect(scope.phones).toEqual([]); 30. $httpBackend.flush(); 31. 32. expect(scope.phones).toEqualData(

33. [{name: 'Nexus S'}, {name: 'Motorola DROID'}]); 34. });

35. 36.

37. it('should set the default value of orderProp model', function() { 38. expect(scope.orderProp).toBe('age');

39. }); 40. }); 41. 42.

43. describe('PhoneDetailCtrl', function(){ 44. var scope, $httpBackend, ctrl,

45. xyzPhoneData = function() { 46. return {

47. name: 'phone xyz',

48. images: ['image/url1.png', 'image/url2.png'] 49. }

50. }; 51.

52.

53. beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $c ontroller) {

54. $httpBackend = _$httpBackend_;

55. $httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData()); 56.

57. $routeParams.phoneId = 'xyz'; 58. scope = $rootScope.$new();

59. ctrl = $controller(PhoneDetailCtrl, {$scope: scope}); 60. }));

61. 62.

63. it('should fetch phone detail', function() { 64. expect(scope.phone).toEqualData({}); 65. $httpBackend.flush(); 66. 67. expect(scope.phone).toEqualData(xyzPhoneData()); 68. }); 69. }); 70. });

References

Related documents

Field experiments were conducted at Ebonyi State University Research Farm during 2009 and 2010 farming seasons to evaluate the effect of intercropping maize with

The purpose of the study was to assess customer retention strategies in commercial banks from a customer’s perspective based on gender and age using the

Application of 0.5 % bupivacaine soaked surgicel at the gall bladder bed after laparoscopic cholecystectomy will result in a lower postoperative mean pain score com- pared to

SanDisk Enterprise Cruzer Installs Software Every Time It’s Used I’m a bit concerned that this device actually unpacks a .zip file of software, images and DLLs onto your device

But also the training of model can be a problematic task: even if it can be easily performed in an ATC system (the ATC systems have a testing environment that is a copy of the

Want to get connected with family, friends, or the Kennett Area Senior Center? We have started remote programs using Zoom... Zoom is a service which provides simple online meetings

Ideas are illustrated by fitting, to a bivariate time series data of weekly exchange rates, nine multivariate SV models, including the specifications with Granger causality in

It is seen that application of Makshiklavanadi Varti and Navkarshik Guggulu provides better result in pain, burning sensation, itching, discharge &amp; swelling