Ståle Pettersen
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
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. |
<html ng-app> <script src="lib/angular.min.js"/> <h1>Hello world!</h1> </html>
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>
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>
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...
All $scope
s are children of $rootScope
.
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.
<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"/>
$scope
changes detected by AngularJS if changed within framework.
$scope.name = 'Ståle'
or $('#name').val('Ståle')
will not update DOM.
$scope.name = 'Ståle'; $scope.$apply();
will update DOM.
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>
<input ng-model="{string}" [name="{string}"] [required] [ng-required="{boolean}"] [ng-minlength="{number}"] [ng-maxlength="{number}"] [ng-pattern="{string}"] [ng-change="{string}"] />
<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>
<h1>{{ heading | uppercase }}</h1> <li ng-repeat="r in res | orderBy:'title':reverse | limitTo:10 | filter:query "> <p>{{r.title}} {{r.date}}</p> </li>
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('
'); }; }) ;
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.'); })); }); });
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) {..} } } });
E
- Element name: <my-directive></my-directive>
A
- Attribute: <div my-directive="exp"></div>
C
- Class: <div class="my-directive: exp;"></div>
M
- Comment: <!-- directive: my-directive exp -->
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)); }); } }; }) ;
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 }); } });
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}}" />
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"); }); });
<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; } }); } };
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)
angular.module('csrfTokenSetup', []) .config(function ($httpProvider) { $httpProvider.defaults.headers.post['X-Requested-By'] = $('[name=csrf-token]').attr('content'); });
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 -->
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); }); });
Default implementation calls $log.error.
weatherApp.factory('$exceptionHandler', function($injector) { return function(exception, cause) { var http = $injector.get('$http'); http.post('/errors', { error: exception }); }; });
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); });
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); });
weatherApp.run(function (TrackingService, $rootScope) { $rootScope.$on( '$routeChangeStart', TrackingService.trackEvent ); })
$rootScope.$broadcast('Event for children', args); $scope.$emit('Event for parents', args); $scope.$on('Event for parents', fn(event, args) );
describe('Weather E2E Test', function() { it('should navigate to start page', function(){ browser().navigateTo('/'); }); it('should enter City', function(){ input('search.city').enter('Oslo'); }); });
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/' };