2014-02-06 13:33:42 +00:00
|
|
|
@ngdoc tutorial
|
2016-03-27 21:32:36 +03:00
|
|
|
@name 6 - Two-way Data Binding
|
2014-02-06 13:33:42 +00:00
|
|
|
@step 6
|
2012-03-30 14:02:26 -07:00
|
|
|
@description
|
|
|
|
|
|
2012-04-28 22:45:28 -07:00
|
|
|
<ul doc-tutorial-nav="6"></ul>
|
2012-03-30 14:02:26 -07:00
|
|
|
|
|
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
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.
|
2012-03-30 14:02:26 -07:00
|
|
|
|
|
|
|
|
|
2012-04-28 22:45:28 -07:00
|
|
|
<div doc-tutorial-reset="6"></div>
|
2012-03-30 14:02:26 -07:00
|
|
|
|
|
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
## 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">
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
* 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.
|
|
|
|
|
|
2017-01-24 17:23:54 +00:00
|
|
|
AngularJS creates a two way data-binding between the select element and the `$ctrl.orderProp` model.
|
2016-03-27 21:32:36 +03:00
|
|
|
`$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
|
2017-01-24 17:23:54 +00:00
|
|
|
menu), AngularJS's data-binding will cause the view to automatically update. No bloated DOM
|
2016-03-27 21:32:36 +03:00
|
|
|
manipulation code is necessary!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Component Controller
|
|
|
|
|
|
|
|
|
|
<br />
|
2016-09-29 16:43:58 +02:00
|
|
|
**`app/phone-list/phone-list.component.js`:**
|
2014-02-06 14:02:18 +00:00
|
|
|
|
|
|
|
|
```js
|
2016-03-27 21:32:36 +03:00
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
});
|
2014-02-06 14:02:18 +00:00
|
|
|
```
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
* 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.
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
* 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.
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
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.
|
2014-02-06 14:02:18 +00:00
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
|
2018-02-27 17:33:42 +01:00
|
|
|
## Testing
|
2016-03-27 21:32:36 +03:00
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
});
|
2014-02-06 14:02:18 +00:00
|
|
|
```
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
The unit test now verifies that the default ordering property is set.
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
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.
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
You should now see the following output in the Karma tab:
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
```
|
|
|
|
|
Chrome 49.0: Executed 2 of 2 SUCCESS (0.136 secs / 0.08 secs)
|
|
|
|
|
```
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
Let's turn our attention to the E2E tests.
|
|
|
|
|
|
|
|
|
|
<br />
|
|
|
|
|
**`e2e-tests/scenarios.js`:**
|
2014-02-06 14:02:18 +00:00
|
|
|
|
|
|
|
|
```js
|
2016-03-27 21:32:36 +03:00
|
|
|
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'
|
|
|
|
|
]);
|
2014-04-01 11:13:22 -07:00
|
|
|
});
|
2016-03-27 21:32:36 +03:00
|
|
|
|
|
|
|
|
...
|
2014-02-06 14:02:18 +00:00
|
|
|
```
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
The E2E test verifies that the ordering mechanism of the select box is working correctly.
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2014-04-01 11:13:22 -07:00
|
|
|
You can now rerun `npm run protractor` to see the tests run.
|
2012-07-15 13:18:42 -03:00
|
|
|
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2018-02-27 17:33:42 +01:00
|
|
|
## Experiments
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
<div></div>
|
|
|
|
|
|
|
|
|
|
* In the `phoneList` component's controller, remove the statement that sets the `orderProp` value
|
2017-01-24 17:23:54 +00:00
|
|
|
and you'll see that AngularJS will temporarily add a new blank ("unknown") option to the drop-down
|
2016-03-27 21:32:36 +03:00
|
|
|
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.
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
* Reverse the sort order by adding a `-` symbol before the sorting value:
|
|
|
|
|
`<option value="-age">Oldest</option>`
|
2018-12-03 15:18:54 -08:00
|
|
|
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.
|
2012-07-15 13:18:42 -03:00
|
|
|
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2018-02-27 17:33:42 +01:00
|
|
|
## Summary
|
2012-03-30 14:02:26 -07:00
|
|
|
|
2016-03-27 21:32:36 +03:00
|
|
|
Now that you have added list sorting and tested the application, go to {@link step_07 step 7} to
|
2017-01-24 17:23:54 +00:00
|
|
|
learn about AngularJS services and how AngularJS uses dependency injection.
|
2012-03-30 14:02:26 -07:00
|
|
|
|
|
|
|
|
|
2012-04-28 22:45:28 -07:00
|
|
|
<ul doc-tutorial-nav="6"></ul>
|