diff options
Diffstat (limited to 'docs/content/tutorial/step_05.ngdoc')
| -rwxr-xr-x | docs/content/tutorial/step_05.ngdoc | 228 | 
1 files changed, 151 insertions, 77 deletions
| diff --git a/docs/content/tutorial/step_05.ngdoc b/docs/content/tutorial/step_05.ngdoc index 3727d8f0..1e63e383 100755 --- a/docs/content/tutorial/step_05.ngdoc +++ b/docs/content/tutorial/step_05.ngdoc @@ -1,4 +1,4 @@ -@ngdoc overview +@ngdoc overview  @name Tutorial: Step 5  @description  <table id="tutorial_nav"> @@ -13,33 +13,46 @@ Diff}</td>  </tr>  </table> +  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 {@link angular.service services} called {@link -angular.service.$xhr $xhr}. We will use angular's dependency injection to provide the service to -the `PhoneListCtrl` controller. +angular.service.$xhr $xhr}. We will use angular's {@link guide.di dependency injection (DI)} to +provide the service to the `PhoneListCtrl` controller. + + +1. Reset your workspace to step 5. + -1. Reset your workspace to Step 5 using: +         git checkout -f step-5 -         git checkout --force step-5    or +           ./goto_step.sh 5 + +  2. Refresh your browser or check the app out on {@link -http://angular.github.com/angular-phonecat/step-5/app angular's server}. You should now see a list -of 20 phones. +http://angular.github.com/angular-phonecat/step-5/app angular's server}. + +You should now see a list of 20 phones. + +  The most important changes are listed below. You can see the full diff on {@link  https://github.com/angular/angular-phonecat/compare/step-4...step-5  GitHub}: +  ## Data -The `app/phones/phone.json` file in your project is a dataset that contains a larger list of -phones stored in the JSON format. + +The `app/phones/phone.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:  <pre> @@ -56,177 +69,238 @@ Following is a sample of the file:  </pre> + +  ## Controller -In this step, the view template will remain the same but the model and controller will change. -We'll use angular's {@link angular.service.$xhr} service to make an HTTP request to your web -server to fetch the data in the `phones.json` file. + +We'll use angular's {@link angular.service.$xhr $xhr} service in our controller to make an HTTP +request to your web server to fetch the data in the `app/phones/phones.json` file. `$xhr` is just +one of several built-in {@link angular.service 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 {@link guide.di 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:`__  <pre>  function PhoneListCtrl($xhr) {    var self = this; +    $xhr('GET', 'phones/phones.json', function(code, response) {      self.phones = response;    }); +    self.orderProp = 'age';  } +  //PhoneListCtrl.$inject = ['$xhr'];  </pre> -We removed the hard-coded dataset from the controller and instead are using the `$xhr`  service to -access the data stored in `app/phones/phones.json`. The `$xhr` service makes a HTTP GET request to -our web server, asking for `phone/phones.json` (the url is relative to our `index.html` file). The -server responds by providing the data in the json file. -Keep in mind that 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.  +`$xhr` makes an HTTP GET request to our web server, asking for `phone/phones.json` (the url is +relative to our `index.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.) + -Notice that the `$xhr` service takes a callback as the last parameter. This callback is used to -process the response. In our case, we just assign the response to the current scope controlled by -the controller, as a model called `phones`. Have you realized that we didn't even have to parse -the response? Angular took care of that for us. +The `$xhr` service takes a callback as the last argument. This callback is used to process the +response. We assign the response to the scope controlled by the controller, as a model called +`phones`. Notice that angular detected the json response and parsed it for us! -We already mentioned that the `$xhr` function we just used is an angular service. {@link -angular.service Angular services} are substitutable objects managed by angular's {@link guide.di -DI subsystem}. -Dependency injection helps to make your web apps well structured, loosely coupled, and much easier -to test. What's important to understand is how the controllers get access to these services -through dependency injection. +To use a service in angular, you simply declare the names of the services you need as arguments to +the controller's constructor function, as follows: -The dependency injection pattern is based on declaring the dependencies we require and letting the -system provide them to us. To do this in angular, you simply provide the names of the services you -need as arguments to the controller's constructor function, as follows:      function PhoneListCtrl($xhr) { -The name of the argument is significant, because angular recognizes the identity of a service by -the argument name. Once angular knows what services are being requested, it provides them to the -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). -As we mentioned earlier, angular infers the controller's dependencies from the names of arguments -of the controller's constructor function. If you were to minify the JavaScript code for this -controller, all of these function arguments would be minified as well, and the dependency injector -would not being able to identify services correctly.  +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). + + +<img src="img/tutorial/xhr_service_final.png"> + + + + +### '$' 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 {@link http://en.wikipedia.org/wiki/Minification_(programming) +minify} the JavaScript code for `PhoneListCtrl` controller, all of its function arguments would be +minified as well, and the dependency injector would not being able to identify services correctly. +  To overcome issues caused by minification, just assign an array with service identifier strings  into the `$inject` property of the controller function, just like the last line in the snippet  (commented out) suggests: +      PhoneListCtrl.$inject = ['$xhr']; + +  ## 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 `$xhr` 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: + +  <pre>  describe('PhoneCat controllers', function() { +    describe('PhoneListCtrl', function(){      var scope, $browser, ctrl; +      beforeEach(function() {        scope = angular.scope();        $browser = scope.$service('$browser'); +        $browser.xhr.expectGET('phones/phones.json').respond([{name: 'Nexus S'},                                                              {name: 'Motorola DROID'}]);        ctrl = scope.$new(PhoneListCtrl);      }); +  }); +</pre> + + +We created the controller in the test environment, as follows: +* We created a root scope object by calling `angular.scope()` + + +* We called `scope.$new(PhoneListCtrl)` to get angular to create the child scope associated with +the `PhoneListCtrl` controller + + +Because our code now uses the `$xhr` service to fetch the phone list data in our controller, before +we create the `PhoneListCtrl` child scope, we need to tell the testing harness to expect an +incoming request from the controller. To do this we: + + +* Use the {@link angular.scope.$service `$service`} method to retrieve the `$browser` service, a +service that angular uses to represent various browser APIs. In tests, angular automatically uses a +mock version of this service that allows you to write tests without having to deal with these +native APIs and the global state associated with them. + + +* Use the `$browser.expectGET` method to train the `$browser` object to expect an incoming HTTP +request and tell it what to respond with. Note that the responses are not returned before we call +the `$browser.xhr.flush` method. + + +Now, we will make assertions to verify that the `phones` model doesn't exist on the scope, before +the response is received: + + +<pre>      it('should create "phones" model with 2 phones fetched from xhr', function() {        expect(ctrl.phones).toBeUndefined();        $browser.xhr.flush(); +        expect(ctrl.phones).toEqual([{name: 'Nexus S'},                                     {name: 'Motorola DROID'}]);      }); - - -    it('should set the default value of orderProp model', function() { -      expect(ctrl.orderProp).toBe('age'); -    }); -  }); -});  </pre> -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 `$xhr` 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.  +* We flush the xhr queue in the browser by calling `$browser.xhr.flush()`. This causes the callback +we passed into the `$xhr` service to be executed with the trained response. -To create the controller in the test environment, do the following: -* Create a root scope object by calling `angular.scope()` +* We make the assertions, verifying that the phone model now exists on the scope. -* Call `scope.$new(PhoneListCtrl)` to get angular to create the child scope associated with the -`PhoneListCtrl` controller. -Because our code now uses the `$xhr` service to fetch the phone list data in our controller, -before we create the `PhoneListCtrl` child scope, we need to tell the testing harness to expect an -incoming request from the controller. To do this we: - -* Use the {@link angular.scope.$service `$service`} method to retrieve the `$browser` service, a -service that angular uses to represent various browser APIs. In tests, angular automatically uses -a mock version of this service that allows you to write tests without having to deal with these -native APIs and the global state associated with them. - -* We use the `$browser.expectGET` method to train the `$browser` object to expect an incoming HTTP -request and tell it what to respond with. Note that the responses are not returned before we call -the `$browser.xhr.flush` method. +Finally, we verify that the default value of `orderProp` is set correctly: -* We then make assertions to verify that the `phones` model doesn't exist on the scope, before the -response is received. -* We flush the xhr queue in the browser by calling `$browser.xhr.flush()`. This causes the -callback we passed into the `$xhr` service to be executed with the trained response. +<pre> +    it('should set the default value of orderProp model', function() { +      expect(ctrl.orderProp).toBe('age'); +    }); +  }); +}); +</pre> -* Finally, we make the assertions, verifying that the phone model now exists on the scope.  To run the unit tests, execute the `./scripts/test.sh` script and you should see the following  output. +         Chrome: Runner reset.         ..         Total 2 tests (Passed: 2; Fails: 0; Errors: 0) (3.00 ms)           Chrome 11.0.696.57 Mac OS: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (3.00 ms) + +  # Experiments +  * At the bottom of `index.html`, add a `{{phones}}` binding to see the list of phones displayed in  json format. +  * In the `PhoneListCtrl` controller, pre-process the xhr response by limiting the number of phones -to the first 5 in the list.  Use the following code in the xhr callback:  +to the first 5 in the list. Use the following code in the xhr callback: +           self.phones = response.splice(0, 5); + +  # Summary +  Now that you have learned how easy it is to use angular services (thanks to angular's  implementation of dependency injection), go to step 6, where you will add some thumbnail images of  phones and some links. +  <table id="tutorial_nav">  <tr>   <td id="previous_step">{@link tutorial.step_04 Previous}</td>   <td id="step_result">{@link  http://angular.github.com/angular-phonecat/step-5/app Live Demo  }</td>   <td id="tut_home">{@link tutorial Tutorial Home}</td> - <td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-4...step-5 - Code Diff}</td> + <td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-4...step-5 Code +Diff}</td>   <td id="next_step">{@link tutorial.step_06 Next}</td>  </tr>  </table> | 
