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:

<img ng-src="{{$ctrl.phone.images[0]}}" class="phone" />

<h1>{{$ctrl.phone.name}}</h1>

<p>{{$ctrl.phone.description}}</p>

<ul class="phone-thumbs">
  <li ng-repeat="img in $ctrl.phone.images">
    <img ng-src="{{img}}" />
  </li>
</ul>

<ul class="specs">
  <li>
    <span>Availability and Networks</span>
    <dl>
      <dt>Availability</dt>
      <dd ng-repeat="availability in $ctrl.phone.availability">{{availability}}</dd>
    </dl>
  </li>
  ...
  <li>
    <span>Additional Features</span>
    <dd>{{$ctrl.phone.additionalFeatures}}</dd>
  </li>
</ul>
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');
  });

});

...

命令行中输入<code>npm run protractor</code>既可运行。

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:

...
<script src="core/core.module.js"></script>
<script src="core/checkmark/checkmark.filter.js"></script>
...

转换器的语法:

{{expression | filter}}

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

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

...
<dl>
  <dt>Infrared</dt>
  <dd>{{$ctrl.phone.connectivity.infrared | checkmark}}</dd>
  <dt>GPS</dt>
  <dd>{{$ctrl.phone.connectivity.gps | checkmark}}</dd>
</dl>
...

测试

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:

<img ng-src="{{$ctrl.mainImageUrl}}" class="phone" />
...
<ul class="phone-thumbs">
  <li ng-repeat="img in $ctrl.phone.images">
    <img ng-src="{{img}}" ng-click="$ctrl.setImage(img)" />
  </li>
</ul>
...
  • 大图片的ngSrc指令与$ctrl.mainImageUrl属性绑定。
  • 缩略图注册ngClick事件处理程序。当用户点击缩略图时,事件处理程序会调用$ctrl.setImage() 函数,将$ctrl.mainImageUrl属性改为缩略图的url。从而改变大图内容。
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/);
  });

});

...

命令行输入<code>npm run protractor</code>,运行测试。

我们还要重构单元测试,因为这一步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)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容