在這一步你會增加一個讓用戶控制手機列表顯示順序的特性。動態排序可以這樣實現,添加一個新的模型屬性,把它和迭代器集成起來,然后讓數據綁定完成剩下的事情。
請重置工作目錄:
~~~
git checkout -f step-4
~~~
你應該發現除了搜索框之外,你的應用多了一個下來菜單,它可以允許控制電話排列的順序。
步驟3和步驟4之間最重要的不同在下面列出。你可以在[GitHub](https://github.com/angular/angular-phonecat/compare/step-3...step-4)里看到完整的差別。
## 模板
app/index.html
~~~
Search: <input ng-model="query">
Sort by:
<select ng-model="orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>
<ul class="phones">
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
{{phone.name}}
<p>{{phone.snippet}}</p>
</li>
</ul>
~~~
我們在index.html中做了如下更改:
* 首先,我們增加了一個叫做`orderProp`的``標簽,這樣我們的用戶就可以選擇我們提供的兩種排序方法。

* 然后,在`filter`過濾器后面添加一個[orderBy](http://docs.angularjs.org/api/ng.filter:orderBy)過濾器用其來處理進入迭代器的數據。`orderBy`過濾器以一個數組作為輸入,復制一份副本,然后把副本重排序再輸出到迭代器。
AngularJS在`select`元素和`orderProp`模型之間創建了一個雙向綁定。而后,`orderProp`會被用作`orderBy`過濾器的輸入。
正如我們在步驟3中討論數據綁定和迭代器的時候所說的一樣,無論什么時候數據模型發生了改變(比如用戶在下拉菜單中選了不同的順序),AngularJS的數據綁定會讓視圖自動更新。沒有任何笨拙的DOM操作!
## 控制器
app/js/controllers.js:
~~~
function PhoneListCtrl($scope) {
$scope.phones = [
{"name": "Nexus S",
"snippet": "Fast just got faster with Nexus S.",
"age": 0},
{"name": "Motorola XOOM? with Wi-Fi",
"snippet": "The Next, Next Generation tablet.",
"age": 1},
{"name": "MOTOROLA XOOM?",
"snippet": "The Next, Next Generation tablet.",
"age": 2}
];
$scope.orderProp = 'age';
}
~~~
* 我們修改了`phones`模型—— 手機的數組 ——為每一個手機記錄其增加了一個`age`屬性。我們會根據`age`屬性來對手機進行排序。
* 我們在控制器代碼里加了一行讓`orderProp`的默認值為`age`。如果我們不設置默認值,這個模型會在我們的用戶在下拉菜單選擇一個順序之前一直處于未初始化狀態。
現在我們該好好談談雙向數據綁定了。注意到當應用在瀏覽器中加載時,“Newest”在下拉菜單中被選中。這是因為我們在控制器中把`orderProp`設置成了‘age’。所以綁定在從我們模型到用戶界面的方向上起作用——即數據從模型到視圖的綁定。現在當你在下拉菜單中選擇“Alphabetically”,數據模型會被同時更新,并且手機列表數組會被重新排序。這個時候數據綁定從另一個方向產生了作用——即數據從視圖到模型的綁定。
## 測試
我們所做的更改可以通過一個單元測試或者一個端到端測試來驗證正確性。我們首先來看看單元測試:
test/unit/controllersSpec.js:
~~~
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
var scope, ctrl;
beforeEach(function() {
scope = {},
ctrl = new PhoneListCtrl(scope);
});
it('should create "phones" model with 3 phones', function() {
expect(scope.phones.length).toBe(3);
});
it('should set the default value of orderProp model', function() {
expect(scope.orderProp).toBe('age');
});
});
});
~~~
單元測試現在驗證了默認值被正確設置。
我們使用Jasmine的接口把`PhoneListCtrl`控制器提取到一個`beforeEach`塊中,這個塊會被所有的父塊`describe`中的所有測試所共享。
運行這些單元測試,跟以前一樣,執行`./scripts/test.sh`腳本,你應該會看到如下輸出(注意:要在瀏覽器打開[http://localhost:9876](http://localhost:9876/)并進入嚴格模式,測試才會運行!):
~~~
Chrome: Runner reset.
..
Total 2 tests (Passed: 2; Fails: 0; Errors: 0) (3.00 ms)
Chrome 19.0.1084.36 Mac OS: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (3.00 ms)
~~~
現在我們把注意力轉移到端到端測試上來。
test/e2e/scenarios.js:
~~~
...
it('should be possible to control phone order via the drop down select box',
function() {
//let's narrow the dataset to make the test assertions shorter
input('query').enter('tablet');
expect(repeater('.phones li', 'Phone List').column('phone.name')).
toEqual(["Motorola XOOM\u2122 with Wi-Fi",
"MOTOROLA XOOM\u2122"]);
select('orderProp').option('Alphabetical');
expect(repeater('.phones li', 'Phone List').column('phone.name')).
toEqual(["MOTOROLA XOOM\u2122",
"Motorola XOOM\u2122 with Wi-Fi"]);
});
...
~~~
端到端測試驗證了選項框的排序機制是正確的。
你現在可以刷新你的瀏覽器,然后重新跑一遍端到端測試,或者你可以在[AngularJS的服務器](http://angular.github.com/angular-phonecat/step-4/test/e2e/runner.html)上運行一下。
## 練習
* 在`PhoneListCtrl`控制器中,把設置`orderProp`那條語句刪掉,你會看到AngularJS會在下拉菜單中臨時添加一個空白的選項,并且排序順序是默認排序(即未排序)。
* 在`index.html`模板里面添加一個`{{orderProp}}綁定來實時顯示它的值。
## 總結
現在你已經為你的應用提供了搜索功能,并且完整的進行了測試。[步驟5](a008)我們將學習AngularJS的服務以及AngularJS如何使用依賴注入。