From 91ee8af33681b95075944463bf8a89360e7891f7 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Fri, 17 Jun 2016 20:53:11 +0300 Subject: [PATCH 01/16] chore(package): update script for consistency with `angular-seed` --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index eb460443d..b6e63b3d6 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,7 @@ "karma-chrome-launcher": "^0.2.3", "karma-firefox-launcher": "^0.1.7", "karma-jasmine": "^0.3.8", - "protractor": "^3.2.2", - "shelljs": "^0.6.0" + "protractor": "^3.2.2" }, "scripts": { "postinstall": "bower install", @@ -32,6 +31,6 @@ "preprotractor": "npm run update-webdriver", "protractor": "protractor e2e-tests/protractor.conf.js", - "update-index-async": "node -e \"require('shelljs/global'); sed('-i', /\\/\\/@@NG_LOADER_START@@[\\s\\S]*\\/\\/@@NG_LOADER_END@@/, '//@@NG_LOADER_START@@\\n' + sed(/sourceMappingURL=angular-loader.min.js.map/,'sourceMappingURL=bower_components/angular-loader/angular-loader.min.js.map','app/bower_components/angular-loader/angular-loader.min.js') + '\\n//@@NG_LOADER_END@@', 'app/index-async.html');\"" + "update-index-async": "node -e \"var fs=require('fs'),indexFile='app/index-async.html',loaderFile='app/bower_components/angular-loader/angular-loader.min.js',loaderText=fs.readFileSync(loaderFile,'utf-8').split(/sourceMappingURL=angular-loader.min.js.map/).join('sourceMappingURL=bower_components/angular-loader/angular-loader.min.js.map'),indexText=fs.readFileSync(indexFile,'utf-8').split(/\\/\\/@@NG_LOADER_START@@[\\s\\S]*\\/\\/@@NG_LOADER_END@@/).join('//@@NG_LOADER_START@@\\n'+loaderText+' //@@NG_LOADER_END@@');fs.writeFileSync(indexFile,indexText);\"" } } From 015a4c00e437c29734a47ddeb67a5c1c681266cd Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sat, 18 Oct 2014 14:45:31 +0200 Subject: [PATCH 02/16] step-0 Bootstrapping - Add the 'angular.js' script. - Add the `ngApp` directive to bootstrap the application. - Add a simple template with an expression. --- app/index.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/index.html b/app/index.html index 748eef01f..9e5cb5b27 100644 --- a/app/index.html +++ b/app/index.html @@ -1,11 +1,14 @@ - + My HTML File + - + +

Nothing here {{'yet' + '!'}}

+ From 9d4397f91dced56f1a2613177a5155dd7bd7a9b7 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sat, 18 Oct 2014 14:45:31 +0200 Subject: [PATCH 03/16] step-1 Static Template - Add a stylesheet file ('app/app.css'). - Add a static list with two phones. --- app/app.css | 4 ++++ app/index.html | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 app/app.css diff --git a/app/app.css b/app/app.css new file mode 100644 index 000000000..eb61bcb80 --- /dev/null +++ b/app/app.css @@ -0,0 +1,4 @@ +body { + padding-top: 20px; +} + diff --git a/app/index.html b/app/index.html index 9e5cb5b27..88b3a2cc1 100644 --- a/app/index.html +++ b/app/index.html @@ -2,13 +2,27 @@ - My HTML File + Google Phone Gallery + -

Nothing here {{'yet' + '!'}}

+
    +
  • + Nexus S +

    + Fast just got faster with Nexus S. +

    +
  • +
  • + Motorola XOOM™ with Wi-Fi +

    + The Next, Next Generation tablet. +

    +
  • +
From 03a53d757945130d184e334d4c4f89ebdab52e14 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sat, 18 Oct 2014 14:45:31 +0200 Subject: [PATCH 04/16] step-2 Angular Templates - Convert the static phone list to dynamic by: - Creating a `PhoneListController` controller. - Extracting the data from HTML into the controller as an in-memory dataset. - Converting the static document into a template with the use of the `ngRepeat` directive. - Add a simple unit test for the `PhoneListController` controller to show how to write tests and run them using Karma (see README.md for instructions). --- app/app.js | 20 ++++++++++++++++++++ app/app.spec.js | 14 ++++++++++++++ app/index.html | 19 ++++++------------- 3 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 app/app.js create mode 100644 app/app.spec.js diff --git a/app/app.js b/app/app.js new file mode 100644 index 000000000..9b0b3e10f --- /dev/null +++ b/app/app.js @@ -0,0 +1,20 @@ +'use strict'; + +// Define the `phonecatApp` module +var phonecatApp = angular.module('phonecatApp', []); + +// Define the `PhoneListController` controller on the `phonecatApp` module +phonecatApp.controller('PhoneListController', function PhoneListController($scope) { + $scope.phones = [ + { + name: 'Nexus S', + snippet: 'Fast just got faster with Nexus S.' + }, { + name: 'Motorola XOOM™ with Wi-Fi', + snippet: 'The Next, Next Generation tablet.' + }, { + name: 'MOTOROLA XOOM™', + snippet: 'The Next, Next Generation tablet.' + } + ]; +}); diff --git a/app/app.spec.js b/app/app.spec.js new file mode 100644 index 000000000..416e86d35 --- /dev/null +++ b/app/app.spec.js @@ -0,0 +1,14 @@ +'use strict'; + +describe('PhoneListController', function() { + + beforeEach(module('phonecatApp')); + + it('should create a `phones` model with 3 phones', inject(function($controller) { + var scope = {}; + var ctrl = $controller('PhoneListController', {$scope: scope}); + + expect(scope.phones.length).toBe(3); + })); + +}); diff --git a/app/index.html b/app/index.html index 88b3a2cc1..3cf88c8c4 100644 --- a/app/index.html +++ b/app/index.html @@ -1,26 +1,19 @@ - + Google Phone Gallery + - +
    -
  • - Nexus S -

    - Fast just got faster with Nexus S. -

    -
  • -
  • - Motorola XOOM™ with Wi-Fi -

    - The Next, Next Generation tablet. -

    +
  • + {{phone.name}} +

    {{phone.snippet}}

From f54b25a3124940aeb622c8b445b119b2c7a8d268 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 28 Mar 2016 15:22:25 +0300 Subject: [PATCH 05/16] step-3 Components - Introduce components. - Combine the controller and the template into a reusable, isolated `phoneList` component. - Refactor the application and tests to use the `phoneList` component. --- app/app.js | 18 +----------------- app/app.spec.js | 14 -------------- app/index.html | 11 ++++------- app/phone-list.component.js | 28 ++++++++++++++++++++++++++++ app/phone-list.component.spec.js | 19 +++++++++++++++++++ 5 files changed, 52 insertions(+), 38 deletions(-) delete mode 100644 app/app.spec.js create mode 100644 app/phone-list.component.js create mode 100644 app/phone-list.component.spec.js diff --git a/app/app.js b/app/app.js index 9b0b3e10f..2c5913d52 100644 --- a/app/app.js +++ b/app/app.js @@ -1,20 +1,4 @@ 'use strict'; // Define the `phonecatApp` module -var phonecatApp = angular.module('phonecatApp', []); - -// Define the `PhoneListController` controller on the `phonecatApp` module -phonecatApp.controller('PhoneListController', function PhoneListController($scope) { - $scope.phones = [ - { - name: 'Nexus S', - snippet: 'Fast just got faster with Nexus S.' - }, { - name: 'Motorola XOOM™ with Wi-Fi', - snippet: 'The Next, Next Generation tablet.' - }, { - name: 'MOTOROLA XOOM™', - snippet: 'The Next, Next Generation tablet.' - } - ]; -}); +angular.module('phonecatApp', []); diff --git a/app/app.spec.js b/app/app.spec.js deleted file mode 100644 index 416e86d35..000000000 --- a/app/app.spec.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -describe('PhoneListController', function() { - - beforeEach(module('phonecatApp')); - - it('should create a `phones` model with 3 phones', inject(function($controller) { - var scope = {}; - var ctrl = $controller('PhoneListController', {$scope: scope}); - - expect(scope.phones.length).toBe(3); - })); - -}); diff --git a/app/index.html b/app/index.html index 3cf88c8c4..9e049ed41 100644 --- a/app/index.html +++ b/app/index.html @@ -7,15 +7,12 @@ + - + -
    -
  • - {{phone.name}} -

    {{phone.snippet}}

    -
  • -
+ + diff --git a/app/phone-list.component.js b/app/phone-list.component.js new file mode 100644 index 000000000..6c527bcaf --- /dev/null +++ b/app/phone-list.component.js @@ -0,0 +1,28 @@ +'use strict'; + +// Register `phoneList` component, along with its associated controller and template +angular. + module('phonecatApp'). + component('phoneList', { + template: + '
    ' + + '
  • ' + + '{{phone.name}}' + + '

    {{phone.snippet}}

    ' + + '
  • ' + + '
', + controller: function PhoneListController() { + this.phones = [ + { + name: 'Nexus S', + snippet: 'Fast just got faster with Nexus S.' + }, { + name: 'Motorola XOOM™ with Wi-Fi', + snippet: 'The Next, Next Generation tablet.' + }, { + name: 'MOTOROLA XOOM™', + snippet: 'The Next, Next Generation tablet.' + } + ]; + } + }); diff --git a/app/phone-list.component.spec.js b/app/phone-list.component.spec.js new file mode 100644 index 000000000..786137302 --- /dev/null +++ b/app/phone-list.component.spec.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('phoneList', function() { + + // Load the module that contains the `phoneList` component before each test + beforeEach(module('phonecatApp')); + + // Test the controller + describe('PhoneListController', function() { + + it('should create a `phones` model with 3 phones', inject(function($componentController) { + var ctrl = $componentController('phoneList'); + + expect(ctrl.phones.length).toBe(3); + })); + + }); + +}); From 4af7317a6941be49fe8ddd3156fc4e5b10cc6e2d Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 28 Mar 2016 21:53:57 +0300 Subject: [PATCH 06/16] step-4 Directory and File Organization - Refactor the layout of files and directories, applying best practices and techniques that will make the application easier to maintain and expand in the future: - Put each entity in its own file. - Organize code by feature area (instead of by function). - Split code into modules that other modules can depend on. - Use external templates in `.html` files (instead of inline HTML strings). --- app/app.js | 4 ---- app/app.module.js | 7 +++++++ app/index.html | 5 +++-- app/{ => phone-list}/phone-list.component.js | 10 ++-------- app/{ => phone-list}/phone-list.component.spec.js | 2 +- app/phone-list/phone-list.module.js | 4 ++++ app/phone-list/phone-list.template.html | 6 ++++++ 7 files changed, 23 insertions(+), 15 deletions(-) delete mode 100644 app/app.js create mode 100644 app/app.module.js rename app/{ => phone-list}/phone-list.component.js (70%) rename app/{ => phone-list}/phone-list.component.spec.js (91%) create mode 100644 app/phone-list/phone-list.module.js create mode 100644 app/phone-list/phone-list.template.html diff --git a/app/app.js b/app/app.js deleted file mode 100644 index 2c5913d52..000000000 --- a/app/app.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -// Define the `phonecatApp` module -angular.module('phonecatApp', []); diff --git a/app/app.module.js b/app/app.module.js new file mode 100644 index 000000000..9e12af684 --- /dev/null +++ b/app/app.module.js @@ -0,0 +1,7 @@ +'use strict'; + +// Define the `phonecatApp` module +angular.module('phonecatApp', [ + // ...which depends on the `phoneList` module + 'phoneList' +]); diff --git a/app/index.html b/app/index.html index 9e049ed41..d8a5cc09a 100644 --- a/app/index.html +++ b/app/index.html @@ -6,8 +6,9 @@ - - + + + diff --git a/app/phone-list.component.js b/app/phone-list/phone-list.component.js similarity index 70% rename from app/phone-list.component.js rename to app/phone-list/phone-list.component.js index 6c527bcaf..c798dba47 100644 --- a/app/phone-list.component.js +++ b/app/phone-list/phone-list.component.js @@ -2,15 +2,9 @@ // Register `phoneList` component, along with its associated controller and template angular. - module('phonecatApp'). + module('phoneList'). component('phoneList', { - template: - '
    ' + - '
  • ' + - '{{phone.name}}' + - '

    {{phone.snippet}}

    ' + - '
  • ' + - '
', + templateUrl: 'phone-list/phone-list.template.html', controller: function PhoneListController() { this.phones = [ { diff --git a/app/phone-list.component.spec.js b/app/phone-list/phone-list.component.spec.js similarity index 91% rename from app/phone-list.component.spec.js rename to app/phone-list/phone-list.component.spec.js index 786137302..ed2da9fc5 100644 --- a/app/phone-list.component.spec.js +++ b/app/phone-list/phone-list.component.spec.js @@ -3,7 +3,7 @@ describe('phoneList', function() { // Load the module that contains the `phoneList` component before each test - beforeEach(module('phonecatApp')); + beforeEach(module('phoneList')); // Test the controller describe('PhoneListController', function() { diff --git a/app/phone-list/phone-list.module.js b/app/phone-list/phone-list.module.js new file mode 100644 index 000000000..b288efa72 --- /dev/null +++ b/app/phone-list/phone-list.module.js @@ -0,0 +1,4 @@ +'use strict'; + +// Define the `phoneList` module +angular.module('phoneList', []); diff --git a/app/phone-list/phone-list.template.html b/app/phone-list/phone-list.template.html new file mode 100644 index 000000000..c22e755c6 --- /dev/null +++ b/app/phone-list/phone-list.template.html @@ -0,0 +1,6 @@ +
    +
  • + {{phone.name}} +

    {{phone.snippet}}

    +
  • +
From 92f4a7916cd2bd7906be92815687efec3d260d63 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 28 Mar 2016 22:58:42 +0300 Subject: [PATCH 07/16] step-5 Filtering Repeaters - Add a search box to demonstrate: - How the data-binding works on input fields. - How to use the `filter` filter. - How `ngRepeat` automatically shrinks and grows the number of phones in the view. - Add an end-to-end test to: - Show how end-to-end tests are written and used. - Prove that the search box and the repeater are correctly wired together. --- app/phone-list/phone-list.template.html | 27 +++++++++++++++++++------ e2e-tests/scenarios.js | 24 +++++++++++++++++++--- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/app/phone-list/phone-list.template.html b/app/phone-list/phone-list.template.html index c22e755c6..3267d702d 100644 --- a/app/phone-list/phone-list.template.html +++ b/app/phone-list/phone-list.template.html @@ -1,6 +1,21 @@ -
    -
  • - {{phone.name}} -

    {{phone.snippet}}

    -
  • -
+
+
+
+ + + Search: + +
+
+ + +
    +
  • + {{phone.name}} +

    {{phone.snippet}}

    +
  • +
+ +
+
+
diff --git a/e2e-tests/scenarios.js b/e2e-tests/scenarios.js index 8ecf38b99..a69e7e216 100644 --- a/e2e-tests/scenarios.js +++ b/e2e-tests/scenarios.js @@ -3,10 +3,28 @@ // Angular E2E Testing Guide: // https://docs.angularjs.org/guide/e2e-testing -describe('My app', function() { +describe('PhoneCat Application', function() { + + describe('phoneList', function() { + + beforeEach(function() { + browser.get('index.html'); + }); + + it('should filter the phone list as a user types into the search box', function() { + var phoneList = element.all(by.repeater('phone in $ctrl.phones')); + var query = element(by.model('$ctrl.query')); + + expect(phoneList.count()).toBe(3); + + query.sendKeys('nexus'); + expect(phoneList.count()).toBe(1); + + query.clear(); + query.sendKeys('motorola'); + expect(phoneList.count()).toBe(2); + }); - beforeEach(function() { - browser.get('index.html'); }); }); From f8000cc9c0d48000e6c0a57a6fa3e5b4b97e2f93 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sun, 19 Oct 2014 09:19:49 +0100 Subject: [PATCH 08/16] step-6 Two-way Data Binding - Add an `age` property to the phone model. - Add a drop-down menu to control the phone list order. - Override the default order value in controller. - Add unit and end-to-end tests for this feature. Closes #213 --- app/phone-list/phone-list.component.js | 11 ++++++--- app/phone-list/phone-list.component.spec.js | 13 +++++++--- app/phone-list/phone-list.template.html | 15 ++++++++++-- e2e-tests/scenarios.js | 27 +++++++++++++++++++++ 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/app/phone-list/phone-list.component.js b/app/phone-list/phone-list.component.js index c798dba47..73bbbd26b 100644 --- a/app/phone-list/phone-list.component.js +++ b/app/phone-list/phone-list.component.js @@ -9,14 +9,19 @@ angular. this.phones = [ { name: 'Nexus S', - snippet: 'Fast just got faster with Nexus S.' + snippet: 'Fast just got faster with Nexus S.', + age: 1 }, { name: 'Motorola XOOM™ with Wi-Fi', - snippet: 'The Next, Next Generation tablet.' + snippet: 'The Next, Next Generation tablet.', + age: 2 }, { name: 'MOTOROLA XOOM™', - snippet: 'The Next, Next Generation tablet.' + snippet: 'The Next, Next Generation tablet.', + age: 3 } ]; + + this.orderProp = 'age'; } }); diff --git a/app/phone-list/phone-list.component.spec.js b/app/phone-list/phone-list.component.spec.js index ed2da9fc5..4dfe32642 100644 --- a/app/phone-list/phone-list.component.spec.js +++ b/app/phone-list/phone-list.component.spec.js @@ -7,12 +7,19 @@ describe('phoneList', function() { // Test the controller describe('PhoneListController', function() { + var ctrl; - it('should create a `phones` model with 3 phones', inject(function($componentController) { - var ctrl = $componentController('phoneList'); + beforeEach(inject(function($componentController) { + ctrl = $componentController('phoneList'); + })); + it('should create a `phones` model with 3 phones', function() { expect(ctrl.phones.length).toBe(3); - })); + }); + + it('should set a default value for the `orderProp` model', function() { + expect(ctrl.orderProp).toBe('age'); + }); }); diff --git a/app/phone-list/phone-list.template.html b/app/phone-list/phone-list.template.html index 3267d702d..6e330ac50 100644 --- a/app/phone-list/phone-list.template.html +++ b/app/phone-list/phone-list.template.html @@ -3,14 +3,25 @@
- Search: +

+ Search: + +

+ +

+ Sort by: + +

    -
  • +
  • {{phone.name}}

    {{phone.snippet}}

  • diff --git a/e2e-tests/scenarios.js b/e2e-tests/scenarios.js index a69e7e216..1f0ef0cb1 100644 --- a/e2e-tests/scenarios.js +++ b/e2e-tests/scenarios.js @@ -25,6 +25,33 @@ describe('PhoneCat Application', function() { expect(phoneList.count()).toBe(2); }); + it('should be possible to control phone order via the drop-down menu', function() { + var queryField = element(by.model('$ctrl.query')); + var orderSelect = element(by.model('$ctrl.orderProp')); + var nameOption = orderSelect.element(by.css('option[value="name"]')); + var phoneNameColumn = element.all(by.repeater('phone in $ctrl.phones').column('phone.name')); + + function getNames() { + return phoneNameColumn.map(function(elem) { + return elem.getText(); + }); + } + + queryField.sendKeys('tablet'); // Let's narrow the dataset to make the assertions shorter + + expect(getNames()).toEqual([ + 'Motorola XOOM\u2122 with Wi-Fi', + 'MOTOROLA XOOM\u2122' + ]); + + nameOption.click(); + + expect(getNames()).toEqual([ + 'MOTOROLA XOOM\u2122', + 'Motorola XOOM\u2122 with Wi-Fi' + ]); + }); + }); }); From 6f43f4b96c048610af61b3f718cc5327ce1f0f5b Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 09/16] step-7 XHR & Dependency Injection - Replace the in-memory dataset with data loaded from the server (in the form of a static 'phone.json' file to keep the tutorial backend agnostic): - The JSON data is loaded using the `$http` service. - Demonstrate the use of `services` and `dependency injection` (DI): - `$http` is injected into the controller through DI. - Introduce DI annotation methods: `.$inject` and inline array Closes #207 --- app/phone-list/phone-list.component.js | 23 ++++++--------------- app/phone-list/phone-list.component.spec.js | 20 +++++++++++++----- e2e-tests/scenarios.js | 4 ++-- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/app/phone-list/phone-list.component.js b/app/phone-list/phone-list.component.js index 73bbbd26b..39023b675 100644 --- a/app/phone-list/phone-list.component.js +++ b/app/phone-list/phone-list.component.js @@ -5,23 +5,12 @@ angular. module('phoneList'). component('phoneList', { templateUrl: 'phone-list/phone-list.template.html', - controller: function PhoneListController() { - this.phones = [ - { - name: 'Nexus S', - snippet: 'Fast just got faster with Nexus S.', - age: 1 - }, { - name: 'Motorola XOOM™ with Wi-Fi', - snippet: 'The Next, Next Generation tablet.', - age: 2 - }, { - name: 'MOTOROLA XOOM™', - snippet: 'The Next, Next Generation tablet.', - age: 3 - } - ]; + controller: function PhoneListController($http) { + var self = this; + self.orderProp = 'age'; - this.orderProp = 'age'; + $http.get('phones/phones.json').then(function(response) { + self.phones = response.data; + }); } }); diff --git a/app/phone-list/phone-list.component.spec.js b/app/phone-list/phone-list.component.spec.js index 4dfe32642..884d66869 100644 --- a/app/phone-list/phone-list.component.spec.js +++ b/app/phone-list/phone-list.component.spec.js @@ -7,17 +7,27 @@ describe('phoneList', function() { // Test the controller describe('PhoneListController', function() { - var ctrl; + var $httpBackend, ctrl; + + // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). + // This allows us to inject a service and assign it to a variable with the same name + // as the service while avoiding a name conflict. + beforeEach(inject(function($componentController, _$httpBackend_) { + $httpBackend = _$httpBackend_; + $httpBackend.expectGET('phones/phones.json') + .respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]); - beforeEach(inject(function($componentController) { ctrl = $componentController('phoneList'); })); - it('should create a `phones` model with 3 phones', function() { - expect(ctrl.phones.length).toBe(3); + it('should create a `phones` property with 2 phones fetched with `$http`', function() { + expect(ctrl.phones).toBeUndefined(); + + $httpBackend.flush(); + expect(ctrl.phones).toEqual([{name: 'Nexus S'}, {name: 'Motorola DROID'}]); }); - it('should set a default value for the `orderProp` model', function() { + it('should set a default value for the `orderProp` property', function() { expect(ctrl.orderProp).toBe('age'); }); diff --git a/e2e-tests/scenarios.js b/e2e-tests/scenarios.js index 1f0ef0cb1..ad87891bf 100644 --- a/e2e-tests/scenarios.js +++ b/e2e-tests/scenarios.js @@ -15,14 +15,14 @@ describe('PhoneCat Application', function() { var phoneList = element.all(by.repeater('phone in $ctrl.phones')); var query = element(by.model('$ctrl.query')); - expect(phoneList.count()).toBe(3); + expect(phoneList.count()).toBe(20); query.sendKeys('nexus'); expect(phoneList.count()).toBe(1); query.clear(); query.sendKeys('motorola'); - expect(phoneList.count()).toBe(2); + expect(phoneList.count()).toBe(8); }); it('should be possible to control phone order via the drop-down menu', function() { From a417d79f147e6ac407a825c7a1bda78eba29a02c Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 10/16] step-8 Templating Links & Images - Add a phone image and links to phone pages. - Add an end-to-end test that verifies the phone links. - Tweak the CSS to style the page just a notch. --- app/app.css | 17 +++++++++++++++++ app/phone-list/phone-list.component.js | 4 ++-- app/phone-list/phone-list.template.html | 7 +++++-- e2e-tests/scenarios.js | 8 ++++++++ 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/app/app.css b/app/app.css index eb61bcb80..53a7cfe50 100644 --- a/app/app.css +++ b/app/app.css @@ -2,3 +2,20 @@ body { padding-top: 20px; } +.phones { + list-style: none; +} + +.phones li { + clear: both; + height: 115px; + padding-top: 15px; +} + +.thumb { + float: left; + height: 100px; + margin: -0.5em 1em 1.5em 0; + padding-bottom: 1em; + width: 100px; +} diff --git a/app/phone-list/phone-list.component.js b/app/phone-list/phone-list.component.js index 39023b675..dd47fc8e7 100644 --- a/app/phone-list/phone-list.component.js +++ b/app/phone-list/phone-list.component.js @@ -5,12 +5,12 @@ angular. module('phoneList'). component('phoneList', { templateUrl: 'phone-list/phone-list.template.html', - controller: function PhoneListController($http) { + controller: ['$http', function PhoneListController($http) { var self = this; self.orderProp = 'age'; $http.get('phones/phones.json').then(function(response) { self.phones = response.data; }); - } + }] }); diff --git a/app/phone-list/phone-list.template.html b/app/phone-list/phone-list.template.html index 6e330ac50..a3f5a6437 100644 --- a/app/phone-list/phone-list.template.html +++ b/app/phone-list/phone-list.template.html @@ -21,8 +21,11 @@ diff --git a/e2e-tests/scenarios.js b/e2e-tests/scenarios.js index ad87891bf..91b124e45 100644 --- a/e2e-tests/scenarios.js +++ b/e2e-tests/scenarios.js @@ -52,6 +52,14 @@ describe('PhoneCat Application', function() { ]); }); + it('should render phone specific links', function() { + var query = element(by.model('$ctrl.query')); + query.sendKeys('nexus'); + + element.all(by.css('.phones li a')).first().click(); + expect(browser.getLocationAbsUrl()).toBe('/phones/nexus-s'); + }); + }); }); From 24331c987ff661a04e8c17bbe28a55af1ee47a8b Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 20 Jan 2016 14:54:29 +0000 Subject: [PATCH 11/16] step-9 Routing & Multiple Views - Introduce the `$route` service, which allows binding URLs to views for routing and deep-linking: - Add the `ngRoute` module as a dependency. - Configure routes for the application. - Use the `ngView` directive in 'index.html'. - Create a phone list route (`/phones`): - Map `/phones` to the existing `phoneList` component. - Create a phone detail route (`/phones/:phoneId`): - Map `/phones/:phoneId` to a new `phoneDetail` component. - Create a dummy `phoneDetail` component, which displays the selected phone ID. - Pass the `phoneId` parameter to the component's controller via `$routeParams`. --- app/app.config.js | 18 ++++++++++++++++++ app/app.module.js | 5 +++-- app/index.html | 7 +++++-- app/phone-detail/phone-detail.component.js | 13 +++++++++++++ app/phone-detail/phone-detail.module.js | 6 ++++++ app/phone-list/phone-list.template.html | 4 ++-- bower.json | 1 + e2e-tests/scenarios.js | 21 +++++++++++++++++++-- karma.conf.js | 1 + 9 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 app/app.config.js create mode 100644 app/phone-detail/phone-detail.component.js create mode 100644 app/phone-detail/phone-detail.module.js diff --git a/app/app.config.js b/app/app.config.js new file mode 100644 index 000000000..a060f5906 --- /dev/null +++ b/app/app.config.js @@ -0,0 +1,18 @@ +'use strict'; + +angular. + module('phonecatApp'). + config(['$locationProvider' ,'$routeProvider', + function config($locationProvider, $routeProvider) { + $locationProvider.hashPrefix('!'); + + $routeProvider. + when('/phones', { + template: '' + }). + when('/phones/:phoneId', { + template: '' + }). + otherwise('/phones'); + } + ]); diff --git a/app/app.module.js b/app/app.module.js index 9e12af684..e84780584 100644 --- a/app/app.module.js +++ b/app/app.module.js @@ -2,6 +2,7 @@ // Define the `phonecatApp` module angular.module('phonecatApp', [ - // ...which depends on the `phoneList` module - 'phoneList' + 'ngRoute', + 'phoneDetail', + 'phoneList', ]); diff --git a/app/index.html b/app/index.html index d8a5cc09a..e8457c675 100644 --- a/app/index.html +++ b/app/index.html @@ -6,14 +6,17 @@ + + + + - - +
    diff --git a/app/phone-detail/phone-detail.component.js b/app/phone-detail/phone-detail.component.js new file mode 100644 index 000000000..186925c88 --- /dev/null +++ b/app/phone-detail/phone-detail.component.js @@ -0,0 +1,13 @@ +'use strict'; + +// Register `phoneDetail` component, along with its associated controller and template +angular. + module('phoneDetail'). + component('phoneDetail', { + template: 'TBD: Detail view for {{$ctrl.phoneId}}', + controller: ['$routeParams', + function PhoneDetailController($routeParams) { + this.phoneId = $routeParams.phoneId; + } + ] + }); diff --git a/app/phone-detail/phone-detail.module.js b/app/phone-detail/phone-detail.module.js new file mode 100644 index 000000000..70eceecdb --- /dev/null +++ b/app/phone-detail/phone-detail.module.js @@ -0,0 +1,6 @@ +'use strict'; + +// Define the `phoneDetail` module +angular.module('phoneDetail', [ + 'ngRoute' +]); diff --git a/app/phone-list/phone-list.template.html b/app/phone-list/phone-list.template.html index a3f5a6437..b0d81d4cb 100644 --- a/app/phone-list/phone-list.template.html +++ b/app/phone-list/phone-list.template.html @@ -22,10 +22,10 @@ diff --git a/bower.json b/bower.json index a7a00a589..5ccd8ac68 100644 --- a/bower.json +++ b/bower.json @@ -8,6 +8,7 @@ "dependencies": { "angular": "1.5.x", "angular-mocks": "1.5.x", + "angular-route": "1.5.x", "bootstrap": "3.3.x" } } diff --git a/e2e-tests/scenarios.js b/e2e-tests/scenarios.js index 91b124e45..0af78dcde 100644 --- a/e2e-tests/scenarios.js +++ b/e2e-tests/scenarios.js @@ -5,10 +5,15 @@ describe('PhoneCat Application', function() { - describe('phoneList', function() { + it('should redirect `index.html` to `index.html#!/phones', function() { + browser.get('index.html'); + expect(browser.getLocationAbsUrl()).toBe('/phones'); + }); + + describe('View: Phone list', function() { beforeEach(function() { - browser.get('index.html'); + browser.get('index.html#!/phones'); }); it('should filter the phone list as a user types into the search box', function() { @@ -62,4 +67,16 @@ describe('PhoneCat Application', function() { }); + describe('View: Phone detail', function() { + + beforeEach(function() { + browser.get('index.html#!/phones/nexus-s'); + }); + + it('should display placeholder page with `phoneId`', function() { + expect(element(by.binding('$ctrl.phoneId')).getText()).toBe('nexus-s'); + }); + + }); + }); diff --git a/karma.conf.js b/karma.conf.js index 704f45ed4..af3a1e53c 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -6,6 +6,7 @@ module.exports = function(config) { files: [ 'bower_components/angular/angular.js', + 'bower_components/angular-route/angular-route.js', 'bower_components/angular-mocks/angular-mocks.js', '**/*.module.js', '*!(.module|.spec).js', From ef0166517cc1c365932709758091c21eb2e9f7c7 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 12/16] step-10 More Templating - Implement fetching data for the selected phone and rendering to the view: - Use `$http` in `PhoneDetailController` to fetch the phone details from a JSON file. - Create the template for the detail view. - Add CSS styles to make the phone detail page look "pretty-ish". --- app/app.css | 59 ++++++++- app/phone-detail/phone-detail.component.js | 12 +- .../phone-detail.component.spec.js | 30 +++++ app/phone-detail/phone-detail.template.html | 113 ++++++++++++++++++ e2e-tests/scenarios.js | 4 +- 5 files changed, 211 insertions(+), 7 deletions(-) create mode 100644 app/phone-detail/phone-detail.component.spec.js create mode 100644 app/phone-detail/phone-detail.template.html diff --git a/app/app.css b/app/app.css index 53a7cfe50..72d3554be 100644 --- a/app/app.css +++ b/app/app.css @@ -1,7 +1,12 @@ body { - padding-top: 20px; + padding: 20px; } +h1 { + border-bottom: 1px solid gray; +} + +/* View: Phone list */ .phones { list-style: none; } @@ -19,3 +24,55 @@ body { padding-bottom: 1em; width: 100px; } + +/* View: Phone detail */ +.phone { + background-color: white; + border: 1px solid black; + float: left; + height: 400px; + margin-bottom: 2em; + margin-right: 3em; + padding: 2em; + width: 400px; +} + +.phone-thumbs { + list-style: none; + margin: 0; +} + +.phone-thumbs img { + height: 100px; + padding: 1em; + width: 100px; +} + +.phone-thumbs li { + background-color: white; + border: 1px solid black; + display: inline-block; + margin: 1em; +} + +.specs { + clear: both; + list-style: none; + margin: 0; + padding: 0; +} + +.specs dt { + font-weight: bold; +} + +.specs > li { + display: inline-block; + vertical-align: top; + width: 200px; +} + +.specs > li > span { + font-size: 1.2em; + font-weight: bold; +} diff --git a/app/phone-detail/phone-detail.component.js b/app/phone-detail/phone-detail.component.js index 186925c88..f29846f0c 100644 --- a/app/phone-detail/phone-detail.component.js +++ b/app/phone-detail/phone-detail.component.js @@ -4,10 +4,14 @@ angular. module('phoneDetail'). component('phoneDetail', { - template: 'TBD: Detail view for {{$ctrl.phoneId}}', - controller: ['$routeParams', - function PhoneDetailController($routeParams) { - this.phoneId = $routeParams.phoneId; + 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; + }); } ] }); diff --git a/app/phone-detail/phone-detail.component.spec.js b/app/phone-detail/phone-detail.component.spec.js new file mode 100644 index 000000000..3c2f83248 --- /dev/null +++ b/app/phone-detail/phone-detail.component.spec.js @@ -0,0 +1,30 @@ +'use strict'; + +describe('phoneDetail', function() { + + // Load the module that contains the `phoneDetail` component before each test + beforeEach(module('phoneDetail')); + + // Test the controller + 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'}); + }); + + }); + +}); diff --git a/app/phone-detail/phone-detail.template.html b/app/phone-detail/phone-detail.template.html new file mode 100644 index 000000000..02a9d8eb9 --- /dev/null +++ b/app/phone-detail/phone-detail.template.html @@ -0,0 +1,113 @@ + + +

    {{$ctrl.phone.name}}

    + +

    {{$ctrl.phone.description}}

    + +
      +
    • + +
    • +
    + +
      +
    • + Availability and Networks +
      +
      Availability
      +
      {{availability}}
      +
      +
    • +
    • + Battery +
      +
      Type
      +
      {{$ctrl.phone.battery.type}}
      +
      Talk Time
      +
      {{$ctrl.phone.battery.talkTime}}
      +
      Standby time (max)
      +
      {{$ctrl.phone.battery.standbyTime}}
      +
      +
    • +
    • + Storage and Memory +
      +
      RAM
      +
      {{$ctrl.phone.storage.ram}}
      +
      Internal Storage
      +
      {{$ctrl.phone.storage.flash}}
      +
      +
    • +
    • + Connectivity +
      +
      Network Support
      +
      {{$ctrl.phone.connectivity.cell}}
      +
      WiFi
      +
      {{$ctrl.phone.connectivity.wifi}}
      +
      Bluetooth
      +
      {{$ctrl.phone.connectivity.bluetooth}}
      +
      Infrared
      +
      {{$ctrl.phone.connectivity.infrared}}
      +
      GPS
      +
      {{$ctrl.phone.connectivity.gps}}
      +
      +
    • +
    • + Android +
      +
      OS Version
      +
      {{$ctrl.phone.android.os}}
      +
      UI
      +
      {{$ctrl.phone.android.ui}}
      +
      +
    • +
    • + Size and Weight +
      +
      Dimensions
      +
      {{dim}}
      +
      Weight
      +
      {{$ctrl.phone.sizeAndWeight.weight}}
      +
      +
    • +
    • + Display +
      +
      Screen size
      +
      {{$ctrl.phone.display.screenSize}}
      +
      Screen resolution
      +
      {{$ctrl.phone.display.screenResolution}}
      +
      Touch screen
      +
      {{$ctrl.phone.display.touchScreen}}
      +
      +
    • +
    • + Hardware +
      +
      CPU
      +
      {{$ctrl.phone.hardware.cpu}}
      +
      USB
      +
      {{$ctrl.phone.hardware.usb}}
      +
      Audio / headphone jack
      +
      {{$ctrl.phone.hardware.audioJack}}
      +
      FM Radio
      +
      {{$ctrl.phone.hardware.fmRadio}}
      +
      Accelerometer
      +
      {{$ctrl.phone.hardware.accelerometer}}
      +
      +
    • +
    • + Camera +
      +
      Primary
      +
      {{$ctrl.phone.camera.primary}}
      +
      Features
      +
      {{$ctrl.phone.camera.features.join(', ')}}
      +
      +
    • +
    • + Additional Features +
      {{$ctrl.phone.additionalFeatures}}
      +
    • +
    diff --git a/e2e-tests/scenarios.js b/e2e-tests/scenarios.js index 0af78dcde..ae53fe56c 100644 --- a/e2e-tests/scenarios.js +++ b/e2e-tests/scenarios.js @@ -73,8 +73,8 @@ describe('PhoneCat Application', function() { browser.get('index.html#!/phones/nexus-s'); }); - it('should display placeholder page with `phoneId`', function() { - expect(element(by.binding('$ctrl.phoneId')).getText()).toBe('nexus-s'); + it('should display the `nexus-s` page', function() { + expect(element(by.binding('$ctrl.phone.name')).getText()).toBe('Nexus S'); }); }); From 5d3259cbe076f72a65c25a491c908bb44acd0071 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 13/16] step-11 Custom Filters - Implement a custom `checkmark` filter. - Update the `phoneDetail` template to use the `checkmark` filter. - Add a unit test for the `checkmark` filter. --- app/app.module.js | 1 + app/core/checkmark/checkmark.filter.js | 9 +++++++++ app/core/checkmark/checkmark.filter.spec.js | 14 ++++++++++++++ app/core/core.module.js | 4 ++++ app/index.html | 2 ++ app/phone-detail/phone-detail.template.html | 10 +++++----- 6 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 app/core/checkmark/checkmark.filter.js create mode 100644 app/core/checkmark/checkmark.filter.spec.js create mode 100644 app/core/core.module.js diff --git a/app/app.module.js b/app/app.module.js index e84780584..d2ee1bf04 100644 --- a/app/app.module.js +++ b/app/app.module.js @@ -3,6 +3,7 @@ // Define the `phonecatApp` module angular.module('phonecatApp', [ 'ngRoute', + 'core', 'phoneDetail', 'phoneList', ]); diff --git a/app/core/checkmark/checkmark.filter.js b/app/core/checkmark/checkmark.filter.js new file mode 100644 index 000000000..0132dfc02 --- /dev/null +++ b/app/core/checkmark/checkmark.filter.js @@ -0,0 +1,9 @@ +'use strict'; + +angular. + module('core'). + filter('checkmark', function() { + return function(input) { + return input ? '\u2713' : '\u2718'; + }; + }); diff --git a/app/core/checkmark/checkmark.filter.spec.js b/app/core/checkmark/checkmark.filter.spec.js new file mode 100644 index 000000000..4d53baab9 --- /dev/null +++ b/app/core/checkmark/checkmark.filter.spec.js @@ -0,0 +1,14 @@ +'use strict'; + +describe('checkmark', function() { + + beforeEach(module('core')); + + it('should convert boolean values to unicode checkmark or cross', + inject(function(checkmarkFilter) { + expect(checkmarkFilter(true)).toBe('\u2713'); + expect(checkmarkFilter(false)).toBe('\u2718'); + }) + ); + +}); diff --git a/app/core/core.module.js b/app/core/core.module.js new file mode 100644 index 000000000..93922c310 --- /dev/null +++ b/app/core/core.module.js @@ -0,0 +1,4 @@ +'use strict'; + +// Define the `core` module +angular.module('core', []); diff --git a/app/index.html b/app/index.html index e8457c675..653eb7453 100644 --- a/app/index.html +++ b/app/index.html @@ -9,6 +9,8 @@ + + diff --git a/app/phone-detail/phone-detail.template.html b/app/phone-detail/phone-detail.template.html index 02a9d8eb9..45ea62b09 100644 --- a/app/phone-detail/phone-detail.template.html +++ b/app/phone-detail/phone-detail.template.html @@ -48,9 +48,9 @@

    {{$ctrl.phone.name}}

    Bluetooth
    {{$ctrl.phone.connectivity.bluetooth}}
    Infrared
    -
    {{$ctrl.phone.connectivity.infrared}}
    +
    {{$ctrl.phone.connectivity.infrared | checkmark}}
    GPS
    -
    {{$ctrl.phone.connectivity.gps}}
    +
    {{$ctrl.phone.connectivity.gps | checkmark}}
  • @@ -79,7 +79,7 @@

    {{$ctrl.phone.name}}

    Screen resolution
    {{$ctrl.phone.display.screenResolution}}
    Touch screen
    -
    {{$ctrl.phone.display.touchScreen}}
    +
    {{$ctrl.phone.display.touchScreen | checkmark}}
  • @@ -92,9 +92,9 @@

    {{$ctrl.phone.name}}

    Audio / headphone jack
    {{$ctrl.phone.hardware.audioJack}}
    FM Radio
    -
    {{$ctrl.phone.hardware.fmRadio}}
    +
    {{$ctrl.phone.hardware.fmRadio | checkmark}}
    Accelerometer
    -
    {{$ctrl.phone.hardware.accelerometer}}
    +
    {{$ctrl.phone.hardware.accelerometer | checkmark}}
  • From 809e14b351f7f28b01dc2622cb1437488d75fe62 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 14/16] step-12 Event Handlers - Make the thumbnail images in the phone detail view clickable: - Introduce a `mainImageUrl` property on `PhoneDetailController`. - Implement the `setImage()` method for changing the main image. - Use `ngClick` on the thumbnails to register a handler that changes the main image. - Add an end-to-end test for this feature. --- app/app.css | 1 + app/phone-detail/phone-detail.component.js | 5 +++++ app/phone-detail/phone-detail.component.spec.js | 8 ++++++-- app/phone-detail/phone-detail.template.html | 4 ++-- e2e-tests/scenarios.js | 17 +++++++++++++++++ 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/app/app.css b/app/app.css index 72d3554be..eb74556db 100644 --- a/app/app.css +++ b/app/app.css @@ -51,6 +51,7 @@ h1 { .phone-thumbs li { background-color: white; border: 1px solid black; + cursor: pointer; display: inline-block; margin: 1em; } diff --git a/app/phone-detail/phone-detail.component.js b/app/phone-detail/phone-detail.component.js index f29846f0c..60594f20a 100644 --- a/app/phone-detail/phone-detail.component.js +++ b/app/phone-detail/phone-detail.component.js @@ -9,8 +9,13 @@ angular. 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]); }); } ] diff --git a/app/phone-detail/phone-detail.component.spec.js b/app/phone-detail/phone-detail.component.spec.js index 3c2f83248..fe79eb238 100644 --- a/app/phone-detail/phone-detail.component.spec.js +++ b/app/phone-detail/phone-detail.component.spec.js @@ -8,10 +8,14 @@ describe('phoneDetail', function() { // Test the controller describe('PhoneDetailController', 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({name: 'phone xyz'}); + $httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData); $routeParams.phoneId = 'xyz'; @@ -22,7 +26,7 @@ describe('phoneDetail', function() { expect(ctrl.phone).toBeUndefined(); $httpBackend.flush(); - expect(ctrl.phone).toEqual({name: 'phone xyz'}); + expect(ctrl.phone).toEqual(xyzPhoneData); }); }); diff --git a/app/phone-detail/phone-detail.template.html b/app/phone-detail/phone-detail.template.html index 45ea62b09..09061ce7a 100644 --- a/app/phone-detail/phone-detail.template.html +++ b/app/phone-detail/phone-detail.template.html @@ -1,4 +1,4 @@ - +

    {{$ctrl.phone.name}}

    @@ -6,7 +6,7 @@

    {{$ctrl.phone.name}}

    • - +
    diff --git a/e2e-tests/scenarios.js b/e2e-tests/scenarios.js index ae53fe56c..072deaf5e 100644 --- a/e2e-tests/scenarios.js +++ b/e2e-tests/scenarios.js @@ -77,6 +77,23 @@ describe('PhoneCat Application', function() { expect(element(by.binding('$ctrl.phone.name')).getText()).toBe('Nexus S'); }); + 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/); + }); + }); }); From ed4a32b0157be91ee33841316ba3f6573b4cbdb5 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 15/16] step-13 REST and Custom Services - Replace `$http` with `$resource`. - Create a custom `Phone` service that represents the RESTful client. - Use a custom Jasmine equality tester in unit tests to ignore irrelevant properties. --- app/core/core.module.js | 2 +- app/core/phone/phone.module.js | 4 ++ app/core/phone/phone.service.js | 15 +++++++ app/core/phone/phone.service.spec.js | 43 +++++++++++++++++++ app/index.html | 3 ++ app/phone-detail/phone-detail.component.js | 12 +++--- .../phone-detail.component.spec.js | 4 +- app/phone-detail/phone-detail.module.js | 3 +- app/phone-list/phone-list.component.js | 14 +++--- app/phone-list/phone-list.component.spec.js | 7 ++- app/phone-list/phone-list.module.js | 2 +- bower.json | 1 + karma.conf.js | 1 + 13 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 app/core/phone/phone.module.js create mode 100644 app/core/phone/phone.service.js create mode 100644 app/core/phone/phone.service.spec.js diff --git a/app/core/core.module.js b/app/core/core.module.js index 93922c310..84a91dc7a 100644 --- a/app/core/core.module.js +++ b/app/core/core.module.js @@ -1,4 +1,4 @@ 'use strict'; // Define the `core` module -angular.module('core', []); +angular.module('core', ['core.phone']); diff --git a/app/core/phone/phone.module.js b/app/core/phone/phone.module.js new file mode 100644 index 000000000..0b6b34889 --- /dev/null +++ b/app/core/phone/phone.module.js @@ -0,0 +1,4 @@ +'use strict'; + +// Define the `core.phone` module +angular.module('core.phone', ['ngResource']); diff --git a/app/core/phone/phone.service.js b/app/core/phone/phone.service.js new file mode 100644 index 000000000..048e66ae8 --- /dev/null +++ b/app/core/phone/phone.service.js @@ -0,0 +1,15 @@ +'use strict'; + +angular. + module('core.phone'). + factory('Phone', ['$resource', + function($resource) { + return $resource('phones/:phoneId.json', {}, { + query: { + method: 'GET', + params: {phoneId: 'phones'}, + isArray: true + } + }); + } + ]); diff --git a/app/core/phone/phone.service.spec.js b/app/core/phone/phone.service.spec.js new file mode 100644 index 000000000..f045c561c --- /dev/null +++ b/app/core/phone/phone.service.spec.js @@ -0,0 +1,43 @@ +'use strict'; + +describe('Phone', function() { + var $httpBackend; + var Phone; + var phonesData = [ + {name: 'Phone X'}, + {name: 'Phone Y'}, + {name: 'Phone Z'} + ]; + + // Add a custom equality tester before each test + beforeEach(function() { + jasmine.addCustomEqualityTester(angular.equals); + }); + + // Load the module that contains the `Phone` service before each test + beforeEach(module('core.phone')); + + // Instantiate the service and "train" `$httpBackend` before each test + beforeEach(inject(function(_$httpBackend_, _Phone_) { + $httpBackend = _$httpBackend_; + $httpBackend.expectGET('phones/phones.json').respond(phonesData); + + Phone = _Phone_; + })); + + // Verify that there are no outstanding expectations or requests after each test + afterEach(function () { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('should fetch the phones data from `/phones/phones.json`', function() { + var phones = Phone.query(); + + expect(phones).toEqual([]); + + $httpBackend.flush(); + expect(phones).toEqual(phonesData); + }); + +}); diff --git a/app/index.html b/app/index.html index 653eb7453..ef9c965fb 100644 --- a/app/index.html +++ b/app/index.html @@ -6,11 +6,14 @@ + + + diff --git a/app/phone-detail/phone-detail.component.js b/app/phone-detail/phone-detail.component.js index 60594f20a..3b38bf3af 100644 --- a/app/phone-detail/phone-detail.component.js +++ b/app/phone-detail/phone-detail.component.js @@ -5,18 +5,16 @@ angular. module('phoneDetail'). component('phoneDetail', { templateUrl: 'phone-detail/phone-detail.template.html', - controller: ['$http', '$routeParams', - function PhoneDetailController($http, $routeParams) { + controller: ['$routeParams', 'Phone', + function PhoneDetailController($routeParams, Phone) { var self = this; + self.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) { + self.setImage(phone.images[0]); + }); 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]); - }); } ] }); diff --git a/app/phone-detail/phone-detail.component.spec.js b/app/phone-detail/phone-detail.component.spec.js index fe79eb238..8f4982682 100644 --- a/app/phone-detail/phone-detail.component.spec.js +++ b/app/phone-detail/phone-detail.component.spec.js @@ -23,7 +23,9 @@ describe('phoneDetail', function() { })); it('should fetch the phone details', function() { - expect(ctrl.phone).toBeUndefined(); + jasmine.addCustomEqualityTester(angular.equals); + + expect(ctrl.phone).toEqual({}); $httpBackend.flush(); expect(ctrl.phone).toEqual(xyzPhoneData); diff --git a/app/phone-detail/phone-detail.module.js b/app/phone-detail/phone-detail.module.js index 70eceecdb..fd7cb3b92 100644 --- a/app/phone-detail/phone-detail.module.js +++ b/app/phone-detail/phone-detail.module.js @@ -2,5 +2,6 @@ // Define the `phoneDetail` module angular.module('phoneDetail', [ - 'ngRoute' + 'ngRoute', + 'core.phone' ]); diff --git a/app/phone-list/phone-list.component.js b/app/phone-list/phone-list.component.js index dd47fc8e7..484be4f88 100644 --- a/app/phone-list/phone-list.component.js +++ b/app/phone-list/phone-list.component.js @@ -5,12 +5,10 @@ angular. module('phoneList'). component('phoneList', { templateUrl: 'phone-list/phone-list.template.html', - controller: ['$http', function PhoneListController($http) { - var self = this; - self.orderProp = 'age'; - - $http.get('phones/phones.json').then(function(response) { - self.phones = response.data; - }); - }] + controller: ['Phone', + function PhoneListController(Phone) { + this.phones = Phone.query(); + this.orderProp = 'age'; + } + ] }); diff --git a/app/phone-list/phone-list.component.spec.js b/app/phone-list/phone-list.component.spec.js index 884d66869..572260e49 100644 --- a/app/phone-list/phone-list.component.spec.js +++ b/app/phone-list/phone-list.component.spec.js @@ -9,9 +9,6 @@ describe('phoneList', function() { describe('PhoneListController', function() { var $httpBackend, ctrl; - // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). - // This allows us to inject a service and assign it to a variable with the same name - // as the service while avoiding a name conflict. beforeEach(inject(function($componentController, _$httpBackend_) { $httpBackend = _$httpBackend_; $httpBackend.expectGET('phones/phones.json') @@ -21,7 +18,9 @@ describe('phoneList', function() { })); it('should create a `phones` property with 2 phones fetched with `$http`', function() { - expect(ctrl.phones).toBeUndefined(); + jasmine.addCustomEqualityTester(angular.equals); + + expect(ctrl.phones).toEqual([]); $httpBackend.flush(); expect(ctrl.phones).toEqual([{name: 'Nexus S'}, {name: 'Motorola DROID'}]); diff --git a/app/phone-list/phone-list.module.js b/app/phone-list/phone-list.module.js index b288efa72..8ade7c5b8 100644 --- a/app/phone-list/phone-list.module.js +++ b/app/phone-list/phone-list.module.js @@ -1,4 +1,4 @@ 'use strict'; // Define the `phoneList` module -angular.module('phoneList', []); +angular.module('phoneList', ['core.phone']); diff --git a/bower.json b/bower.json index 5ccd8ac68..76eb41364 100644 --- a/bower.json +++ b/bower.json @@ -8,6 +8,7 @@ "dependencies": { "angular": "1.5.x", "angular-mocks": "1.5.x", + "angular-resource": "1.5.x", "angular-route": "1.5.x", "bootstrap": "3.3.x" } diff --git a/karma.conf.js b/karma.conf.js index af3a1e53c..9ef4349ee 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -6,6 +6,7 @@ module.exports = function(config) { files: [ 'bower_components/angular/angular.js', + 'bower_components/angular-resource/angular-resource.js', 'bower_components/angular-route/angular-route.js', 'bower_components/angular-mocks/angular-mocks.js', '**/*.module.js', From 038aa9228f1771beceb0b40f8453d66d3ae1390b Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 20 Jan 2016 14:55:34 +0000 Subject: [PATCH 16/16] step-14 Animations - Add animations to the application: - Animate changes to the phone list, adding, removing and reordering phones with `ngRepeat`. - Animate view transitions with `ngView`. - Animate changes to the main phone image in the phone detail view. - Showcase three different kinds of animations: - CSS transition animations. - CSS keyframe animations. - JavaScript-based animations. --- app/app.animations.css | 67 +++++++++++++++++++++ app/app.animations.js | 43 +++++++++++++ app/app.css | 16 ++++- app/app.module.js | 1 + app/index.html | 9 ++- app/phone-detail/phone-detail.template.html | 6 +- app/phone-list/phone-list.template.html | 3 +- bower.json | 4 +- e2e-tests/scenarios.js | 4 +- karma.conf.js | 1 + 10 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 app/app.animations.css create mode 100644 app/app.animations.js diff --git a/app/app.animations.css b/app/app.animations.css new file mode 100644 index 000000000..175320b50 --- /dev/null +++ b/app/app.animations.css @@ -0,0 +1,67 @@ +/* Animate `ngRepeat` in `phoneList` component */ +.phone-list-item.ng-enter, +.phone-list-item.ng-leave, +.phone-list-item.ng-move { + overflow: hidden; + transition: 0.5s linear all; +} + +.phone-list-item.ng-enter, +.phone-list-item.ng-leave.ng-leave-active, +.phone-list-item.ng-move { + height: 0; + margin-bottom: 0; + opacity: 0; + padding-bottom: 0; + padding-top: 0; +} + +.phone-list-item.ng-enter.ng-enter-active, +.phone-list-item.ng-leave, +.phone-list-item.ng-move.ng-move-active { + height: 120px; + margin-bottom: 20px; + opacity: 1; + padding-bottom: 4px; + padding-top: 15px; +} + +/* Animate view transitions with `ngView` */ +.view-container { + position: relative; +} + +.view-frame { + margin-top: 20px; +} + +.view-frame.ng-enter, +.view-frame.ng-leave { + background: white; + left: 0; + position: absolute; + right: 0; + top: 0; +} + +.view-frame.ng-enter { + animation: 1s fade-in; + z-index: 100; +} + +.view-frame.ng-leave { + animation: 1s fade-out; + z-index: 99; +} + +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fade-out { + from { opacity: 1; } + to { opacity: 0; } +} + +/* Older browsers might need vendor-prefixes for keyframes and animation! */ diff --git a/app/app.animations.js b/app/app.animations.js new file mode 100644 index 000000000..394fcc944 --- /dev/null +++ b/app/app.animations.js @@ -0,0 +1,43 @@ +'use strict'; + +angular. + module('phonecatApp'). + animation('.phone', function phoneAnimationFactory() { + return { + addClass: animateIn, + removeClass: animateOut + }; + + function animateIn(element, className, done) { + if (className !== 'selected') return; + + element.css({ + display: 'block', + position: 'absolute', + top: 500, + left: 0 + }).animate({ + top: 0 + }, done); + + return function animateInEnd(wasCanceled) { + if (wasCanceled) element.stop(); + }; + } + + function animateOut(element, className, done) { + if (className !== 'selected') return; + + element.css({ + position: 'absolute', + top: 0, + left: 0 + }).animate({ + top: -500 + }, done); + + return function animateOutEnd(wasCanceled) { + if (wasCanceled) element.stop(); + }; + } + }); diff --git a/app/app.css b/app/app.css index eb74556db..f4b45b02a 100644 --- a/app/app.css +++ b/app/app.css @@ -4,6 +4,7 @@ body { h1 { border-bottom: 1px solid gray; + margin-top: 0; } /* View: Phone list */ @@ -28,7 +29,7 @@ h1 { /* View: Phone detail */ .phone { background-color: white; - border: 1px solid black; + display: none; float: left; height: 400px; margin-bottom: 2em; @@ -37,6 +38,19 @@ h1 { width: 400px; } +.phone:first-child { + display: block; +} + +.phone-images { + background-color: white; + float: left; + height: 450px; + overflow: hidden; + position: relative; + width: 450px; +} + .phone-thumbs { list-style: none; margin: 0; diff --git a/app/app.module.js b/app/app.module.js index d2ee1bf04..ab6d353ee 100644 --- a/app/app.module.js +++ b/app/app.module.js @@ -2,6 +2,7 @@ // Define the `phonecatApp` module angular.module('phonecatApp', [ + 'ngAnimate', 'ngRoute', 'core', 'phoneDetail', diff --git a/app/index.html b/app/index.html index ef9c965fb..9ab7622ff 100644 --- a/app/index.html +++ b/app/index.html @@ -5,11 +5,16 @@ Google Phone Gallery + + + + + @@ -21,7 +26,9 @@ -
    +
    +
    +
    diff --git a/app/phone-detail/phone-detail.template.html b/app/phone-detail/phone-detail.template.html index 09061ce7a..f48657803 100644 --- a/app/phone-detail/phone-detail.template.html +++ b/app/phone-detail/phone-detail.template.html @@ -1,4 +1,8 @@ - +
    + +

    {{$ctrl.phone.name}}

    diff --git a/app/phone-list/phone-list.template.html b/app/phone-list/phone-list.template.html index b0d81d4cb..90548f9f9 100644 --- a/app/phone-list/phone-list.template.html +++ b/app/phone-list/phone-list.template.html @@ -21,7 +21,8 @@
      -
    • +
    • {{phone.name}} diff --git a/bower.json b/bower.json index 76eb41364..08280c3de 100644 --- a/bower.json +++ b/bower.json @@ -7,9 +7,11 @@ "private": true, "dependencies": { "angular": "1.5.x", + "angular-animate": "1.5.x", "angular-mocks": "1.5.x", "angular-resource": "1.5.x", "angular-route": "1.5.x", - "bootstrap": "3.3.x" + "bootstrap": "3.3.x", + "jquery": "2.2.x" } } diff --git a/e2e-tests/scenarios.js b/e2e-tests/scenarios.js index 072deaf5e..64f9f32d1 100644 --- a/e2e-tests/scenarios.js +++ b/e2e-tests/scenarios.js @@ -78,13 +78,13 @@ describe('PhoneCat Application', function() { }); it('should display the first phone image as the main phone image', function() { - var mainImage = element(by.css('img.phone')); + var mainImage = element(by.css('img.phone.selected')); 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 mainImage = element(by.css('img.phone.selected')); var thumbnails = element.all(by.css('.phone-thumbs img')); thumbnails.get(2).click(); diff --git a/karma.conf.js b/karma.conf.js index 9ef4349ee..b365c8ead 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -6,6 +6,7 @@ module.exports = function(config) { files: [ 'bower_components/angular/angular.js', + 'bower_components/angular-animate/angular-animate.js', 'bower_components/angular-resource/angular-resource.js', 'bower_components/angular-route/angular-route.js', 'bower_components/angular-mocks/angular-mocks.js',