导言
最近在学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}}
测试
我们编写了一个新的单元测试,与第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。从而改变大图内容。
测试
为了验证新特性,增加了两个端到端测试。一个验证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)