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. |
1 2 3 4 | < html ng-app> < script src = "lib/angular.min.js" /> < h1 >Hello world!</ h1 > </ html > |
1 | var weatherApp = angular.module( 'weatherApp' , []); |
1 2 3 4 5 6 7 8 9 10 | < 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
1 2 3 4 5 | weatherApp.controller( 'WeatherController' , function ($scope) { $scope.heading = 'Weather forecast' ; $scope.searchTerm = 'Oslo' ; }); |
1 2 3 4 5 6 7 8 | < div ng-app = "weatherApp" > < div ng-controller = "WeatherController" > {{heading}} < form > < input ng-model = "searchTerm" > </ form > </ div > </ div > |
1 2 3 4 5 6 7 8 | 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.
1 2 3 4 5 6 7 8 9 10 11 | < 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | < 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 > |
1 2 3 4 5 6 7 8 9 | <input ng-model= "{string}" [name= "{string}" ] [required] [ng-required= "{boolean}" ] [ng-minlength= "{number}" ] [ng-maxlength= "{number}" ] [ng-pattern= "{string}" ] [ng-change= "{string}" ] /> |
1 2 3 4 5 6 7 8 9 10 11 | < 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 > |
1 2 3 4 5 6 | < h1 >{{ heading | uppercase }}</ h1 > < li ng-repeat = "r in res | orderBy:'title':reverse | limitTo:10 | filter:query " > < p >{{r.title}} {{r.date}}</ p > </ li > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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( '<br>' ); }; }) ; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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<br> line.' ); })); }); }); |
Only place DOM manipulation is allowed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | myApp.directive( 'myDirective' , function (dep1, dep2) { return { replace: true , restrict: 'EACM' , priority: 0, templateUrl: 'directive.html' , template: '<div></div>' , 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | .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 }); } }); |
1 2 3 4 5 6 7 8 9 10 11 | weatherApp.module( 'weatherDirectives' ) .directive( 'waTabindex' , function () { return function (scope, element, attrs) { // link fn scope.$watch(attrs.ngTabindex, function (newVal) { element.attr( 'tabindex' , newVal); }); }; }); |
1 2 | < input name = "someInput" wa-tabindex = "{{tab.index}}" /> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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" ); }); }); |
1 | < input ng-model = "degrees" smart-float/> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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) |
1 2 3 4 5 | 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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(); }); |
1 2 3 | < script src = "mocks/mockServices.js" ></ script > < div ng-app = "weatherAppDev" /> <-- 'weatherApp' for no mocks --> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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.
1 2 3 4 5 6 7 | weatherApp.factory( '$exceptionHandler' , function ($injector) { return function (exception, cause) { var http = $injector.get( '$http' ); http.post( '/errors' , { error: exception }); }; }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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/>
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 ); }); |
1 2 3 4 5 6 7 | weatherApp.run( function (TrackingService, $rootScope) { $rootScope.$on( '$routeChangeStart' , TrackingService.trackEvent ); }) |
1 2 3 | $rootScope.$broadcast( 'Event for children' , args); $scope.$emit( 'Event for parents' , args); $scope.$on( 'Event for parents' , fn(event, args) ); |
1 2 3 4 5 6 7 8 9 10 | 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>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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 = { }; |