From b160462dea8cfc4a78fb627aa5d3f5207dc853cb Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sat, 18 Oct 2014 14:45:31 +0200 Subject: [PATCH 01/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 30fd06fccd526552ecb0cbff38d78241b78c80d9 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sat, 18 Oct 2014 14:45:31 +0200 Subject: [PATCH 02/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' + '!'}}

+ From 38674717761a3213b864fea52a6c0f63fcd93bec 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-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 c1ca5d0a242808fb70fdb61cfee61b18a7e0f462 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 28 Mar 2016 15:22:25 +0300 Subject: [PATCH 04/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 dc86082ea70fdcfafc0c8ed1219e294cb23906ab Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 28 Mar 2016 21:53:57 +0300 Subject: [PATCH 05/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 c2ca489137fd485560f0fe273187b7711dda542f Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 28 Mar 2016 22:58:42 +0300 Subject: [PATCH 06/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 7e60b73abeb31e56befeb711fe558f22fd31f950 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sun, 19 Oct 2014 09:19:49 +0100 Subject: [PATCH 07/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 9b1ee8fb6eb03fd1fa6da54936e0c3646945a23d Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 08/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 baf5a9bdeda72bc8ef1290bf9e9bf4b8b397d408 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-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 53d9314e7a098f5472efcd4f3f0fb3f8c29114fa Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 20 Jan 2016 14:54:29 +0000 Subject: [PATCH 10/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 | 3 ++- 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, 67 insertions(+), 7 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..b4f676f30 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 + '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 4b67b044db7e266b802047045669d6a1e7799e74 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 11/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 aeefb7cc58cf8c7f9ef9211851bb12855d055c0e Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 12/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 b4f676f30..651b0a20b 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 b535c429c0ed1b9bc033d54eb7e1fde960fa2cf7 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 13/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 7122fc77a70c0aabf5d8509d15e60eadbf168df4 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 14/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 0970e05d951d2d1485e5b6700032d9729b77d1df Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 20 Jan 2016 14:55:34 +0000 Subject: [PATCH 15/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 651b0a20b..8c392c4d6 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', From 76bfe2aef881380ced23066a5e8279f83a1f002d Mon Sep 17 00:00:00 2001 From: Felix Zapata Date: Tue, 13 Dec 2016 16:15:12 +0100 Subject: [PATCH 16/16] feat(a11y): improve the accessibility of the project --- README.md | 14 +++++- app/app.css | 29 +++++++++++ app/index.html | 1 + app/phone-detail/phone-detail.template.html | 25 +++++----- app/phone-list/phone-list.directive.js | 42 ++++++++++++++++ app/phone-list/phone-list.template.html | 26 +++++----- e2e-tests/protractor.conf.js | 12 ++++- e2e-tests/scenarios.js | 54 +++++++++++++-------- package.json | 4 +- 9 files changed, 162 insertions(+), 45 deletions(-) create mode 100644 app/phone-list/phone-list.directive.js diff --git a/README.md b/README.md index 36fbee57b..e0ba9dd95 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,18 @@ git diff step-?..step-? - CSS transition animations. - CSS keyframe animations. - JavaScript-based animations. + +### step-15 _Accessibility (a11y)_ + +- Add labels to the search and order fields. +- Add accessibility plugin for Protractor. + - Add missing alt attributes in the phone detail. +- Add aria live regions to inform the user about results after searching and filtering elements. +- Improve access via keyboard: + - Navigate between the images in a phone detail. + - Add headings elements. +- Add information about checkmarks in the phone detail specs. + ## Development with `angular-phonecat` @@ -276,4 +288,4 @@ For more information on AngularJS, please check out https://angularjs.org/. [karma]: https://karma-runner.github.io/ [node]: https://nodejs.org/ [protractor]: http://www.protractortest.org/ -[selenium]: http://docs.seleniumhq.org/ +[protractor-accessibility-plugin]: https://github.com/angular/protractor-accessibility-plugin diff --git a/app/app.css b/app/app.css index f4b45b02a..3854d3bf9 100644 --- a/app/app.css +++ b/app/app.css @@ -7,6 +7,9 @@ h1 { margin-top: 0; } +.aria-status { margin-bottom: 10px; } +.aria-status-order{ margin-bottom: 5px; } + /* View: Phone list */ .phones { list-style: none; @@ -70,6 +73,19 @@ h1 { margin: 1em; } +.phone-thumbs img:focus { + border:2px solid black; +} + +.phone-thumbs img:hover { + cursor: pointer; +} + +.phone-thumbs button { + background: none; + border: 0; + } + .specs { clear: both; list-style: none; @@ -91,3 +107,16 @@ h1 { font-size: 1.2em; font-weight: bold; } + +ul.specs h2 { font-size:1.5em } + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; +} \ No newline at end of file diff --git a/app/index.html b/app/index.html index 9ab7622ff..b10570885 100644 --- a/app/index.html +++ b/app/index.html @@ -20,6 +20,7 @@ + diff --git a/app/phone-detail/phone-detail.template.html b/app/phone-detail/phone-detail.template.html index f48657803..03d92fb08 100644 --- a/app/phone-detail/phone-detail.template.html +++ b/app/phone-detail/phone-detail.template.html @@ -1,5 +1,6 @@
      @@ -8,22 +9,24 @@

      {{$ctrl.phone.name}}

      {{$ctrl.phone.description}}

      +

      Click on each image to see the enlarge version.

      +
      • - +
      • - Availability and Networks +

        Availability and Networks

        Availability
        {{availability}}
      • - Battery +

        Battery

        Type
        {{$ctrl.phone.battery.type}}
        @@ -34,7 +37,7 @@

        {{$ctrl.phone.name}}

      • - Storage and Memory +

        Storage and Memory

        RAM
        {{$ctrl.phone.storage.ram}}
        @@ -43,7 +46,7 @@

        {{$ctrl.phone.name}}

      • - Connectivity +

        Connectivity

        Network Support
        {{$ctrl.phone.connectivity.cell}}
        @@ -58,7 +61,7 @@

        {{$ctrl.phone.name}}

      • - Android +

        Android

        OS Version
        {{$ctrl.phone.android.os}}
        @@ -67,7 +70,7 @@

        {{$ctrl.phone.name}}

      • - Size and Weight +

        Size and Weight

        Dimensions
        {{dim}}
        @@ -76,7 +79,7 @@

        {{$ctrl.phone.name}}

      • - Display +

        Display

        Screen size
        {{$ctrl.phone.display.screenSize}}
        @@ -87,7 +90,7 @@

        {{$ctrl.phone.name}}

      • - Hardware +

        Hardware

        CPU
        {{$ctrl.phone.hardware.cpu}}
        @@ -102,7 +105,7 @@

        {{$ctrl.phone.name}}

      • - Camera +

        Camera

        Primary
        {{$ctrl.phone.camera.primary}}
        @@ -111,7 +114,7 @@

        {{$ctrl.phone.name}}

      • - Additional Features +

        Additional Features

        {{$ctrl.phone.additionalFeatures}}
      diff --git a/app/phone-list/phone-list.directive.js b/app/phone-list/phone-list.directive.js new file mode 100644 index 000000000..2ed354493 --- /dev/null +++ b/app/phone-list/phone-list.directive.js @@ -0,0 +1,42 @@ + +'use strict'; + +/* Directives */ + +angular. + module('phoneList'). + directive('resultList', [function () { + var ariaStatus = document.querySelector('.aria-status'); + return { + restrict: 'A', + link: function ($scope) { + $scope.$watch('filtered.length', function (length) { + if(length === 1) { + ariaStatus.innerHTML = length + ' item found'; + } else if(length > 1) { + ariaStatus.innerHTML = length + ' items found'; + } else { + ariaStatus.innerHTML = 'No items found'; + } + }); + } + } +} +]). + directive('informOrder', [function () { + var ariaStatusSort = document.querySelector('.aria-status-sort'); + return { + restrict: 'A', + link: function ($scope) { + $scope.$watch('$ctrl.orderProp', function (order) { + if(order === 'age') { + ariaStatusSort.innerHTML = 'Items filter by newest' + } else { + ariaStatusSort.innerHTML = 'Items filter by alphabetical order' + } + + }); + } + } + } +]); \ No newline at end of file diff --git a/app/phone-list/phone-list.template.html b/app/phone-list/phone-list.template.html index 90548f9f9..7016920a4 100644 --- a/app/phone-list/phone-list.template.html +++ b/app/phone-list/phone-list.template.html @@ -3,25 +3,29 @@
      -

      - Search: - -

      - -

      - Sort by: - +

      + +
      + +
      + + -

      +
      + +
-