angular/angular.js

View on GitHub
docs/content/tutorial/step_06.ngdoc

Summary

Maintainability
Test Coverage
@ngdoc tutorial
@name 6 - Two-way Data Binding
@step 6
@description

<ul doc-tutorial-nav="6"></ul>


In this step, we will add a feature to let our 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.

* In addition to the search box, the application displays a drop-down menu that allows users to
  control the order in which the phones are listed.


<div doc-tutorial-reset="6"></div>


## Component Template

<br />
**`app/phone-list/phone-list.template.html`:**

```html
  <div class="container-fluid">
    <div class="row">
      <div class="col-md-2">
        <!--Sidebar content-->

        <p>
          Search:
          <input ng-model="$ctrl.query">
        </p>

        <p>
          Sort by:
          <select ng-model="$ctrl.orderProp">
            <option value="name">Alphabetical</option>
            <option value="age">Newest</option>
          </select>
        </p>

      </div>
      <div class="col-md-10">
        <!--Body content-->

        <ul class="phones">
          <li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp">
            <span>{{phone.name}}</span>
            <p>{{phone.snippet}}</p>
          </li>
        </ul>

      </div>
    </div>
  </div>
```

We made the following changes to the `phone-list.template.html` template:

* First, we added a `<select>` element bound to `$ctrl.orderProp`, so that our users can pick from
  the two provided sorting options.

  <img class="diagram" src="img/tutorial/tutorial_06.png">

* We then chained the `filter` filter with the {@link orderBy orderBy} filter to further process the
  input for the repeater. `orderBy` is a filter that takes an input array, copies it and reorders
  the copy which is then returned.

  AngularJS 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 {@link step_05 step 5},
whenever the model changes (for example because a user changes the order with the select drop-down
menu), AngularJS's data-binding will cause the view to automatically update. No bloated DOM
manipulation code is necessary!


## Component Controller

<br />
**`app/phone-list/phone-list.component.js`:**

```js
  angular.
    module('phoneList').
    component('phoneList', {
      templateUrl: 'phone-list/phone-list.template.html',
      controller: function PhoneListController() {
        this.phones = [
          {
            name: 'Nexus S',
            snippet: 'Fast just got faster with Nexus S.',
            age: 1
          }, {
            name: 'Motorola XOOM™ with Wi-Fi',
            snippet: 'The Next, Next Generation tablet.',
            age: 2
          }, {
            name: 'MOTOROLA XOOM™',
            snippet: 'The Next, Next Generation tablet.',
            age: 3
          }
        ];

        this.orderProp = 'age';
      }
    });
```

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

* We added a line to the controller that sets the default value of `orderProp` to `age`. If we had
  not set a default value here, the `orderBy` filter would remain uninitialized until the user
  picked an option from the drop-down menu.

This is a good time to talk about two-way data-binding. Notice that when the application 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.


## Testing

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

<br />
**`app/phone-list/phone-list.component.spec.js`:**

```js
describe('phoneList', function() {

  // Load the module that contains the `phoneList` component before each test
  beforeEach(module('phoneList'));

  // Test the controller
  describe('PhoneListController', function() {
    var ctrl;

    beforeEach(inject(function($componentController) {
      ctrl = $componentController('phoneList');
    }));

    it('should create a `phones` model with 3 phones', function() {
      expect(ctrl.phones.length).toBe(3);
    });

    it('should set a default value for the `orderProp` model', function() {
      expect(ctrl.orderProp).toBe('age');
    });

  });

});
```

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 Karma tab:

```
Chrome 49.0: Executed 2 of 2 SUCCESS (0.136 secs / 0.08 secs)
```

Let's turn our attention to the E2E tests.

<br />
**`e2e-tests/scenarios.js`:**

```js
  describe('PhoneCat Application', function() {

    describe('phoneList', function() {

      ...

      it('should be possible to control phone order via the drop-down menu', function() {
        var queryField = element(by.model('$ctrl.query'));
        var orderSelect = element(by.model('$ctrl.orderProp'));
        var nameOption = orderSelect.element(by.css('option[value="name"]'));
        var phoneNameColumn = element.all(by.repeater('phone in $ctrl.phones').column('phone.name'));

        function getNames() {
          return phoneNameColumn.map(function(elem) {
            return elem.getText();
          });
        }

        queryField.sendKeys('tablet');   // Let's narrow the dataset to make the assertions shorter

        expect(getNames()).toEqual([
          'Motorola XOOM\u2122 with Wi-Fi',
          'MOTOROLA XOOM\u2122'
        ]);

        nameOption.click();

        expect(getNames()).toEqual([
          'MOTOROLA XOOM\u2122',
          'Motorola XOOM\u2122 with Wi-Fi'
        ]);
      });

      ...
```

The E2E test verifies that the ordering mechanism of the select box is working correctly.

You can now rerun `npm run protractor` to see the tests run.


## Experiments

<div></div>

* In the `phoneList` component's controller, remove the statement that sets the `orderProp` value
  and you'll see that AngularJS will temporarily add a new blank ("unknown") option to the drop-down
  list and the ordering will default to unordered/natural order.

* Add a `{{$ctrl.orderProp}}` binding into the `phone-list.template.html` template to display its
  current value as text.

* Reverse the sort order by adding a `-` symbol before the sorting value:
  `<option value="-age">Oldest</option>`
  After making this change, you'll notice that the drop-down list has a blank option selected and does not default to age anymore.
  Fix this by updating the `orderProp` value in `phone-list.component.js` to match the new value on the `<option>` element.


## Summary

Now that you have added list sorting and tested the application, go to {@link step_07 step 7} to
learn about AngularJS services and how AngularJS uses dependency injection.


<ul doc-tutorial-nav="6"></ul>