From f72257d10b38ec29df7f3d0bc1ed248ec5baf923 Mon Sep 17 00:00:00 2001 From: Tero Parviainen Date: Sun, 24 Jan 2016 15:16:43 +0200 Subject: [PATCH] docs(tutorial): update PhoneCat tutorial to Angular 1.5, components, and style guide Bring the PhoneCat tutorial up to speed with Angular 1.5. Introduce components instead of bare ngControllers. Add an additional chapter about organizing the code in feature modules and using the style guide. This also makes the guide compatible with the Angular 2 upgrade tutorial on angular.io. --- docs/content/tutorial/index.ngdoc | 3 +- docs/content/tutorial/step_02.ngdoc | 150 +++++----- docs/content/tutorial/step_03.ngdoc | 100 +------ docs/content/tutorial/step_04.ngdoc | 44 +-- docs/content/tutorial/step_05.ngdoc | 88 +++--- docs/content/tutorial/step_06.ngdoc | 11 +- docs/content/tutorial/step_07.ngdoc | 151 ++++------ docs/content/tutorial/step_08.ngdoc | 43 +-- docs/content/tutorial/step_09.ngdoc | 10 +- docs/content/tutorial/step_10.ngdoc | 51 ++-- docs/content/tutorial/step_11.ngdoc | 135 +++------ docs/content/tutorial/step_12.ngdoc | 42 +-- docs/content/tutorial/step_13.ngdoc | 435 ++++++++++++++++++++++++++++ 13 files changed, 789 insertions(+), 474 deletions(-) create mode 100644 docs/content/tutorial/step_13.ngdoc diff --git a/docs/content/tutorial/index.ngdoc b/docs/content/tutorial/index.ngdoc index f96c0a2ad5e0..19e2d4741bb1 100644 --- a/docs/content/tutorial/index.ngdoc +++ b/docs/content/tutorial/index.ngdoc @@ -29,9 +29,10 @@ When you finish the tutorial you will be able to: * Use data binding to wire up your data model to your views. * Create and run unit tests, with Karma. * Create and run end to end tests, with Protractor. -* Move application logic out of the template and into Controllers. +* Move application logic out of the template and into components and controllers. * Get data from a server using Angular services. * Apply animations to your application, using ngAnimate. +* Organize your application code for larger projects. * Identify resources for learning more about AngularJS. The tutorial guides you through the entire process of building a simple application, including diff --git a/docs/content/tutorial/step_02.ngdoc b/docs/content/tutorial/step_02.ngdoc index 624bbc322e6e..ec1d5122de88 100644 --- a/docs/content/tutorial/step_02.ngdoc +++ b/docs/content/tutorial/step_02.ngdoc @@ -7,12 +7,12 @@ 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. +code for the component 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](http://en.wikipedia.org/wiki/Model–View–Controller) 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. +JavaScript to add models, views, and controllers to our app. - The list of three phones is now generated dynamically from data @@ -25,67 +25,69 @@ In Angular, the __view__ is a projection of the model through the HTML __templat whenever the model changes, Angular refreshes the appropriate binding points, which updates the view. -The view component is constructed by Angular from this template: +The view is constructed by Angular from this template. -__`app/index.html`:__ +__`app/partials/phone-list.html`:__ ```html - - - ... - - - - - - - - - + ``` -We replaced the hard-coded phone list with the {@link ng.directive:ngRepeat ngRepeat directive} +Instead of a hard-coded phone list we've used the {@link ng.directive:ngRepeat ngRepeat directive} and two {@link guide/expression Angular expressions}: -* The `ng-repeat="phone in phones"` attribute in the `
  • ` tag is an Angular repeater directive. +* The `ng-repeat="phone in $ctrl.phones"` attribute in the `
  • ` tag is an Angular repeater directive. The repeater tells Angular to create a `
  • ` element for each phone in the list using the `
  • ` tag as the template. * The expressions wrapped in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) will be replaced -by the value of the expressions. +by the value of the expressions. The expressions denote bindings that refer to our application model, +which is set up in our controller. -We have added a new directive, called `ng-controller`, which attaches a `PhoneListCtrl` -__controller__ to the <body> tag. At this point: +In `index.html` we no longer have the hard-coded phone list. Instead we are using a `` +component element: -* The expressions in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) denote -bindings, which are referring to our application model, which is set up in our `PhoneListCtrl` -controller. +__`app/index.html`:__ + +```html + + + ... + + + + + + + +```
    Note: We have specified an {@link angular.Module Angular Module} to load using `ng-app="phonecatApp"`, -where `phonecatApp` is the name of our module. This module will contain the `PhoneListCtrl`. +where `phonecatApp` is the name of our module. This module will contain the phone list component.
    - - -## Model and Controller +## Component, Model, and Controller The data __model__ (a simple array of phones in object literal notation) is now instantiated within -the `PhoneListCtrl` __controller__. The __controller__ is simply a constructor function that takes a -`$scope` parameter: +the `PhoneListCtrl` __controller__. The __controller__ is simply a constructor function. It is used +by the `phoneList` component: -__`app/js/controllers.js`:__ +__`app/js/components.js`:__ ```js var phonecatApp = angular.module('phonecatApp', []); -phonecatApp.controller('PhoneListCtrl', function ($scope) { - $scope.phones = [ +phonecatApp.component('phoneList', { + controller: 'PhoneListCtrl', + templateUrl: 'partials/phone-list.html' +}).controller('PhoneListCtrl', function() { + this.phones = [ {'name': 'Nexus S', 'snippet': 'Fast just got faster with Nexus S.'}, {'name': 'Motorola XOOM™ with Wi-Fi', @@ -97,40 +99,42 @@ phonecatApp.controller('PhoneListCtrl', function ($scope) { ``` -Here we declared a controller called `PhoneListCtrl` and registered it in an AngularJS -module, `phonecatApp`. Notice that our `ng-app` directive (on the `` tag) now specifies the `phonecatApp` -module name as the module to load when bootstrapping the Angular application. +Here we declared a component called `phoneList` and a controller called `PhoneListCtrl` +and registered both in an AngularJS module, `phonecatApp`. Notice that our `ng-app` directive +(on the `` tag) now specifies the same `phonecatApp` module name as the module to load +when bootstrapping the Angular application. Although the controller is not yet doing very much, it plays 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 +the model and the view. We connected the dots between the presentation, data, and logic as follows: -* The {@link ng.directive:ngController ngController} directive, located on the `` tag, -references the name of our controller, `PhoneListCtrl` (located in the JavaScript file -`controllers.js`). +* The `` element, located on the `` tag, creates a + `phoneList` component (located in the JavaScript file `components.js`). -* The `PhoneListCtrl` controller attaches the phone data to the `$scope` that was injected into our -controller function. This *scope* is a prototypical descendant of the *root scope* that was created -when the application was defined. This controller scope is available to all bindings located within -the `` tag. +* The `phoneList` component creates an internal view from the `phone-list.html` template + and creates an instance of the controller `PhoneListCtrl`. + +* The `PhoneListCtrl` controller attaches the phone data to an attribute on itself. This controller + is available through the `$ctrl` alias to all bindings located within `phone-list.html` component + template. ### Scope -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. +Behind the scenes, Angular creates a **scope** for the component and uses it to bridge the component's controller and template 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 {@link ng.$rootScope.Scope angular scope documentation}. + ## Tests The "Angular way" of separating controller from the view, makes it easy to test code as it is being -developed. If our controller is available on the global namespace then we could simply instantiate it -with a mock `scope` object: +developed. If our controller is available on the global namespace then we could simply instantiate it: __`test/e2e/scenarios.js`:__ @@ -138,16 +142,15 @@ __`test/e2e/scenarios.js`:__ describe('PhoneListCtrl', function(){ it('should create "phones" model with 3 phones', function() { - var scope = {}, - ctrl = new PhoneListCtrl(scope); + var ctrl = new PhoneListCtrl(); - expect(scope.phones.length).toBe(3); + expect(ctrl.phones.length).toBe(3); }); }); ``` -The test instantiates `PhoneListCtrl` and verifies that the phones array property on the scope +The test instantiates `PhoneListCtrl` and verifies that the phones array property on it contains three records. This example demonstrates how easy it is to create a unit test for code in Angular. Since testing is such a critical part of software development, we make it easy to create tests in Angular so that developers are encouraged to write them. @@ -169,10 +172,9 @@ describe('PhoneListCtrl', function(){ beforeEach(module('phonecatApp')); it('should create "phones" model with 3 phones', inject(function($controller) { - var scope = {}, - ctrl = $controller('PhoneListCtrl', {$scope:scope}); + var ctrl = $controller('PhoneListCtrl'); - expect(scope.phones.length).toBe(3); + expect(ctrl.phones.length).toBe(3); })); }); @@ -181,14 +183,14 @@ describe('PhoneListCtrl', function(){ * Before each test we tell Angular to load the `phonecatApp` module. * We ask Angular to `inject` the `$controller` service into our test function * We use `$controller` to create an instance of the `PhoneListCtrl` -* With this instance, we verify that the phones array property on the scope contains three records. +* With this instance, we verify that the phones array property on it contains three records. ### Writing and Running Tests 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 v1.3. You can learn about Jasmine on the [Jasmine home page][jasmine] and +this tutorial in Jasmine v2.4. You can learn about Jasmine on the [Jasmine home page][jasmine] and at the [Jasmine docs][jasmine-docs]. The angular-seed project is pre-configured to run unit tests using [Karma][karma] but you will need @@ -230,25 +232,25 @@ browser is limited, which results in your karma tests running extremely slow. # Experiments -* Add another binding to `index.html`. For example: +* Add another binding to `phone-list.html`. For example: ```html -

    Total number of phones: {{phones.length}}

    +

    Total number of phones: {{ctrl.phones.length}}

    ``` * Create a new model property in the controller and bind to it from the template. For example: - $scope.name = "World"; + this.name = "World"; - Then add a new binding to `index.html`: + Then add a new binding to `phone-list.html`: -

    Hello, {{name}}!

    +

    Hello, {{ctrl.name}}!

    Refresh your browser and verify that it says "Hello, World!". * Update the unit test for the controller in `./test/unit/controllersSpec.js` to reflect the previous change. For example by adding: - expect(scope.name).toBe('World'); + expect(ctrl.name).toBe('World'); * Create a repeater in `index.html` that constructs a simple table: @@ -266,12 +268,12 @@ browser is limited, which results in your karma tests running extremely slow. Extra points: try and make an 8x8 table using an additional `ng-repeat`. -* Make the unit test fail by changing `expect(scope.phones.length).toBe(3)` to instead use `toBe(4)`. +* Make the unit test fail by changing `expect(ctrl.phones.length).toBe(3)` to instead use `toBe(4)`. # Summary -You now have a dynamic app that features separate model, view, and controller components, and you +You now have a dynamic app that features separate models, views, and controllers, and you are testing as you go. Now, let's go to {@link step_03 step 3} to learn how to add full text search to the app. @@ -279,5 +281,5 @@ to the app.
      [jasmine]: http://jasmine.github.io/ -[jasmine-docs]: http://jasmine.github.io/1.3/introduction.html +[jasmine-docs]: http://jasmine.github.io/2.4/introduction.html [karma]: http://karma-runner.github.io/ diff --git a/docs/content/tutorial/step_03.ngdoc b/docs/content/tutorial/step_03.ngdoc index 4025b520db2e..5e071142e5bd 100644 --- a/docs/content/tutorial/step_03.ngdoc +++ b/docs/content/tutorial/step_03.ngdoc @@ -17,14 +17,14 @@ user types into the search box.
      -## Controller +## Component and Controller -We made no changes to the controller. +We made no changes to the component or controller. ## Template -__`app/index.html`:__ +__`app/partials/phone-list.html`:__ ```html
      @@ -32,14 +32,14 @@ __`app/index.html`:__
      - Search: + Search:
        -
      • +
      • {{phone.name}}

        {{phone.snippet}}

      • @@ -60,15 +60,15 @@ 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 + In this code, the data that a user types into the input box (named __`$ctrl.query`__) is immediately +available as a filter input in the list repeater (`phone in $ctrl.phones | filter:`__`$ctrl.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 {@link ng.filter:filter filter} function uses the -`query` value to create a new array that contains only those records that match the `query`. +`$ctrl.query` value to create a new array that contains only those records that match the query. `ngRepeat` automatically updates the view in response to the changing number of phones returned by the `filter` filter. The process is completely transparent to the developer. @@ -76,7 +76,7 @@ by the `filter` filter. 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 +controllers and other parts 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. @@ -97,8 +97,8 @@ describe('PhoneCat App', function() { it('should filter the phone list as a user types into the search box', function() { - var phoneList = element.all(by.repeater('phone in phones')); - var query = element(by.model('query')); + var phoneList = element.all(by.repeater('phone in $ctrl.phones')); + var query = element(by.model('$ctrl.query')); expect(phoneList.count()).toBe(3); @@ -139,79 +139,12 @@ To rerun the test suite, execute `npm run protractor` again. # Experiments ### Display Current Query -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. - -### Display Query in Title -Let's see how we can get the current value of the `query` model to appear in the HTML page title. - -* Add an end-to-end test into the `describe` block, `test/e2e/scenarios.js` should look like this: - - ```js - describe('PhoneCat App', function() { - - describe('Phone list view', function() { - - beforeEach(function() { - browser.get('app/index.html'); - }); - - var phoneList = element.all(by.repeater('phone in phones')); - var query = element(by.model('query')); - - it('should filter the phone list as a user types into the search box', function() { - expect(phoneList.count()).toBe(3); - - query.sendKeys('nexus'); - expect(phoneList.count()).toBe(1); - - query.clear(); - query.sendKeys('motorola'); - expect(phoneList.count()).toBe(2); - }); - - it('should display the current filter value in the title bar', function() { - query.clear(); - expect(browser.getTitle()).toMatch(/Google Phone Gallery:\s*$/); - - query.sendKeys('nexus'); - expect(browser.getTitle()).toMatch(/Google Phone Gallery: nexus$/); - }); - }); - }); - ``` - - Run protractor (`npm run protractor`) to see this test fail. - - -* You might think you could just add the `{{query}}` to the title tag element as follows: - - Google Phone Gallery: {{query}} - - 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 `ng-controller="PhoneListCtrl"` directive, on the body - element: - - - - If you want to bind to the query model from the `` 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="phonecatApp" ng-controller="PhoneListCtrl"> - - Be sure to __remove__ the `ng-controller` declaration from the body element. - -* Re-run `npm run protractor` to see the test now pass. - -* 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 {@link ng.directive:ngBind ngBind} or -{@link ng.directive:ngBindTemplate ngBindTemplate} directives, which are invisible to the user -while the page is loading: - - <title ng-bind-template="Google Phone Gallery: {{query}}">Google Phone Gallery +Display the current value of the `query` model by adding a `{{$ctrl.query}}` binding into the +`phone-list.html` template, and see how it changes when you type in the input box. +You might also try to add the `{{$ctrl.query}}` to the `index.html` template. 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 `` component. # Summary @@ -220,4 +153,3 @@ to {@link step_04 step 4} to learn how to add sorting capability to the phone ap
          - diff --git a/docs/content/tutorial/step_04.ngdoc b/docs/content/tutorial/step_04.ngdoc index 2a041030f2ad..9b2f9a7555b4 100644 --- a/docs/content/tutorial/step_04.ngdoc +++ b/docs/content/tutorial/step_04.ngdoc @@ -19,28 +19,28 @@ the repeater, and letting the data binding magic do the rest of the work. ## Template -__`app/index.html`:__ +__`app/partials/phone-list.html`:__ ```html - Search: + Search: Sort by: -
            -
          • +
          • {{phone.name}}

            {{phone.snippet}}

          ``` -We made the following changes to the `index.html` template: +We made the following changes to the `phone-list.html` template: -* First, we added a `` html element named `$ctrl.orderProp`, so that our users can pick from the two provided sorting options. @@ -49,8 +49,8 @@ two provided sorting options. 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. +Angular creates a two way data-binding between the select element and the `$ctrl.orderProp` model. +`$ctrl.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 @@ -61,13 +61,16 @@ necessary! ## Controller -__`app/js/controllers.js`:__ +__`app/js/components.js`:__ ```js var phonecatApp = angular.module('phonecatApp', []); -phonecatApp.controller('PhoneListCtrl', function ($scope) { - $scope.phones = [ +phonecatApp.component('phoneList', { + controller: 'PhoneListCtrl', + templateUrl: 'partials/phone-list.html' +}).controller('PhoneListCtrl', function() { + this.phones = [ {'name': 'Nexus S', 'snippet': 'Fast just got faster with Nexus S.', 'age': 1}, @@ -79,7 +82,7 @@ phonecatApp.controller('PhoneListCtrl', function ($scope) { 'age': 3} ]; - $scope.orderProp = 'age'; + this.orderProp = 'age'; }); ``` @@ -110,22 +113,21 @@ __`test/unit/controllersSpec.js`:__ describe('PhoneCat controllers', function() { describe('PhoneListCtrl', function(){ - var scope, ctrl; + var ctrl; beforeEach(module('phonecatApp')); beforeEach(inject(function($controller) { - scope = {}; - ctrl = $controller('PhoneListCtrl', {$scope:scope}); + ctrl = $controller('PhoneListCtrl'); })); it('should create "phones" model with 3 phones', function() { - expect(scope.phones.length).toBe(3); + expect(ctrl.phones.length).toBe(3); }); it('should set the default value of orderProp model', function() { - expect(scope.orderProp).toBe('age'); + expect(ctrl.orderProp).toBe('age'); }); }); }); @@ -150,8 +152,8 @@ __`test/e2e/scenarios.js`:__ ... it('should be possible to control phone order via the drop down select box', function() { - var phoneNameColumn = element.all(by.repeater('phone in phones').column('phone.name')); - var query = element(by.model('query')); + var phoneNameColumn = element.all(by.repeater('phone in $ctrl.phones').column('phone.name')); + var query = element(by.model('$ctrl.query')); function getNames() { return phoneNameColumn.map(function(elm) { @@ -166,7 +168,7 @@ __`test/e2e/scenarios.js`:__ "MOTOROLA XOOM\u2122" ]); - element(by.model('orderProp')).element(by.css('option[value="name"]')).click(); + element(by.model('$ctrl.orderProp')).element(by.css('option[value="name"]')).click(); expect(getNames()).toEqual([ "MOTOROLA XOOM\u2122", @@ -185,7 +187,7 @@ You can now rerun `npm run protractor` to see the tests run. you'll see that Angular will temporarily add a new blank ("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 +* Add an `{{$ctrl.orderProp}}` binding into the `phone-list.html` template to display its current value as text. * Reverse the sort order by adding a `-` symbol before the sorting value: `` diff --git a/docs/content/tutorial/step_05.ngdoc b/docs/content/tutorial/step_05.ngdoc index c7db2889567e..26b3955f9f1a 100644 --- a/docs/content/tutorial/step_05.ngdoc +++ b/docs/content/tutorial/step_05.ngdoc @@ -36,7 +36,7 @@ Following is a sample of the file: ``` -## Controller +## Component and Controller We'll use Angular's {@link ng.$http $http} service in our controller to make an HTTP request to your web server to fetch the data in the `app/phones/phones.json` file. `$http` is just @@ -48,18 +48,24 @@ helps to make your web apps both well-structured (e.g., separate components for and control) and loosely coupled (dependencies between components are not resolved by the components themselves, but by the DI subsystem). -__`app/js/controllers.js:`__ +__`app/js/components.js:`__ ```js var phonecatApp = angular.module('phonecatApp', []); -phonecatApp.controller('PhoneListCtrl', function ($scope, $http) { +phonecatApp.component('phoneList', { + controller: 'PhoneListCtrl', + templateUrl: 'partials/phone-list.html' +}).controller('PhoneListCtrl', function ($http) { + var ctrl = this; + $http.get('phones/phones.json').success(function(data) { - $scope.phones = data; + ctrl.phones = data; }); - $scope.orderProp = 'age'; + ctrl.orderProp = 'age'; }); + ``` `$http` makes an HTTP GET request to our web server, asking for `phones/phones.json` (the url is @@ -69,14 +75,18 @@ browser and our app, they both look the same. For the sake of simplicity, we use tutorial.) The `$http` service returns a {@link ng.$q 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 called `phones`. Notice that Angular detected the +method. We call this method to handle the asynchronous response and assign the phone +data to the controller, as a model called `phones`. Notice that Angular detected the json response and parsed it for us! +Since we're making the assignment of the `phones` model in a callback function where +the `this` value is not defined, we also introduce a local variable called `ctrl` that +points back to the controller instance. + 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: - phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {...} + .controller('PhoneListCtrl', function ($http) {...} 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 @@ -117,8 +127,8 @@ as strings, which will not get minified. There are two ways to provide these inj In our example, we would write: ```js - function PhoneListCtrl($scope, $http) {...} - PhoneListCtrl.$inject = ['$scope', '$http']; + function PhoneListCtrl($http) {...} + PhoneListCtrl.$inject = ['$http']; phonecatApp.controller('PhoneListCtrl', PhoneListCtrl); ``` @@ -126,8 +136,8 @@ as strings, which will not get minified. There are two ways to provide these inj This array contains a list of the service names, followed by the function itself. ```js - function PhoneListCtrl($scope, $http) {...} - phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', PhoneListCtrl]); + function PhoneListCtrl($http) {...} + phonecatApp.controller('PhoneListCtrl', ['$http', PhoneListCtrl]); ``` Both of these methods work with any function that can be injected by Angular, so it's up to your @@ -137,7 +147,7 @@ When using the second method, it is common to provide the constructor function i anonymous function when registering the controller: ```js - phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', function($scope, $http) {...}]); + phonecatApp.controller('PhoneListCtrl', ['$http', function($http) {...}]); ``` From this point onward, we're going to use the inline method in the tutorial. With that in mind, @@ -148,14 +158,19 @@ __`app/js/controllers.js:`__ ```js var phonecatApp = angular.module('phonecatApp', []); -phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', - function ($scope, $http) { - $http.get('phones/phones.json').success(function(data) { - $scope.phones = data; - }); +phonecatApp.component('phoneList', { + controller: 'PhoneListCtrl', + templateUrl: 'partials/phone-list.html' +}).controller('PhoneListCtrl', ['$http', function ($http) { + var ctrl = this; + + $http.get('phones/phones.json').success(function(data) { + ctrl.phones = data; + }); + + ctrl.orderProp = 'age'; +}]); - $scope.orderProp = 'age'; - }]); ``` ## Test @@ -172,7 +187,7 @@ methods on a service called `$httpBackend`: describe('PhoneCat controllers', function() { describe('PhoneListCtrl', function(){ - var scope, ctrl, $httpBackend; + var ctrl, $httpBackend; // Load our app module definition before each test. beforeEach(module('phonecatApp')); @@ -180,13 +195,12 @@ describe('PhoneCat controllers', function() { // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). // This allows us to inject a service but then attach it to a variable // with the same name as the service in order to avoid a name conflict. - beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) { + beforeEach(inject(function(_$httpBackend_, $controller) { $httpBackend = _$httpBackend_; $httpBackend.expectGET('phones/phones.json'). respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]); - scope = $rootScope.$new(); - ctrl = $controller('PhoneListCtrl', {$scope: scope}); + ctrl = $controller('PhoneListCtrl'); })); ``` @@ -197,20 +211,16 @@ 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 -{@link ng.$rootScope $rootScope}, {@link ng.$controller $controller} and {@link ng.$httpBackend $httpBackend} services 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 name of the `PhoneListCtrl` controller -and the created scope as parameters. +* We called the injected `$controller` function passing the name of the `PhoneListCtrl` controller as a parameter. Because our code now uses the `$http` 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 +we create the `PhoneListCtrl`, 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 @@ -222,31 +232,31 @@ native APIs and the global state associated with them — both of which make tes 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 +Now we will make assertions to verify that the `phones` model doesn't exist on the controller before the response is received: ```js it('should create "phones" model with 2 phones fetched from xhr', function() { - expect(scope.phones).toBeUndefined(); + expect(ctrl.phones).toBeUndefined(); $httpBackend.flush(); - expect(scope.phones).toEqual([{name: 'Nexus S'}, + expect(ctrl.phones).toEqual([{name: 'Nexus S'}, {name: 'Motorola DROID'}]); }); ``` * 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. See -'Flushing HTTP requests' in the {@link ngMock.$httpBackend mock $httpBackend} documentation for +promise returned by the `$http` service to be resolved with the trained response. See +'Flushing HTTP requests' in the {@link ngMock.$httpBackend mock $httpBackend} documentation for a full explanation of why this is necessary. -* We make the assertions, verifying that the phone model now exists on the scope. +* We make the assertions, verifying that the phone model now exists on the controller. Finally, we verify that the default value of `orderProp` is set correctly: ```js it('should set the default value of orderProp model', function() { - expect(scope.orderProp).toBe('age'); + expect(ctrl.orderProp).toBe('age'); }); ``` @@ -258,13 +268,13 @@ You should now see the following output in the Karma tab: # Experiments -* At the bottom of `index.html`, add a `
          {{phones | filter:query | orderBy:orderProp | json}}
          ` +* At the bottom of `phone-list.html`, add a `
          {{$ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp | json}}
          ` binding to see the list of phones displayed in json format. * 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); + ctrl.phones = data.splice(0, 5); # Summary diff --git a/docs/content/tutorial/step_06.ngdoc b/docs/content/tutorial/step_06.ngdoc index 90862cb1f62a..f2eb6d6ae756 100644 --- a/docs/content/tutorial/step_06.ngdoc +++ b/docs/content/tutorial/step_06.ngdoc @@ -37,13 +37,16 @@ __`app/phones/phones.json`__ (sample snippet): ## Template -__`app/index.html`:__ +__`app/partials/phone-list.html`:__ ```html ...
            -
          • - {{phone.name}} +
          • + + {{phone.name}} + {{phone.name}}

            {{phone.snippet}}

          • @@ -71,7 +74,7 @@ __`test/e2e/scenarios.js`__: ```js ... it('should render phone specific links', function() { - var query = element(by.model('query')); + var query = element(by.model('$ctrl.query')); query.sendKeys('nexus'); element.all(by.css('.phones li a')).first().click(); browser.getLocationAbsUrl().then(function(url) { diff --git a/docs/content/tutorial/step_07.ngdoc b/docs/content/tutorial/step_07.ngdoc index 824e322d3e48..b983d45f0741 100644 --- a/docs/content/tutorial/step_07.ngdoc +++ b/docs/content/tutorial/step_07.ngdoc @@ -33,17 +33,17 @@ We are using [Bower][bower] to install client-side dependencies. This step upda "license": "MIT", "private": true, "dependencies": { - "angular": "1.4.x", - "angular-mocks": "1.4.x", + "angular": "1.5.x", + "angular-mocks": "1.5.x", "jquery": "~2.1.1", "bootstrap": "~3.1.1", - "angular-route": "1.4.x" + "angular-route": "1.5.x" } } ``` -The new dependency `"angular-route": "1.4.x"` tells bower to install a version of the -angular-route component that is compatible with version 1.4.x. We must tell bower to download +The new dependency `"angular-route": "1.5.x"` tells bower to install a version of the +angular-route component that is compatible with version 1.5.x. We must tell bower to download and install this dependency. If you have bower installed globally, then you can run `bower install` but for this project, we have @@ -54,22 +54,21 @@ npm install ``` -## Multiple Views, Routing and Layout Template +## Multiple Components, 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 +a single component (the list of all phones), and all of the template code was located in the +`phone-list.html` file. The next step in building the app is to add a component 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. +To add the detailed component, 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 {@link ngRoute.$routeProvider $routeProvider}, which is the provider of the {@link ngRoute.$route $route service}. This service makes it easy to -wire together controllers, view templates, and the current URL location in the browser. Using this +wire together components, view templates, and the current URL location in the browser. Using this feature, we can implement [deep linking](http://en.wikipedia.org/wiki/Deep_linking), which lets us utilize the browser's history (back and forward navigation) and bookmarks. @@ -129,7 +128,7 @@ __`app/index.html`:__ - + @@ -145,41 +144,8 @@ application: - `angular-route.js` : defines the Angular `ngRoute` module, which provides us with routing. - `app.js` : this file now holds the root module of our application. -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`:__ - -```html -
            -
            -
            - - - Search: - Sort by: - - -
            -
            - - - - -
            -
            -
            -``` +Note that we removed the `` line from the `index.html` template and +replaced it with a single line containing a div with the `ng-view` attribute.
            TODO! @@ -191,18 +157,18 @@ We also added a placeholder template for the phone details view: __`app/partials/phone-detail.html`:__ ```html -TBD: detail view for {{phoneId}} +TBD: detail view for {{$ctrl.phoneId}} ``` -Note how we are using the `phoneId` expression which will be defined in the `PhoneDetailCtrl` controller. +Note how we are using the `$ctrl.phoneId` expression which will be defined in the controller of the `phoneDetail` component, `PhoneDetailCtrl`. ## The App Module To improve the organization of the app, we are making use of Angular's `ngRoute` module and we've -moved the controllers into their own module `phonecatControllers` (as shown below). +moved the components into their own module `phonecatComponents` (as shown below). -We added `angular-route.js` to `index.html` and created a new `phonecatControllers` module in -`controllers.js`. That's not all we need to do to be able to use their code, however. We also have +We added `angular-route.js` to `index.html` and created a new `phonecatComponents` module in +`components.js`. That's not all we need to do to be able to use their code, however. We also have to add the modules as dependencies of our app. By listing these two modules as dependencies of `phonecatApp`, we can use the directives and services they provide. @@ -212,13 +178,13 @@ __`app/js/app.js`:__ ```js var phonecatApp = angular.module('phonecatApp', [ 'ngRoute', - 'phonecatControllers' + 'phonecatComponents' ]); ... ``` -Notice the second argument passed to `angular.module`, `['ngRoute', 'phonecatControllers']`. This +Notice the second argument passed to `angular.module`, `['ngRoute', 'phonecatComponents']`. This array lists the modules that `phonecatApp` depends on. @@ -229,12 +195,10 @@ phonecatApp.config(['$routeProvider', function($routeProvider) { $routeProvider. when('/phones', { - templateUrl: 'partials/phone-list.html', - controller: 'PhoneListCtrl' + template: '' }). when('/phones/:phoneId', { - templateUrl: 'partials/phone-detail.html', - controller: 'PhoneDetailCtrl' + template: '' }). otherwise({ redirectTo: '/phones' @@ -249,20 +213,20 @@ define our routes. Our application routes are defined as follows: * `when('/phones')`: 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. + construct this view, Angular will use the `` template, which instantiates the `phoneList` component. Note that this is the same markup that we had in + the `index.html` file earlier. * `when('/phones/:phoneId')`: The phone details view will be shown when the URL hash fragment matches '/phones/:phoneId', where `:phoneId` is a variable part of the URL. To construct the phone - details view, Angular will use the `phone-detail.html` template and the `PhoneDetailCtrl` - controller. + details view, Angular will use the `` template, which instantiates + the `phoneDetail` component. * `otherwise({redirectTo: '/phones'})`: triggers a redirection to `/phones` when the browser address doesn't match either of our routes. -We reused the `PhoneListCtrl` controller that we constructed in previous steps and we added a new, -empty `PhoneDetailCtrl` controller to the `app/js/controllers.js` file for the phone details view. +We reused the `phoneList` component that we constructed in previous steps and we added a new, +empty `phoneDetail` component to the `app/js/components.js` file for the phone details view. Note the use of the `:phoneId` parameter in the second route declaration. The `$route` service uses @@ -271,36 +235,45 @@ URL. All variables defined with the `:` notation are extracted into the {@link ngRoute.$routeParams `$routeParams`} object. -## Controllers +## Components and Controllers -__`app/js/controllers.js`:__ +__`app/js/components.js`:__ ```js -var phonecatControllers = angular.module('phonecatControllers', []); +var phonecatComponents = angular.module('phonecatComponents', []); -phonecatControllers.controller('PhoneListCtrl', ['$scope', '$http', - function ($scope, $http) { - $http.get('phones/phones.json').success(function(data) { - $scope.phones = data; - }); +phonecatComponents.component('phoneList', { + controller: 'PhoneListCtrl', + templateUrl: 'partials/phone-list.html' +}).controller('PhoneListCtrl', ['$http', function ($http) { + var ctrl = this; - $scope.orderProp = 'age'; - }]); + $http.get('phones/phones.json').success(function(data) { + ctrl.phones = data; + }); + + ctrl.orderProp = 'age'; +}]); -phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', - function($scope, $routeParams) { - $scope.phoneId = $routeParams.phoneId; + +phonecatComponents.component('phoneDetail', { + controller: 'PhoneDetailCtrl', + templateUrl: 'partials/phone-detail.html' +}).controller('PhoneDetailCtrl', ['$routeParams', + function($routeParams) { + this.phoneId = $routeParams.phoneId; }]); + ``` -Again, note that we created a new module called `phonecatControllers`. For small AngularJS -applications, it's common to create just one module for all of your controllers if there are just a +Again, note that we created a new module called `phonecatComponents`. For small AngularJS +applications, it's common to create just one module for all of your components if there are just a few. As your application grows, it is quite common to refactor your code into additional modules. For larger apps, you will probably want to create separate modules for each major feature of -your app. +your app, as we'll see in the last chapter of this tutorial. -Because our example app is relatively small, we'll just add all of our controllers to the -`phonecatControllers` module. +Because our example app is relatively small, we'll just add all of our components to the +`phonecatComponents` module. ## Test @@ -331,7 +304,7 @@ to various URLs and verify that the correct view was rendered. it('should display placeholder page with phoneId', function() { - expect(element(by.binding('phoneId')).getText()).toBe('nexus-s'); + expect(element(by.binding('$ctrl.phoneId')).getText()).toBe('nexus-s'); }); }); ``` @@ -342,10 +315,10 @@ You can now rerun `npm run protractor` to see the tests run. # Experiments -* Try to add an `{{orderProp}}` binding to `index.html`, and you'll see that nothing happens even +* Try to add an `{{$ctrl.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 `
            ` element. If you add -the same binding into the `phone-list.html` template, the binding will work as expected. +scope managed by the `phoneList` components, which is associated with the `
            ` element. +If you add the same binding into the `phone-list.html` template, the binding will work as expected.
            * In `PhoneCatCtrl`, create a new model called "`hero`" with `this.hero = 'Zoro'`. In diff --git a/docs/content/tutorial/step_08.ngdoc b/docs/content/tutorial/step_08.ngdoc index 6e5ef9abd8f7..9a11c93d3ddd 100644 --- a/docs/content/tutorial/step_08.ngdoc +++ b/docs/content/tutorial/step_08.ngdoc @@ -51,20 +51,24 @@ Each of these files describes various properties of the phone using the same dat show this data in the phone detail view. -## Controller +## Component and 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`:__ +__`app/js/components.js`:__ ```js -var phonecatControllers = angular.module('phonecatControllers',[]); - -phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http', - function($scope, $routeParams, $http) { +var phonecatComponents = angular.module('phonecatComponents',[]); + +phonecatComponents.component('phoneDetail', { + controller: 'PhoneDetailCtrl', + templateUrl: 'partials/phone-detail.html' +}).controller('PhoneDetailCtrl', ['$routeParams', '$http', + function($routeParams, $http) { + var ctrl = this; $http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) { - $scope.phone = data; + ctrl.phone = data; }); }]); ``` @@ -83,14 +87,14 @@ our model into the view. __`app/partials/phone-detail.html`:__ ```html - + -

            {{phone.name}}

            +

            {{$ctrl.phone.name}}

            -

            {{phone.description}}

            +

            {{$ctrl.phone.description}}

              -
            • +
            @@ -100,13 +104,13 @@ __`app/partials/phone-detail.html`:__ Availability and Networks
            Availability
            -
            {{availability}}
            +
            {{availability}}
            ...
          • Additional Features -
            {{phone.additionalFeatures}}
            +
            {{$ctrl.phone.additionalFeatures}}
          ``` @@ -130,23 +134,22 @@ __`test/unit/controllersSpec.js`:__ ... describe('PhoneDetailCtrl', function(){ - var scope, $httpBackend, ctrl; + var $httpBackend, ctrl; - beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) { + beforeEach(inject(function(_$httpBackend_, $routeParams, $controller) { $httpBackend = _$httpBackend_; $httpBackend.expectGET('phones/xyz.json').respond({name:'phone xyz'}); $routeParams.phoneId = 'xyz'; - scope = $rootScope.$new(); - ctrl = $controller('PhoneDetailCtrl', {$scope: scope}); + ctrl = $controller('PhoneDetailCtrl'); })); it('should fetch phone detail', function() { - expect(scope.phone).toBeUndefined(); + expect(ctrl.phone).toBeUndefined(); $httpBackend.flush(); - expect(scope.phone).toEqual({name:'phone xyz'}); + expect(ctrl.phone).toEqual({name:'phone xyz'}); }); }); ... @@ -172,7 +175,7 @@ __`test/e2e/scenarios.js`:__ it('should display nexus-s page', function() { - expect(element(by.binding('phone.name')).getText()).toBe('Nexus S'); + expect(element(by.binding('$ctrl.phone.name')).getText()).toBe('Nexus S'); }); }); ... diff --git a/docs/content/tutorial/step_09.ngdoc b/docs/content/tutorial/step_09.ngdoc index a0984e49adf1..187975352674 100644 --- a/docs/content/tutorial/step_09.ngdoc +++ b/docs/content/tutorial/step_09.ngdoc @@ -39,7 +39,7 @@ __`app/js/app.js`:__ ```js ... -angular.module('phonecatApp', ['ngRoute','phonecatControllers','phonecatFilters']); +angular.module('phonecatApp', ['ngRoute','phonecatComponents','phonecatFilters']); ... ``` @@ -53,7 +53,7 @@ __`app/index.html`:__ ```html ... - + ... ``` @@ -72,9 +72,9 @@ __`app/partials/phone-detail.html`:__ ...
          Infrared
          -
          {{phone.connectivity.infrared | checkmark}}
          +
          {{$ctrl.phone.connectivity.infrared | checkmark}}
          GPS
          -
          {{phone.connectivity.gps | checkmark}}
          +
          {{$ctrl.phone.connectivity.gps | checkmark}}
          ... ``` @@ -82,7 +82,7 @@ __`app/partials/phone-detail.html`:__ ## Test -Filters, like any other component, should be tested and these tests are very easy to write. +Filters, like any other code, should be tested and these tests are very easy to write. __`test/unit/filtersSpec.js`:__ diff --git a/docs/content/tutorial/step_10.ngdoc b/docs/content/tutorial/step_10.ngdoc index fd073a5c9d41..fdb364a83904 100644 --- a/docs/content/tutorial/step_10.ngdoc +++ b/docs/content/tutorial/step_10.ngdoc @@ -14,23 +14,27 @@ clicking on the desired thumbnail image. Let's have a look at how we can do this
          -## Controller +## Component and Controller -__`app/js/controllers.js`:__ +__`app/js/components.js`:__ ```js ... -var phonecatControllers = angular.module('phonecatControllers',[]); - -phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http', - function($scope, $routeParams, $http) { +var phonecatComponents = angular.module('phonecatComponents', []); + +phonecatComponents.component('phoneDetail', { + controller: 'PhoneDetailCtrl', + templateUrl: 'partials/phone-detail.html' +}).controller('PhoneDetailCtrl', ['$routeParams', '$http', + function($routeParams, $http) { + var ctrl = this; $http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) { - $scope.phone = data; - $scope.mainImageUrl = data.images[0]; + ctrl.phone = data; + ctrl.mainImageUrl = data.images[0]; }); - $scope.setImage = function(imageUrl) { - $scope.mainImageUrl = imageUrl; + ctrl.setImage = function(imageUrl) { + ctrl.mainImageUrl = imageUrl; }; }]); ``` @@ -38,7 +42,7 @@ phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$h 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`. +We also created a `setImage` event handler method that will change the value of `mainImageUrl`. ## Template @@ -46,23 +50,23 @@ We also created a `setImage` event handler function that will change the value o __`app/partials/phone-detail.html`:__ ```html - + ...
            -
          • - +
          • +
          ... ``` -We bound the `ngSrc` directive of the large image to the `mainImageUrl` property. +We bound the `ngSrc` directive of the large image to the `$ctrl.mainImageUrl` property. We also registered an {@link ng.directive:ngClick `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 +use the `$ctrl.setImage` event handler method to change the value of the `$ctrl.mainImageUrl` property to the URL of the thumbnail image.
          @@ -115,7 +119,7 @@ __`test/unit/controllersSpec.js`:__ ... describe('PhoneDetailCtrl', function(){ - var scope, $httpBackend, ctrl, + var $httpBackend, ctrl, xyzPhoneData = function() { return { name: 'phone xyz', @@ -124,21 +128,20 @@ __`test/unit/controllersSpec.js`:__ }; - beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) { + beforeEach(inject(function(_$httpBackend_, $routeParams, $controller) { $httpBackend = _$httpBackend_; $httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData()); $routeParams.phoneId = 'xyz'; - scope = $rootScope.$new(); - ctrl = $controller('PhoneDetailCtrl', {$scope: scope}); + ctrl = $controller('PhoneDetailCtrl'); })); it('should fetch phone detail', function() { - expect(scope.phone).toBeUndefined(); + expect(ctrl.phone).toBeUndefined(); $httpBackend.flush(); - expect(scope.phone).toEqual(xyzPhoneData()); + expect(ctrl.phone).toEqual(xyzPhoneData()); }); }); ``` @@ -150,13 +153,13 @@ Your unit tests should now be passing. * Let's add a new controller method to `PhoneDetailCtrl`: - $scope.hello = function(name) { + ctrl.hello = function(name) { alert('Hello ' + (name || 'world') + '!'); } and add: - + to the `phone-detail.html` template. diff --git a/docs/content/tutorial/step_11.ngdoc b/docs/content/tutorial/step_11.ngdoc index 946a7185ecd0..e40ef7363a56 100644 --- a/docs/content/tutorial/step_11.ngdoc +++ b/docs/content/tutorial/step_11.ngdoc @@ -32,18 +32,18 @@ We are using [Bower][bower] to install client side dependencies. This step upda "license": "MIT", "private": true, "dependencies": { - "angular": "1.4.x", - "angular-mocks": "1.4.x", + "angular": "1.5.x", + "angular-mocks": "1.5.x", "jquery": "~2.1.1", "bootstrap": "~3.1.1", - "angular-route": "1.4.x", - "angular-resource": "1.4.x" + "angular-route": "1.5.x", + "angular-resource": "1.5.x" } } ``` -The new dependency `"angular-resource": "1.4.x"` tells bower to install a version of the -angular-resource component that is compatible with version 1.4.x. We must ask bower to download +The new dependency `"angular-resource": "1.5.x"` tells bower to install a version of the +angular-resource component that is compatible with version 1.5.x. We must ask bower to download and install this dependency. We can do this by running: ``` @@ -108,16 +108,16 @@ __`app/js/app.js`.__ ```js ... -angular.module('phonecatApp', ['ngRoute', 'phonecatControllers','phonecatFilters', 'phonecatServices']). +angular.module('phonecatApp', ['ngRoute', 'phonecatComponents','phonecatFilters', 'phonecatServices']). ... ``` We need to add the 'phonecatServices' module dependency to 'phonecatApp' module's requires array. -## Controller +## Component and Controller -We simplified our sub-controllers (`PhoneListCtrl` and `PhoneDetailCtrl`) by factoring out the +We simplified our component controllers (`PhoneListCtrl` and `PhoneDetailCtrl`) by factoring out the lower-level {@link ng.$http $http} service, replacing it with a new service called `Phone`. Angular's {@link ngResource.$resource `$resource`} service is easier to use than `$http` for interacting with data sources exposed as RESTful resources. It is also easier @@ -126,35 +126,42 @@ now to understand what the code in our controllers is doing. __`app/js/controllers.js`.__ ```js -var phonecatControllers = angular.module('phonecatControllers', []); - -... - -phonecatControllers.controller('PhoneListCtrl', ['$scope', 'Phone', function($scope, Phone) { - $scope.phones = Phone.query(); - $scope.orderProp = 'age'; +var phonecatComponents = angular.module('phonecatComponents', []); + +phonecatComponents.component('phoneList', { + controller: 'PhoneListCtrl', + templateUrl: 'partials/phone-list.html' +}).controller('PhoneListCtrl', ['Phone', function (Phone) { + this.phones = Phone.query(); + this.orderProp = 'age'; }]); -phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', 'Phone', function($scope, $routeParams, Phone) { - $scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) { - $scope.mainImageUrl = phone.images[0]; - }); - $scope.setImage = function(imageUrl) { - $scope.mainImageUrl = imageUrl; - } -}]); +phonecatComponents.component('phoneDetail', { + controller: 'PhoneDetailCtrl', + templateUrl: 'partials/phone-detail.html' +}).controller('PhoneDetailCtrl', ['$routeParams', 'Phone', + function($routeParams, Phone) { + var ctrl = this; + ctrl.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) { + ctrl.mainImageUrl = phone.images[0]; + }); + + ctrl.setImage = function(imageUrl) { + ctrl.mainImageUrl = imageUrl; + }; + }]); ``` Notice how in `PhoneListCtrl` we replaced: $http.get('phones/phones.json').success(function(data) { - $scope.phones = data; + ctrl.phones = data; }); with: - $scope.phones = Phone.query(); + this.phones = Phone.query(); This is a simple statement that we want to query for all phones. @@ -195,9 +202,9 @@ service correctly. The {@link ngResource.$resource $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][jasmine-matchers]. When -the `toEqualData` matcher compares two objects, it takes only object properties into account and -ignores methods. +solve the problem, we instruct Jasmine to use a [custom equality tester][jasmine-equality] +to compare two objects. It takes only object properties into account and ignores properties +added by the `$resource` service. __`test/unit/controllersSpec.js`:__ @@ -206,72 +213,13 @@ __`test/unit/controllersSpec.js`:__ describe('PhoneCat controllers', function() { beforeEach(function(){ - this.addMatchers({ - toEqualData: function(expected) { - return angular.equals(this.actual, expected); - } - }); + jasmine.addCustomEqualityTester(angular.equals); }); beforeEach(module('phonecatApp')); - beforeEach(module('phonecatServices')); - - - describe('PhoneListCtrl', function(){ - var scope, ctrl, $httpBackend; - - beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) { - $httpBackend = _$httpBackend_; - $httpBackend.expectGET('phones/phones.json'). - respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]); - - scope = $rootScope.$new(); - ctrl = $controller('PhoneListCtrl', {$scope: scope}); - })); - - it('should create "phones" model with 2 phones fetched from xhr', function() { - expect(scope.phones).toEqualData([]); - $httpBackend.flush(); + // ... - expect(scope.phones).toEqualData( - [{name: 'Nexus S'}, {name: 'Motorola DROID'}]); - }); - - - it('should set the default value of orderProp model', function() { - expect(scope.orderProp).toBe('age'); - }); - }); - - - describe('PhoneDetailCtrl', function(){ - var scope, $httpBackend, ctrl, - xyzPhoneData = function() { - return { - name: 'phone xyz', - images: ['image/url1.png', 'image/url2.png'] - } - }; - - - beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) { - $httpBackend = _$httpBackend_; - $httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData()); - - $routeParams.phoneId = 'xyz'; - scope = $rootScope.$new(); - ctrl = $controller('PhoneDetailCtrl', {$scope: scope}); - })); - - - it('should fetch phone detail', function() { - expect(scope.phone).toEqualData({}); - $httpBackend.flush(); - - expect(scope.phone).toEqualData(xyzPhoneData()); - }); - }); }); ``` @@ -282,12 +230,13 @@ You should now see the following output in the Karma tab: # Summary -Now that we've seen how to build a custom service as a RESTful client, we're ready for {@link step_12 step 12} (the last step!) to -learn how to improve this application with animations. +Now that we've seen how to build a custom service as a RESTful client, we're +ready for {@link step_12 step 12} to learn how to improve this application with +animations.
            [restful]: http://en.wikipedia.org/wiki/Representational_State_Transfer -[jasmine-matchers]: http://jasmine.github.io/1.3/introduction.html#section-Matchers +[jasmine-equality]: http://jasmine.github.io/2.4/custom_equality.html [bower]: http://bower.io/ diff --git a/docs/content/tutorial/step_12.ngdoc b/docs/content/tutorial/step_12.ngdoc index 79b3233b78a3..9445b88543e3 100644 --- a/docs/content/tutorial/step_12.ngdoc +++ b/docs/content/tutorial/step_12.ngdoc @@ -6,7 +6,7 @@
              -In this final step, we will enhance our phonecat web application by attaching CSS and JavaScript +In this step, we will enhance our phonecat web application by attaching CSS and JavaScript animations on top of the template code we created before. * We now use the `ngAnimate` module to enable animations throughout the application. @@ -36,19 +36,19 @@ We are using [Bower][bower] to install client side dependencies. This step upda "license": "MIT", "private": true, "dependencies": { - "angular": "1.4.x", - "angular-mocks": "1.4.x", + "angular": "1.5.x", + "angular-mocks": "1.5.x", "jquery": "~2.1.1", "bootstrap": "~3.1.1", - "angular-route": "1.4.x", - "angular-resource": "1.4.x", - "angular-animate": "1.4.x" + "angular-route": "1.5.x", + "angular-resource": "1.5.x", + "angular-animate": "1.5.x" } } ``` -* `"angular-animate": "1.4.x"` tells bower to install a version of the -angular-animate component that is compatible with version 1.4.x. +* `"angular-animate": "1.5.x"` tells bower to install a version of the +angular-animate component that is compatible with version 1.5.x. * `"jquery": "~2.1.1"` tells bower to install the 2.1.1 version of jQuery. Note that this is not an Angular library, it is the standard jQuery library. We can use bower to install a wide range of 3rd party libraries. @@ -111,7 +111,7 @@ __`app/index.html`.__ ```
              - **Important:** Be sure to use jQuery version 2.1 or newer when using Angular 1.4; jQuery 1.x is + **Important:** Be sure to use jQuery version 2.1 or newer when using Angular 1.5; jQuery 1.x is not officially supported. Be sure to load jQuery before all AngularJS scripts, otherwise AngularJS won't detect jQuery and animations will not work as expected. @@ -142,7 +142,7 @@ angular.module('phonecatApp', [ 'ngRoute', 'phonecatAnimations', - 'phonecatControllers', + 'phonecatComponents', 'phonecatFilters', 'phonecatServices', ]); @@ -165,9 +165,11 @@ __`app/partials/phone-list.html`.__ which we will later use for animations: -->
                -
              • - + + {{phone.name}} + {{phone.name}}

                {{phone.snippet}}

              • @@ -379,17 +381,17 @@ __`app/partials/phone-detail.html`.__
                + ng-repeat="img in $ctrl.phone.images" + ng-class="{active:$ctrl.mainImageUrl==img}">
                -

                {{phone.name}}

                +

                {{$ctrl.phone.name}}

                -

                {{phone.description}}

                +

                {{$ctrl.phone.description}}

                  -
                • - +
                • +
                ``` @@ -530,8 +532,8 @@ do any cleanup necessary for when the animation finishes. # Summary -There you have it! We have created a web app in a relatively short amount of time. In the {@link -the_end closing notes} we'll cover where to go from here. +Our application is now much more pleasant to use, thanks to the smooth transitions between pages +and UI states. We're ready to move on to {@link step_13 step 13} where we'll prepare the code for future expansion.
                  diff --git a/docs/content/tutorial/step_13.ngdoc b/docs/content/tutorial/step_13.ngdoc new file mode 100644 index 000000000000..7db46dab1a79 --- /dev/null +++ b/docs/content/tutorial/step_13.ngdoc @@ -0,0 +1,435 @@ +@ngdoc tutorial +@name 13 - Style Guide & Feature Folders +@step 13 +@description + +
                    + +In this final step, we will reorganize our application to prepare it for future development. + +The way we have organized code so far is to put all components in one file, all filters in +another file, all services in a third file, and so on. This has worked relatively well, +but if we plan to expand the application and maintain it for an extended period of time, +it is likely to cause maintenance issues. If all components are in one file, the file is +likely to get very large and it'll be difficult to navigate and find the code you're looking +for. + +We can future-proof our code organization by applying a couple of tricks: + +* We'll organize our code by *feature area* instead of by function. +* We'll put each part of the application in its own file, where it can be easily found. + +These principles are explained in great detail in the [Angular Style Guide][styleguide], +which also contains many more techniques for effectively organizing Angular codebases. + +
                    + +## Core Module and Services + +We now have a `core` subdirectory under `js` that contains common services that are used all across +the application. At the moment we have two such services in particular: The `Phone` factory +that currently resides in `services.js` and the `checkmark` filter that resides in `filters.js`. + +Inside the `core` folder there is a file that introduces a `phonecat.core` module: + +__`app/js/core/core.module.js`.__ + +```js +'use strict'; + +angular.module('phonecat.core', ['ngResource']); +``` + +Since the `Phone` factory uses `ngResource`, it is defined as a dependency here. + +Also inside the `core` folder, there is a `phone.factory.js` file. This defines +the `Phones` factory that was previously in `services.js` (which has been removed). +The factory is added into the `phonecat.core` module: + +__`app/js/core/phone.factory.js`.__ + +```js +'use strict'; + +angular.module('phonecat.core') + .factory('Phone', ['$resource', + function($resource) { + return $resource('phones/:phoneId.json', {}, { + query: {method:'GET', params:{phoneId:'phones'}, isArray:true} + }); + }]); +``` + +The third file under `core` is `checkmark.filter.js`. It registers the `checkmark` +filter and replaces the old `filters.js` file. + +__`app/js/core/checkmark.filter.js`.__ + +```js +'use strict'; + +angular.module('phonecat.core').filter('checkmark', function() { + return function(input) { + return input ? '\u2713' : '\u2718'; + }; +}); +``` + +## Phone List Component Module + +In the new `phone_list` subdirectory there are four files: + +* `phone_list.module.js` +* `phone_list.component.js` +* `phone_list.controller.js` +* `phone_list.template.html` + +These are all the constituent parts that make up the `phoneList` component, conveniently +located in the same place and named consistently. + +The `phone_list.template.html` file is the same template that the component has previously, +moved here from the `partials` directory. + +The module file introduces the component module. It has a dependency to the `phonecat.core` +module because the component needs the `Phone` service from that module: + +__`app/js/phone_list/phone_list.module.js`.__ + +```js +'use strict'; + +angular.module('phonecat.phoneList', ['phonecat.core']); +``` + +The component file adds the component definition to the module. This was previously +in `components.js`: + +__`app/js/phone_list/phone_list.component.js`.__ + +```js +'use strict'; + +angular.module('phonecat.phoneList') + .component('phoneList', { + controller: 'PhoneListCtrl', + templateUrl: 'js/phone_list/phone_list.html' + }); +``` + +Note that we've changed the value of the `templateUrl` attribute because of the changed +location of the component template. + +The controller file adds the component controller. This also was previously in +`components.js`: + +__`app/js/phone_list/phone_list.component.js`.__ + +```js +'use strict'; + +angular.module('phonecat.phoneList').controller('PhoneListCtrl', + ['Phone', function (Phone) { + this.phones = Phone.query(); + this.orderProp = 'age'; + }]); +``` + +## Phone Detail Component Module + +In the new `phone_detail` subdirectory we have five files: + +* `phone_detail.module.js` +* `phone_detail.component.js` +* `phone_detail.controller.js` +* `phone_detail.template.html` +* `phone.animation.js` + +These are the parts that make up the `phoneDetail` component, augmented by the +JavaScript animation that we are using in that component. + +Again, the template file `phone_detail.template.html` is just the existing +template, moved here from the `partials` directory. + +The module file introduces the module. It depends on `phonecat.core` for the +`Phone` service, on `ngAnimate` for the JavaScript animation, and on `ngRoute` +for the `$routeParams` dependency used in the controller. + +__`app/js/phone_detail/phone_detail.module.js`.__ + +```js +'use strict'; + +angular.module('phonecat.phoneDetail', [ + 'ngAnimate', + 'ngRoute', + 'phonecat.core' +]); +``` + +The component file registers the `phoneDetail` component definition: + +__`app/js/phone_detail/phone_detail.component.js`.__ + +```js +'use strict'; + +angular.module('phonecat.phoneDetail').component('phoneDetail', { + controller: 'PhoneDetailCtrl', + templateUrl: 'js/phone_detail/phone_detail.html' + }); +``` + +Here also we have changed the component's `templateUrl` to point to this +new component directory. + +The controller file registers the component controller: + +__`app/js/phone_detail/phone_detail.controller.js`.__ + +```js +'use strict'; + +angular.module('phonecat.phoneDetail').controller('PhoneDetailCtrl', + ['$routeParams', 'Phone', function($routeParams, Phone) { + var ctrl = this; + ctrl.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) { + ctrl.mainImageUrl = phone.images[0]; + }); + + ctrl.setImage = function(imageUrl) { + ctrl.mainImageUrl = imageUrl; + }; + }]); +``` + +The `phone.animation.js` file registers the JavaScript animation. The code +here is the same as what used to be in `animations.js`, the only difference +being the Angular module into which the animation is registered. It is now +in the phone detail component module: + +__`app/js/phone_detail/phone.animation.js`.__ + +```js +'use strict'; + +angular.module('phonecat.phoneDetail').animation('.phone', function() { + + var animateUp = function(element, className, done) { + // ... + } + + var animateDown = function(element, className, done) { + // ... + } + + return { + addClass: animateUp, + removeClass: animateDown + }; +}); +``` + +## Main module and `index.html` + +Since the module structure of the application has changed, the dependencies of +the main application module have changed as well. It no longer requires +functional area modules like `phonecatServices` or `phonecatComponents`. +Instead it requires the new feature area modules we have introduced. Also, to match +our new file naming scheme, the name of the main module file has been renamed +from `app.js` to `app.module.js`: + +__`app/js/app.module.js`.__ + +```js +var phonecatApp = angular.module('phonecatApp', [ + 'ngRoute', + 'phonecat.core', + 'phonecat.phoneList', + 'phonecat.phoneDetail' +]); +``` + +Also, in `index.html` the loaded files have changed. We need to replace the +previous ` + + + + + + + + + + +``` + +## Tests + +Our E2E Protractor test suite does not require any changes at this point. That's because +it just uses the application UI and does not care how it's structured internally. + +Unit tests, on the other hand, need some work. We'll want to organize our unit test code +so that it matches our application code as closely as possible. That means we want to have +the tests of each service, filter, and controller in its own file, whose name matches the +application code file. + +`filtersSpec.js` has been renamed to `checkmark.filter.spec.js`. It only loads the core +module and only tests the checkmark filter: + +__`test/unit/checkmark.filter.spec.js`.__ + +```js +'use strict'; + +describe('checkmarkFilter', function() { + + beforeEach(module('phonecat.core')); + + it('should convert boolean values to unicode checkmark or cross', + inject(function(checkmarkFilter) { + expect(checkmarkFilter(true)).toBe('\u2713'); + expect(checkmarkFilter(false)).toBe('\u2718'); + })); +}); +``` + +Likewise, `servicesSpec.js` is now `phone.factory.spec.js`. It also only loads the core +module and only tests the phone factory: + +__`test/unit/phone.factory.spec.js`.__ + +```js +'use strict'; + +describe('Phone', function() { + + // load modules + beforeEach(module('phonecat.core')); + + // Test service availability + it('check the existence of Phone factory', inject(function(Phone) { + expect(Phone).toBeDefined(); + })); +}); +``` + +The `controllersSpec.js` file has been split into two: `phone_list.controller.spec.js` and +`phone_detail.controller.spec.js`. The list controller spec has the tests for that +controller. It only loads the phone list module: + +__`test/unit/phone_list.controller.spec.js`.__ + +```js +'use strict'; + +describe('PhoneListCtrl', function() { + + var ctrl, $httpBackend; + + beforeEach(function(){ + jasmine.addCustomEqualityTester(angular.equals); + }); + + beforeEach(module('phonecat.phoneList')); + + beforeEach(inject(function(_$httpBackend_, $controller) { + $httpBackend = _$httpBackend_; + $httpBackend.expectGET('phones/phones.json'). + respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]); + + ctrl = $controller('PhoneListCtrl'); + })); + + it('should create "phones" model with 2 phones fetched from xhr', function() { + expect(ctrl.phones).toEqual([]); + $httpBackend.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'); + }); + +}); +``` + +The phone detail spec file contains the rest of the tests that used to be in +`controllersSpec.js` - the ones for the phone details controller: + +__`test/unit/phone_detail.controller.spec.js`.__ + +```js +'use strict'; + +describe('PhoneDetailCtrl', function() { + + var $httpBackend, ctrl, + xyzPhoneData = function() { + return { + name: 'phone xyz', + images: ['image/url1.png', 'image/url2.png'] + } + }; + + beforeEach(function() { + jasmine.addCustomEqualityTester(angular.equals); + }); + + beforeEach(module('phonecat.phoneDetail')); + + beforeEach(inject(function(_$httpBackend_, $routeParams, $controller) { + $httpBackend = _$httpBackend_; + $httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData()); + + $routeParams.phoneId = 'xyz'; + ctrl = $controller('PhoneDetailCtrl'); + })); + + + it('should fetch phone detail', function() { + expect(ctrl.phone).toEqual({}); + $httpBackend.flush(); + + expect(ctrl.phone).toEqual(xyzPhoneData()); + }); +}); +``` + +The final change we need for our unit tests is to tweak the load order used +by Karma when it loads all the application files. It is important to load the +module file of each of our modules first, and only then load all the component +files that register things into that module. We can make this work easily because +of the file naming scheme we're using. We can instruct it to first load all +the application files that have `.module.js` in their name, and after that all +the ones that don't: + +__`test/unit/karma.conf.js`.__ + +```js +files : [ + 'app/bower_components/angular/angular.js', + 'app/bower_components/angular-route/angular-route.js', + 'app/bower_components/angular-resource/angular-resource.js', + 'app/bower_components/angular-animate/angular-animate.js', + 'app/bower_components/angular-mocks/angular-mocks.js', + 'app/js/**/*.module.js', + 'app/js/**/*.!(module).js', + 'test/unit/**/*.js' +], +``` + + +# Summary + +There you have it! We have created a web app in a relatively short amount of time and organized +it in a way that makes it very easy to extend in the future. In the {@link the_end closing notes} +we'll cover where to go from here. + +[styleguide]: https://github.com/johnpapa/angular-styleguide