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
-
Thanks!
- Keep in touch: pete@bacondarwin.com
- Find out more: https://doc.angularjs.org
- Get involved: http://angularconnect.com
Further AngularJS 2015
By Pete Bacon Darwin
Further AngularJS 2015
- 2,184