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.
app/phone-list/phone-list.template.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.
We then chained the filter
filter with the 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 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!
app/phone-list/phone-list.component.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.
The changes we made should be verified with both a unit test and an E2E test. Let's look at the unit test first.
app/phone-list/phone-list.component.spec.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.
e2e-tests/scenarios.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.
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.
Now that you have added list sorting and tested the application, go to step 7 to learn about AngularJS services and how AngularJS uses dependency injection.