AngularJS Phonecat (步骤10-步骤12)

导言


最近在学AngularJS的实例教程PhoneCat Tutorial App,发现网上的中文教程都比较久远,与英文版对应不上,而且缺少组件和文件重构两节。所以决定自己整理一个中文简明教程。

此篇为10-12节。

上一篇:AngularJS Phonecat (步骤8-步骤9)

10 更多模板


在这一步中,我们将实现手机详情视图,当用户手机列表上的某一项时显示。我们将使用的$ HTTP来获取我们的数据,并修改phoneDetail组件的模板。

数据

除了phoens.json,app/phones/文件也包含每款手机的JSON文件:

app/phones/nexus-s.json: (一个例子)

{
  "additionalFeatures": "Contour Display, Near Field Communications (NFC), ...",
  "android": {
    "os": "Android 2.3",
    "ui": "Android"
  },
  ...
  "images": [
    "img/phones/nexus-s.0.jpg",
    "img/phones/nexus-s.1.jpg",
    "img/phones/nexus-s.2.jpg",
    "img/phones/nexus-s.3.jpg"
  ],
  "storage": {
    "flash": "16384MB",
    "ram": "512MB"
  }
}

这些文件使用相同的数据结构描述手机的各种特性,我们要将这些信息显示到手机详情视图中。

组件控制器

我们利用$http服务请求JSON文件,来增强手机详情组件的控制器。这与手机列表控件控制器的工作原理相同。

app/phone-detail/phone-detail.component.js:

angular.
  module('phoneDetail').
  component('phoneDetail', {
    templateUrl: 'phone-detail/phone-detail.template.html',
    controller: ['$http', '$routeParams',
      function PhoneDetailController($http, $routeParams) {
        var self = this;

        $http.get('phones/' + $routeParams.phoneId + '.json').then(function(response) {
          self.phone = response.data;
        });
      }
    ]
  });

为了构建HTTP的URL请求,我们使用了$routeParams.phoneId,它是通过$route服务从当前路由中提取出来的。

组件模板

前面用占位符粗略定义的模板已经被替换成一个成熟的外部模板,该模板包含手机列表和手机详情的数据绑定。
注意,我们使用{{表达式}}和ngRepeat将数据模型中的手机信息传送到视图。

app/phone-detail/phone-detail.template.html:



{{$ctrl.phone.name}}

{{$ctrl.phone.description}}

  • Availability and Networks
    Availability
    {{availability}}
  • ...
  • Additional Features
    {{$ctrl.phone.additionalFeatures}}
AngularJS Phonecat (步骤10-步骤12)_第1张图片
tutorial_10.png

测试

我们编写了一个新的单元测试,与第7节中phoneList组件控制器的测试类似。

app/phone-detail/phone-detail.component.spec.js:

describe('phoneDetail', function() {

  // 在每次测试前,加载包含`phoneDetail`组件的功能
  beforeEach(module('phoneDetail'));

  // 测试控制器
  describe('PhoneDetailController', function() {
    var $httpBackend, ctrl;

    beforeEach(inject(function($componentController, _$httpBackend_, $routeParams) {
      $httpBackend = _$httpBackend_;
      $httpBackend.expectGET('phones/xyz.json').respond({name: 'phone xyz'});

      $routeParams.phoneId = 'xyz';

      ctrl = $componentController('phoneDetail');
    }));

    it('should fetch the phone details', function() {
      expect(ctrl.phone).toBeUndefined();

      $httpBackend.flush();
      expect(ctrl.phone).toEqual({name: 'phone xyz'});
    });

  });

});

我们也增加了一个端到端测试:导航到'Nexus S' 详情页,验证页面头部是否为"Nexus S"。

e2e-tests/scenarios.js

...

describe('View: Phone detail', function() {

  beforeEach(function() {
    browser.get('index.html#!/phones/nexus-s');
  });

  it('should display the `nexus-s` page', function() {
    expect(element(by.binding('$ctrl.phone.name')).getText()).toBe('Nexus S');
  });

});

...

命令行中输入npm run protractor既可运行。

11 自定义转换器


这一节,我们要创建一个自定义显示转换器。
上一节,详情页面直接用"true"和"false"来显示某个手机特性是否被支持,在这里,我们将定制一个转换器将字符转成图形,符号:✓ 对应 "true", ✘ 对应 "false"。

检查标识转换器

由于该转换器是通用的(不是只用于单个视图或组件),所以我们将它注册到覆盖应用程序范围的核心模块。

app/core/core.module.js:

angular.module('core', []);

app/core/checkmark/checkmark.filter.js:

angular.
  module('core').
  filter('checkmark', function() {
    return function(input) {
      return input ? '\u2713' : '\u2718';
    };
  });

我们的转换器叫做"checkmark",输入的值为trur或false。返回结果是unicode字符:true (\u2713 -> ✓) 或者false (\u2718 -> ✘)。

现在转换器已经OK,接着需要注册其核心模块,作为主模块phonecatApp的依赖。

app/app.module.js:

angular.module('phonecatApp', [
  ...
  'core',
  ...
]);

模板

我们已经创建了两个新文件(core.module.js, checkmark.filter.js),还需要将它们引入我们的布局模板中。
app/index.html:

...


...

转换器的语法:

{{expression | filter}}

将转换器引入手机详情模板:

app/phone-detail/phone-detail.template.html:

...
Infrared
{{$ctrl.phone.connectivity.infrared | checkmark}}
GPS
{{$ctrl.phone.connectivity.gps | checkmark}}
...

测试

app/core/checkmark/checkmark.filter.spec.js:

describe('checkmark', function() {

  beforeEach(module('core'));

  it('should convert boolean values to unicode checkmark or cross',
    //注入转换器
    inject(function(checkmarkFilter) {
    //检查转换器字符串与unicode编码是否对应
      expect(checkmarkFilter(true)).toBe('\u2713');
      expect(checkmarkFilter(false)).toBe('\u2718');
    })
  );

});

在每次测试前,beforeEach(module('core')) 加载了核心模板(包含checkmark转换器)。
我们还调用了辅助功能函数inject(function(checkmarkFilter) { ... })来访问待测试的转换器。具体功能函数请参阅angular.mock.inject。

注入时,转换器名称需要加后缀"Filter"。比如,checkmark转化器以checkmarkFilter注入。更多内容,请参阅Filters。

12 事件处理


在这一步中,我们会增加可点击的手机图片,点击后进入手机详情页。手机详情视图展示一个当前手机的大图和其他手机的缩略图。点击缩略图会切换大图。

组件控制器

app/phone-detail/phone-detail.component.js:

...
controller: ['$http', '$routeParams',
  function PhoneDetailController($http, $routeParams) {
    var self = this;

    self.setImage = function setImage(imageUrl) {
      self.mainImageUrl = imageUrl;
    };

    $http.get('phones/' + $routeParams.phoneId + '.json').then(function(response) {
      self.phone = response.data;
      self.setImage(self.phone.images[0]);
    });
  }
]
...

在phoneDetail控制器中,我们创建了一个mainImageUrl模型属性,并且设置默认值为第一个手机图片的URL。而setImage()是事件处理程序,用于改变mainImageUrl。

组件模板

app/phone-detail/phone-detail.template.html:


...
...
  • 大图片的ngSrc指令与$ctrl.mainImageUrl属性绑定。
  • 缩略图注册ngClick事件处理程序。当用户点击缩略图时,事件处理程序会调用$ctrl.setImage() 函数,将$ctrl.mainImageUrl属性改为缩略图的url。从而改变大图内容。
AngularJS Phonecat (步骤10-步骤12)_第2张图片
tutorial_12.png

测试

为了验证新特性,增加了两个端到端测试。一个验证mainImageUrl默认值是第一张手机图片的url。另一个验证点击缩略图时,大图的url会跟着改变(即大图可正常切换)。

e2e-tests/scenarios.js:

...

describe('View: Phone detail', function() {

  ...
//验证大图是第一张手机图片
  it('should display the first phone image as the main phone image', function() {
    var mainImage = element(by.css('img.phone'));

    expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
  });
//验证图片切换
  it('should swap the main image when clicking on a thumbnail image', function() {
    var mainImage = element(by.css('img.phone'));
    var thumbnails = element.all(by.css('.phone-thumbs img'));

    thumbnails.get(2).click();
    expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.2.jpg/);

    thumbnails.get(0).click();
    expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
  });

});

...

命令行输入npm run protractor,运行测试。

我们还要重构单元测试,因为这一步phoneDetial添加了mainImageUrl模型属性。与之前一样,我们会在测试中使用模拟响应。

app/phone-detail/phone-detail.component.spec.js:

...

describe('controller', function() {
  var $httpBackend, ctrl
  var xyzPhoneData = {
    name: 'phone xyz',
    images: ['image/url1.png', 'image/url2.png']
  };

  beforeEach(inject(function($componentController, _$httpBackend_, _$routeParams_) {
    $httpBackend = _$httpBackend_;
    $httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData);

    ...
  }));

  it('should fetch phone details', function() {
    expect(ctrl.phone).toBeUndefined();

    $httpBackend.flush();
    expect(ctrl.phone).toEqual(xyzPhoneData);
  });

});

...

就这样,我们的单元测试也完成了。

下一篇:AngularJS Phonecat (步骤13-步骤14)

你可能感兴趣的:(AngularJS Phonecat (步骤10-步骤12))