Further AngularJS

Workshop

Pete Bacon Darwin

AngularJS 1.x Team Lead

Get the slides

FoodMe App

Get the code

clone using git

git clone https://github.com/petebacondarwin/foodme-further

or download zip file

https://github.com/petebacondarwin/foodme-further/archive/master.zip 

or copy from USB stick

Install development tools

Make sure you have all these installed

  • node.js
  • npm
  • bower
  • http-server
  • karma
  • protractor

Step 0

Serve the application from a local webserver

  • Install a simple http server: `npm install -g http-server`
  • Start the server: `http-server`
  • Browse to the application: http://localhost:8080/step-01

Jasmine Test Specs

A JavaScript library for writing unit tests

  • describe('...', function() { ... });
  • runBefore(function() { ... });
  • it('should ...', function() { ... });

Angular test helpers

angular-mocks.js provides Jasmine helpers

  • load a module: `module('app'));`
  • get hold of a service: `inject(function($controller) { ... });

Karma Test Runner

http://karma-runner.github.io/0.12/index.html

  • Command line utility / library
  • Runs unit tests in one or more browsers
  • Aggregates and reports the results back

Step 1

Install & configure Karma test runner

Create and run initial unit tests for `AppController`

  • Setup npm locally: `npm init`
  • Install a Karma CLI tool: `npm install -g karma-cli`
  • Install the Karma libraries:
    • `npm install --save-dev karma`
    • `npm install --save-dev karma-chrome-launcher`
    • `npm install --save-dev karma-jasmine`
  • Configure Karma: `karma init`

Step 1 cont.

Write and run unit tests for AppController

  • Create app.spec.js spec file for the `AppController`
  • Add `describe` and `it` blocks
  • Run Karma: `karma start`

ngMock and $httpBackend

Angular provides various mock services to help with testing

$httpBackend.when(...).respond(...);

$httpBackend.flush();

$rootScope.$digest();

Step 2

Create more unit tests for `AppController` methods

  • Add tests for AppController methods
  • Mock the `localStorage` object for testing `user` object
  • Test that the user object has been initialized correctly
  • Mock $http service to return test restaurant data for testing
  • Flush mock $http service to trigger response and test the restaurants property on the controller
  • Modify filters and check that `filteredRestaurants` changes

Syntactic Injection Sugar

When injecting we can wrap params in underscores

inject(function(_$http_) { ... })

 

Getting filters for testing

There are two ways to get hold of a filter

  • Use the $filter service: var ratingFilter = $filter('rating');
  • Have it injected directly : inject(function(ratingFilter) { ... })

Step 3

Create unit tests for `rating` filter

  • Inject $filter to test the ratingFilter
  • Inject the rating filter (as `ratingFilter`) directly, instead
  • Use _underscores_ to assign ratingFilter outside of the current function context

Testing Directives

The general approach is:

  • compile some HTML containing the directive
  • manipulate the scope and DOM and see what happens

Jasmine Matchers

We can clean up our tests by defining custom matchers:

expect(element.find('li')).toHaveClass('selected', 3);

Step 4

Create unit tests for `fmRating` directive

  • Create `fmRating` directive instance using `$compile`
  • Create a `toHaveClass` Jasmine matcher
  • Use the `toHaveClass` to test number of icons displayed
  • Change rating on scope to test that the HTML changes
  • Simulate clicking the directive to test changing the rating

Routing

ngRoute

Routes

  • A route maps a URL (pattern) to a view
  • The view is described by an HTML template
  • Routes can have a controller and parameters
  • Routes are defined on the `$routeProvider`
  • Views are displayed via `ng-view` directive

Step 5

Implement routing to enable multiple URL driven views

 

  • Load the `../js/angular-route.js` file
  • Add `ngRoute` module as a dependency of `app` module
  • Add `angular-route.js` to the files to load in karma config

Step 5 cont.

Move the current HTML into a new `restaurants` view

  • Create a new `components/restaurants/index.html` template for the current HTML
  • Remove the restaurant specific HTML from index.html and replace with an `ng-view` directive
  • Configure the application to display this component when we navigate to the root of the application
  • Check that the karma tests still pass

Step 6

Add additional static views and navigation links

  • Create `components/help/index.html` template
  • Create `components/about-us/index.html` template
  • Create `components/how-it-works/index.html` template
  • Configure the application routing to display these views
  • Add links to these views in the navigation panel
  • Check that the karma and protractor tests still pass

Step 7

Install & configure protractor

Create and run basic e2e tests

  • Install protractor and webdriver
  • Ensure that the http server is running
  • Create protractor.conf.js in the step folder
  • Exclude the `protractor.conf.js` from the karma config
  • Create `e2e/app.spec.js` protractor spec file
  • Execute the protractor specs from the step folder

Step 8

Use a PageObject to make e2e tests clearer

  • Create a `HomePage` PageObject for the app
  • Import and use `HomePage` in the app.spec.js e2e file

Step 9

Move `restaurants` route and logic into its own module

  • Add `restaurants` module in
    components/restaurants/index.js
  • Load this new `index.js` file in index.html
  • Add `restaurants` module as `app` module dependency
  • Add `RestaurantsController` to the `restaurants` module
  • Remove this logic from the `AppController`
  • Move the route config into the `restaurants` module

Step 9 cont.

Update the views and tests

  • Update the view to use the new `RestaurantsController`
  • Update the HomePage PageObject to use new controller
  • Check that the protractor tests still pass
  • Move the restaurant specific unit tests from the `AppController` into the `RestaurantsController`
  • Update the karma config to load these new files

$location

$location.path() provides the relative path to the current route

Step 10

Add CSS classes to show currently selected route

  • Create a `NavigationController` in app.js, which provides `routeIs()` helper
  • Add unit tests for this controller to app.spec.js
  • Check that the unit tests still pass
  • Use controller to the navigation bar with `ng-controller`

ngAnimate

Provides hooks to trigger animations declaratively

  • Built-in directives already support animations:
    • ng-repeat, ng-if, ng-show, ng-hide, ng-select
  • Animations can be defined via:
    • CSS Transition
    • CSS Keyframe Animation
    • JavaScript callback Animation

CSS transitions

Applying a CSS class to an element, in combination with ngAnimate specific CSS classes, allows you to trigger animations with an animation enabled directive  

  • ngRepeat: ng-enter, ng-leave, ng-move
  • ngShow/ngHide: ng-hide-add, ng-hide-remove
  • ngClass: my-class-add my-class-remove

Step 10 cont.

Use ngAnimate to animate navigation 

  • Load `angular-animate.js` in index.html
  • Add `ngAnimate` as a dependency of the `app` module
  • Add `angular-animate.js` to the Karma config
  • Check that the unit tests still pass

Route resolve values

Routes can have resolves

 

 

 

 

 

These are promises that must be resolved before the route change can take place

The resolved values are available to the route's controller


  $routeProvider
    .when('/restaurants', {
      templateUrl: 'components/restaurants',
      controller: 'RestaurantsController as component',
      resolve: {
        restaurants: 'restaurantListPromise'
      }
    });

Step 11

Refactor restaurant data management into a service

  • Create a `restaurantListPromise` service in the `restaurants` module
  • Add a resolve to the `/restaurants` route to inject the list of restaurants into the controller
  • Update the `RestaurantsController` to use this
  • Move unit tests into `restaurantListPromise` service
  • Inject mock restaurant data directly into the controller
  • Check that the unit and e2e tests still pass

Route Params

Routes can be parameterized to match more than one URL:

 

 

 

The value of the parameters are made available on the $routeParams service


      .when('/restaurants/:id', { ... });

Step 12

Add a new view for a single restaurant showing its menu

  • Add a new restaurant menu route, with a parameter `:id`
  • Create a new view for this route at `components/restaurants/menu.html`
  • Create a `MenuController` for this route, which uses the resolved restaurant data
  • Update the restaurant list view to link restaurant menus

More Directives

Isolated Scope

If a directive declares isolated scope
scope: { ... }

then a new isolated scope is created at that element and the template of the directive is bound to this new scope

bindToController

You can map attributes on the element to properties on the controller

 

 

 

  • `@` means one-way interpolation
  • `=` means two-way binding
  • `&` means expose callback
  
    bindToController: {
      user: '=deliverTo'
    },

Step 13

Refactor the delivery info form and display box into a reusable `fmDeliverTo` directive

  • Create `fmDeliverTo` module and `fmDeliverTo` directive in `fmDeliverTo.js`
  • Move `fmDeliverTo` logic from the `AppController` into `FmDeliverToController`
  • Move the `fmDeliverTo` HTML from `index.html` into `fmDeliverTo.template.html`
  • Move the unit test logic from the `AppController` spec to the `FmDeliverToController` spec

Template Directive Tests

karma-ng-html2js-preprocessor

  • It is not easy to load up templates when testing due to ngMock $httpBackend
  • Instead we can add the template to the $templateCache programmatically
  • We can automate this with a karma preprocessor

Step 13 cont.

Test the `fmDeliverTo` directive

Use `fmDeliverTo` in restaurant and menu views

 

  • Install and configure the Karma HTML to JavaScript preprocessor
  • Write unit tests for the `fmDeliverTo` directive
  • Use the `fmDeliverTo` directive in the restaurant and menu views
  • Update the `HomePage` Protractor page object for the new user bindings

Step 14

Provide a Shopping Cart to store items from the menu

  • Create a `shoppingCart` module, which depends upon `localStorage`
  • Load the `shoppingCart` module in index.html
  • Add `shoppingCart` as a dependency of the `app`
  • Create an `alert` service to wrap the browser's `alert` function for easier testing
  • Create a `shoppingCart` service that stores the cart info in `localStorage`

Step 14 cont.

Provide a Shopping Cart to store items from the menu

  • Create a `ShoppingCartController` that exposes the `shoppingCart` and helper methods
  • Attach the `ShoppingCartController` to the menu view 
  • Add a new item to the cart from the menu items when the plus sign is clicked
  • Display the shopping cart items and total in the side panel

Injecting Services

Services are injected based on named parameters:

 

 

But if you minimize the code then you lose the name

 

 

so you must annotate

 

 

or

 function localStorageBinding(localStorage, $rootScope) { ... }

 function localStorageBinding(a, b) { ... }

 ['localStorage', '$rootScope', function localStorageBinding(localStorage, $rootScope) { ... }]

 localStorageBinding.$inject = ['localStorage', '$rootScope'];

Step 15

Add injection annotations to prevent minification issues

  • Install `gulp` and `gulp-ng-annotate`
  • Create a gulp build file to add the annotation adder
  • Run the `default` gulp task
  • Add `ng-strict-di` directive to ensure all components are correctly annotated

Further AngularJS 2015

By Pete Bacon Darwin

Further AngularJS 2015

  • 2,184