步骤4——双向数据绑定

在这一步骤中,我们将为程序添加一个特性使得可以对手机的显示排序。这还需要我们为手机数据模型中添加一些新信息作为排序的依据,并以此写出转换器的处理让数据起作用使得程序达到预期效果。

  • 程序现在除了有一个搜索框还有一个下拉选择框,它允许选择数据排序的要求。

工作区切换到步骤4

直接用浏览器访问步骤4在线演示

大多数的重要改变都会列在下面,不过你也可以在GitHub看到完整的差异。

模板

app/index.html:

  1. Search: <input ng-model="query">
  2. Sort by:
  3. <select ng-model="orderProp">
  4. <option value="name">Alphabetical</option>
  5. <option value="age">Newest</option>
  6. </select>
  7. <ul class="phones">
  8. <li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
  9. <span>{{phone.name}}</span>
  10. <p>{{phone.snippet}}</p>
  11. </li>
  12. </ul>

这里只列出了在index.html模板中发生变化的部分,主要有:

  • 首先,我们添加了一个<select>的html元素,并命名为orderProp,这样我们就有了两个排序依据选择了。 双向数据绑定示意图
  • 然后我们链接了经过filter转换器过滤后的数据作为orderBy转换器处理的输入。orderBy转换器会依据规则对输入的数据(数组)排序,即输出一个排序后的新数据(数组形式)。

在这里,Angular在select元素和orderProp数据模型间创建了一个双向数据绑定,然后orderProp被用于orderBy转换器(过滤器)。

正如我们在步骤3中看到的,因为这样的数据绑定使得任何的数据变化都可以及时的反映到输出结果中(这里是通过下拉菜单对排序条件的改变),而且这一过程中没有特别指定复杂的DOM操作处理(没有专门写这方面的代码)就自动实现了效果,这就是Angular数据绑定的威力所在。

控制器

app/js/controllers.js:

  1. var phonecatApp = angular.module('phonecatApp', []);
  2. phonecatApp.controller('PhoneListCtrl', function ($scope) {
  3. $scope.phones = [
  4. {'name': 'Nexus S',
  5. 'snippet': 'Fast just got faster with Nexus S.',
  6. 'age': 1},
  7. {'name': 'Motorola XOOM™ with Wi-Fi',
  8. 'snippet': 'The Next, Next Generation tablet.',
  9. 'age': 2},
  10. {'name': 'MOTOROLA XOOM™',
  11. 'snippet': 'The Next, Next Generation tablet.',
  12. 'age': 3}
  13. ];
  14. $scope.orderProp = 'age';
  15. });
  • 我们编辑了phones的数据模型,即手机数组,在这个结构对每条记录中增加了age元素项,其可用于按手机推出时间排序。
  • 我们在控制器中设置了默认的值作为排序依据,即设置了orderProp的值为 age。如果不在这里设置这个值,则orderBy转换器(过滤器)是没有初始化的,直到我们在页面下拉菜单中进行了选择为止。

现在可以好好来谈谈双向数据绑定了。注意,当浏览器加载完成程序后下拉菜单的Newest项目是被选中的,这就是因为我们在控制器中设置了orderProp的排序依据是age,所以发生了数据模型向UI的绑定,现在我们在下拉菜单选择Alphabetically项,则数据模型马上会自动更新,让手机列表的排序依据要求发生变化,这时的数据绑定方向与前面恰好相反,是UI向数据模型的。

测试

这里的改变需要单元测试和端到端测试两个方面内容,先来看看单元测试。 test/unit/controllersSpec.js:

  1. describe('PhoneCat controllers', function() {
  2. describe('PhoneListCtrl', function(){
  3. var scope, ctrl;
  4. beforeEach(module('phonecatApp'));
  5. beforeEach(inject(function($controller) {
  6. scope = {};
  7. ctrl = $controller('PhoneListCtrl', {$scope:scope});
  8. }));
  9. it('should create "phones" model with 3 phones', function() {
  10. expect(scope.phones.length).toBe(3);
  11. });
  12. it('should set the default value of orderProp model', function() {
  13. expect(scope.orderProp).toBe('age');
  14. });
  15. });
  16. });

这个单元测试现在验证了默认排序的设置。

我们在beforeEach块内使用Jasmine的API来提取控制器内容,测试中这将在父级describe块内共享相关内容。

你现在应该能看到Karma输出类似下面信息:

  1. Chrome 22.0: Executed 2 of 2 SUCCESS (0.021 secs / 0.001 secs)

再看看端到端测试。 test/e2e/scenarios.js:

...
    it('should be possible to control phone order via the drop down select box', function() {

      var phoneNameColumn = element.all(by.repeater('phone in phones').column('{{phone.name}}'));
      var query = element(by.model('query'));

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

      query.sendKeys('tablet'); //let's narrow the dataset to make the test assertions shorter

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

      element(by.model('orderProp')).element(by.css('option[value="name"]')).click();

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

这里端到端测试验证下拉菜单的选择是否正确。我们现在可以运行npm run protractor来看看运行情况。

尝试

PhoneListCtrl控制器中移除掉orderProp值的设置,你就会在Angular模板加载时看到下拉菜单会有一个”unknown”(显示为空白)项目,而且列表也会以“未排序/原始定义顺序”来排列。

index.html模板添加一个`{{orderProp}}的绑定来显示当前的值。

反转排序只需要在排序值前面添加一个-号:

<option value="-age">Oldest</option>

小结

这一步我们为程序添加了排序功能,让我们进入步骤5,继续学习Angular的服务和依赖注入。