docs/content/tutorial/step_06.ngdoc
@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>