AngularJS Workshop

Ståle Pettersen

AngularJS Workshop

Ståle Pettersen / @kozmic

May 14th, 2013

Lates version of slides at: http://kozmic.github.com/angularjs-workshop/

One version of the weather app: https://github.com/kozmic/angularjs-workshop

  1. AngularJS stuff
    • Tools
    • Bootstrapping Angular
    • Controllers
    • Binding
    • Hands on!
    • Forms
    • Directives, filters, routing and more!
    • Hands on!
    • f00dz

Disclaimer:

I consider myself an AngularJS n00b

...even with 9 months experience

...I learn something new each week

...there will be questions I can't answer

...we'll Google together! :)

Source: Ben Nadel

AngularJS is a drama queen!

Tools

Useful resources

MVC-ish

Model: Plain Javascript
View: HTML templates (including custom components) bound to model
Controller: Exposes the model to the view by setting model on $scope which is watched for changes. Controller usually has helper methods to view.

Hello world

            <html ng-app>
              <script src="lib/angular.min.js"/>
              <h1>Hello world!</h1>
            </html>
        

Basic app

            var weatherApp = angular.module('weatherApp', []);
        
            <html>
            <body>
                <script src="lib/angular.min.js"/>
                <script src="js/app.js"/>

                <div ng-app="weatherApp">
                    <!-- Angular World -->
                </div>
            </body>
            </html>
        

Basic controller with binding

Exposes data and helper functions to view

                weatherApp.controller('WeatherController', function($scope) {

                    $scope.heading = 'Weather forecast';
                    $scope.searchTerm = 'Oslo';
                });
            
                <div ng-app="weatherApp">
                    <div ng-controller="WeatherController">
                        {{heading}}
                        
</div> </div>

Dependency injection

                angualar.module('myModule', [dep])
                  .config(function(dep){})
                  .run(function(dep) {});
                  .service('serviceName', function(dep) {})
                  .factory('factoryName', function(dep) {})
                  .directive('directiveName', function(dep) {})
                  .filter('filterName', function(dep) {})
                  // and more...
            

Scopes

All $scopes are children of $rootScope.

Binding concepts

The AngularJS $scope variable acts as a big key/value storage hash which is internally looped on a timeout interval and dirty checked against it's former value(s) each time a digestion occurs.

Binding examples (directives)

                <h1>{{model}}</h1>

                <span ng-bind="model" /> <-- ngCloak fix -->

                <input ng-model="model" />

                <div ng-bind-html="model" /> <-- ngSanitize -->

                <div ng-bind-html-unsafe="model" /> <-- ngSanitize -->

				<CmsText data-key=helpText.name"/>
            

Gotcha

Try

Built in directives

Directives (aka components): Extending HTML with your own types, can be simple or crazy complex.

                <li ng-repeat="result in results">
                    <p>{{result.title}} {{result.date}}</p>
                </li>

                <div ng-show="searchResults">
                    {{searchResults}}
                </div>

                <div ng-include src="'templates/search.html'"></div>

                <ng-pluralize count="searchResults.length"
                              when="{'0': 'No results.',
                                   'one': 'Only one result for your search..',
                                 'other': '{} results found.'}" />

				<div ng-class="{answered: question.answer}">

				<h2 ng-show="title">{{title}}</h2>
            

Example: Built in input directive

            <input
                ng-model="{string}"
               [name="{string}"]
               [required]
               [ng-required="{boolean}"]
               [ng-minlength="{number}"]
               [ng-maxlength="{number}"]
               [ng-pattern="{string}"]
               [ng-change="{string}"] />

            

Forms

                <form name="weatherForm" ng-submit="submitForm()">
                    <input name="city" type="input"
                           placeholder="city"
                           ng-model="weather.city"
                           ng-required="true">

                    <span ng-show="weatherForm.city.$error.required">Required</span>

                    <input type="radio" ng-model="weather.unit" value="celcius"/> Celcius
                    <input type="radio" ng-model="weather.unit" value="fahrenheit"/> Fahrenheit
                </form>
            

Filters

                <h1>{{ heading | uppercase }}</h1>
				<li ng-repeat="r in res | orderBy:'title':reverse | limitTo:10 | filter:query ">
				   <p>{{r.title}} {{r.date}}</p>
				</li>

			

Custom filters

			weatherApp.module('weatherFilters', [])

              // <span>{{ temperature | postfix:' C' }}</span>
			  .filter('postfix',function(){
					return function(value, postfix) {
						return !!value ? value + postfix : value;
					};
			  })

              // <div ng-bind-html="textWithNewlines | nl2br"/>
			  .filter('nl2br', function() {
					return function(input) {
						return ('' + input).split('\n').join('
'); }; }) ;

Testing filters

			describe('weatherFilters', function() {

			beforeEach(angular.mock.module('weatherFilters'));

			describe('nl2br filter', function() {

				it('should not modify string without a newline', inject(function($filter) {
					var input = 'my input line.';
					var nl2brFilter = $filter('nl2br')(input);
					expect(nl2brFilter).toBe(input);
				}));

				it('should replace newlines with a br tag', inject(function($filter) {
					var input = 'my input\n line.';
					var nl2brFilter = $filter('nl2br')(input);
					expect(nl2brFilter).toBe('my input
line.'); })); }); });

Custom directives: Two categories

Directive API

Only place DOM manipulation is allowed.

            myApp.directive('myDirective', function(dep1, dep2) {
              return {
                replace: true,
                restrict: 'EACM',
                priority: 0,
                templateUrl: 'directive.html',
                template: '
', scope: { localName:'@' }, controller: function($scope, $element, $attrs, $transclude, dep1) {}, transclude: true, compile: function compile(tElement, tAttrs, transclude) { }, link: function postLink(scope, iElement, iAttrs) { scope.$watch('name', function(newVal, oldVal) {..} } } });

Using directives

Custom directive

DateFormatter directive:

				// Example usage:  <p class="DateFormatter" data-dato="{{myDate}}" />
				weatherApp.directive('DateFormatter', function(DateUtilService) {
					return {
						replace: false
						restrict: 'C',
						link: function($scope, el, attrs) {
							var inputFormat = attrs.inputFormat ? attrs.inputFormat : 'd.M.yyyy';
							var outputFormat = attrs.outputFormat ? attrs.outputFormat : 'd. MMMM yyyy';

							attrs.$observe('dato', function(dato) {
								el.text(DateUtilService.convertFormat(dato, inputFormat, outputFormat));
							});
						}
					};
				})
				;
			

$watch and $observe

Mainly for directives.

            .directive('custom', function () {
              return {
                link: function (scope, iElement, iAttrs) {

                  // Attributes only (interpolated)
                  iAttrs.$observe('myAttr', function (value) {
                    scope.myAttrValue = value;
                  });

                  // Scope and attributes
                  scope.$watch(iAttrs['myAttr'], function (newVal, oldVal, scope) {
                    scope.myAttrValue = value;
                  }, true); // Object equality vs reference
                });
              }
            });
            

Custom directive

				weatherApp.module('weatherDirectives')

				  .directive('waTabindex', function() {

						return function(scope, element, attrs) { // link fn
							scope.$watch(attrs.ngTabindex, function (newVal) {
								element.attr('tabindex', newVal);
							});
						};
					});
			
				<input name="someInput" wa-tabindex="{{tab.index}}" />
			

Testing directive

					describe("wa-tabindex directive test", function () {
					var $compile, $rootScope, element;
					beforeEach(angular.mock.module('weatherDirectives'));
					beforeEach(inject(function ($c, $r) {
							$compile = $c;
							$rootScope = $r;
						}
					));

					beforeEach(function () {
						element = $compile('<div><input wa-tabindex="index"></input></div>')($rootScope);
						$rootScope.tabindex = 1337;
						$rootScope.$digest();
					});

					it("should write tabindex on initialization", function () {
						expect(angular.element(element.html()).attr('tabindex')).toEqual("1337");
					});

					it("should update tabindex", function () {
						$rootScope.tabindex = 1338;
						$rootScope.$digest();
						expect(angular.element(element.html()).attr('tabindex')).toEqual("1338");
					});
				    });
					

Customizing formatter/parser

Form validation directive

                <input ng-model="degrees" smart-float/>
            
                var FLOAT_REGEXP = /^\-?\d+((\.|\,)\d+)?$/;

                app.directive('smartFloat', function() {
                  return {
                    require: 'ngModel',
                    link: function(scope, elm, attrs, ctrl) {

                      ctrl.$parsers.unshift(function(viewValue) {

                        if (FLOAT_REGEXP.test(viewValue)) {
                          ctrl.$setValidity('float', true);
                          return parseFloat(viewValue.replace(',', '.'));

                        } else {
                          ctrl.$setValidity('float', false);
                          return undefined;
                        }
                      });
                    }
                  };
            

$http

Returns promise with success and error (no complete). Use $resource instead of $http.

                 // method, url, params, headers, transformers, cache, timeout
				$http(config)

				$http.get('/weather').success(successCallback);
				$http.post('/weather', data).success(successCallback);
			

Resources

Abstraction on top of $http, angular-resources.js is not bundled in angular.js.

First empty object/list, then backend result.

                weatherApp.factory('WeatherService', function($resource) {
				  return $resource('/weather/:city',{ city: '@city' });
                }

                weatherApp.controller('a', function($scope, WeatherService){}

				WeatherService.get({city: 'Oslo'})   // GET, isArray=false
				WeatherService.query({city: 'Oslo'}) // GET, isArray=true
				WeatherService.save(myWeather)       // POST
				WeatherService.save({ city: 'Oslo' }, body)  // POST
				WeatherService.remove(myWeather)     // DELETE
				WeatherService.delete(myWeather)     // DELETE

                var Oslo = WeatherService.get({city: 'Oslo'})
                Oslo.temp = 100;
                Oslo.$save();

				// With callbacks, not typically used:
				WeatherService.save(myWeather, success, error)
			

CSRF token in all POST requests

			angular.module('csrfTokenSetup', [])
				.config(function ($httpProvider) {
					$httpProvider.defaults.headers.post['X-Requested-By'] = $('[name=csrf-token]').attr('content');
				});
			

Dev/Mock/Test $http/$resource

Mock responses for development with mocked backeds (or testing)

			var weatherAppDev = angular.module('weatherAppDev', ['weatherApp', 'ngMockE2E']);
			weatherAppDev.run(function ($httpBackend) {

				$httpBackend.whenGET('/weather\/\w*$').respond(window.weatherResult_Oslo);
				$httpBackend.whenPOST('/weather$').respond(function (method, url, data, headers) {
					if (angular.fromJson(data).weather.wind === 100) {
						return ['500', '', ''];
					} else {
						return ['200', window.weatherResult_Oslo, ''];
					}
				});


				$httpBackend.whenGET(/.*/).passThrough();
				$httpBackend.whenPOST(/.*/).passThrough();
			});
			
				<script src="mocks/mockServices.js"></script>
				<div ng-app="weatherAppDev"/> <-- 'weatherApp' for no mocks -->
			

Setup for testing controller with mocked backend

            describe('WeatherController', function() {

              describe('WeatherController', function(){
                var scope, ctrl, httpBackend, defaultCity = 'Oslo';

                beforeEach(inject(function($httpBackend, $rootScope, $controller) {
                  httpBackend = $httpBackend;
                  httpBackend.expectJSONP('/weather?q=' + defaultCity).
                      respond({city: 'Oslo', weather: 'Sun' });

                  scope = $rootScope.$new();
                  scope.search = { city: defaultCity };
                  ctrl = $controller(PhoneListCtrl, {$scope: scope});

                  httpBackend.flush();
                }));

                it('should fetch results for Oslo at initialization', function() {
                    expect(scope.result.city).toBe(defaultCity);
                });
            });
            

Wrappers

Error handling

Default implementation calls $log.error.

                weatherApp.factory('$exceptionHandler', function($injector) {

                    return function(exception, cause) {
                        var http = $injector.get('$http');
                        http.post('/errors', { error: exception });
                    };
                });
            

Generic interceptor

weatherApp.config(function ($httpProvider) {
	var errorInterceptor = function ($q, $location) {

		function success(response) {
			return response;
		}

		function error(response) {
			if (response.status < 200 || response.status >= 400) {
				$location.path("/error");
				return $q.reject(response);
			}
		}

		return function (promise) {
			return promise.then(success, error);
		};

	};

	$httpProvider.responseInterceptors.push(errorInterceptor);
});

			

Routes

Loads partials into <ng-view/>.

                angular.module('myRoutes',[]).config(function($routeProvider, $locationProvider) {
                  $routeProvider
					.when('/weather', {
					  templateUrl: 'search.html',
					  controller: SearchCtrl
					})
					.when('/weather/:city', {
						templateUrl: 'result.html',
						controller: ResultCtrl
					})

					.when('/404', { templateUrl: '404.html' })

					.otherwise( { redirectTo: '/404' });
    
                    $locationProvider.html5Mode(true);
                });
            

Listen on route change events

				weatherApp.run(function (TrackingService, $rootScope) {
				  $rootScope.$on(
					'$routeChangeStart',
					TrackingService.trackEvent
				  );
				})
			

Events

                $rootScope.$broadcast('Event for children', args);
                $scope.$emit('Event for parents', args);
                $scope.$on('Event for parents', fn(event, args) );
            

End to end test (e2e)

            describe('Weather E2E Test', function() {

                it('should navigate to start page', function(){
                    browser().navigateTo('/');
                });

                it('should enter City', function(){
                    input('search.city').enter('Oslo');
                });
            });
            

End to end test karma config

karma start weather-e2e.conf.js>

            files = [
                ANGULAR_SCENARIO,
                ANGULAR_SCENARIO_ADAPTER,
                /* E2E Test*/
                '../test/e2e/weather-e2e-test.js'
            ];

            // CI:
            singleRun = true;
            autoWatch = false;
            browsers = ['PhantomJS'];

            // Local:
            //autoWatch = true;
            //singleRun = false;
            //browsers = ['Chrome'];

            urlRoot = '/__karma/';

            proxies = {
                '/':'http://localhost:30000/'
            };
            

Tasks

https://github.com/kozmic/angularjs-workshop