Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Angular.js Workshop (PyCon SG 2014)

Angular.js Workshop (PyCon SG 2014)

51567a4f786cd8a2c41c513b592de9f9?s=128

David Cramer

June 18, 2014
Tweet

Transcript

  1. David Cramer twitter.com/zeeg Flask and Angular

  2. None
  3. In a nutshell, it's MAGIC

  4. <div ng-repeat="post in postList"> <div class="page-header"> <h2> <a href="/#/posts/{{ post.id

    }}">{{ post.title }}</a> </h2> </div> <div class="post-body">{{ post.body }}</div> </div>
  5. What are we doing?

  6. ๏ A LOT of code (mostly JS + Angular) ๏

    A fairly ugly ”modern” blog ๏ API-driven backend, client-driven frontend ๏ Basic dependency management organization ๏ Testing principles (in Flask and Angular)
  7. Questions? Interrupt Me!

  8. Bootstrapping

  9. ☑ Python 2.6+

  10. ☑ Node.js (NPM)

  11. ☑ Virtualenv

  12. # git clone https://github.com/dcramer/ pyconsg-tutorial-bootstrap.git http://git.io/pjNCxA

  13. $ less README.rst Setup the environment: virtualenv ./env source ./env/bin/activate

    Install dependencies: # python pip install -e . # node (tools) npm install # bower (js 3rd party) node_modules/.bin/bower install
  14. $ less setup.py # ... tests_require = [ 'pytest>=2.5.0,<2.6.0', ]

    install_requires = [ 'blinker>=1.3,<1.4', 'flask>=0.10.1,<0.11.0', 'flask-restful>=0.2.12,<0.3.0', 'flask-sqlalchemy>=1.0,<1.1', 'sqlalchemy==0.9.4', ] # ...
  15. $ less package.json { "name": "pyconsg-tutorial-bootstrap", "private": true, "version": "0.0.0",

    "devDependencies": { "bower": "^1.3.1", "karma": "~0.10" }, "scripts": { "postinstall": "bower install", "pretest": "npm install", "test": "karma start tests/karma.conf.js" } }
  16. $ less bower.json { "name": "pyconsg-tutorial-bootstrap", "version": "0.0.0", "ignore": [

    "**/.*", "node_modules", "static/vendor", "tests" ], "dependencies": { "angular": "~1.2.17", "bootstrap": "~3.1.1" }, "resolutions": { "angular": "1.2.17" } }
  17. $ less .bowerrc { "directory": "static/vendor" }

  18. Config blog/config.py

  19. $ less blog/config.py # ... def create_app(**config): app = Flask(

    __name__, static_folder=os.path.join(PROJECT_ROOT, 'static'), template_folder=os.path.join(PROJECT_ROOT, 'templates'), ) app.config['INDUCE_API_DELAY'] = True app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/blog.db' app.config['DEBUG'] = True app.config.update(config) db.init_app(app) configure_api_routes(app) configure_web_routes(app) return app
  20. $ less blog/config.py # ... def configure_web_routes(app): from .web.index import

    IndexView app.add_url_rule( '/', view_func=IndexView.as_view('index'))
  21. $ less blog/config.py # ... def configure_api_routes(app): from .api.post_details import

    PostDetailsResource from .api.post_index import PostIndexResource api.add_resource(PostIndexResource, '/posts/') api.add_resource(PostDetailsResource, '/posts/<post_id>/') api.init_app(app)
  22. Data Models blog/models/*

  23. $ less blog/models/post.py from datetime import datetime from blog.config import

    db class Post(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(80)) body = db.Column(db.Text) pub_date = db.Column(db.DateTime)
  24. REST API blog/api/*

  25. $ less blog/api/post_index.py from blog.api.base import Resource class PostIndexResource(Resource): def

    get(self): """ Return a list of posts. """ def post(self): """ Create a new post. """
  26. $ less blog/api/post_details.py from blog.api.base import Resource class PostDetailsResource(Resource): def

    get(self, post_id): """ Return information about a given post. """ def post(self, post_id): """ Edit an existing post. """
  27. Base Layout templates/index.html

  28. $ less templates/index.html <!doctype html> <html> <head> <base href="{{ url_for('static',

    filename='partials/') }}"> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Blog</title> <link href="{{ url_for('static', filename='css/styles.css') }}” rel="stylesheet" media="screen"> <script src="{{ url_for('static', filename='vendor/angular/angular.js') }}"/> <script src="{{ url_for('static', filename='js/app.js') }}"/> </head> <body ng-app="blog"> <section class="container"> <header> <h1><a href="/#/">My Blog</a></h1> </header> <section ng-view></section> <footer>&copy; My Blog</footer> </section> </body> </html>
  29. Client Application static/js/app.js

  30. $ less static/js/app.js 'use strict'; angular.module('blog', []).config(function() { // TODO:

    Initialize routes/general application config here });
  31. Python Test Config tests/conftest.py

  32. @pytest.fixture(scope='session') def app(request): app = create_app( SQLALCHEMY_DATABASE_URI='sqlite:///', INDUCE_API_DELAY=False, ) app_context

    = app.test_request_context() app_context.push() return app
  33. @pytest.fixture(autouse=True) def setup_db(request, app): db.create_all() request.addfinalizer(db.drop_all) @pytest.fixture(autouse=True) def db_session(request): request.addfinalizer(db.session.remove)

  34. @pytest.fixture(scope='session') def client(app): return app.test_client()

  35. Fixtures are automatic Dependency Injection

  36. $ py.test tests/

  37. ============================= test session starts ============================== platform darwin -- Python 2.7.6

    -- py-1.4.20 -- pytest-2.5.2 collected 1 items tests/python/api/test_post_index.py F =================================== FAILURES =================================== __________________________________ test_list ___________________________________ tests/python/api/test_post_index.py:31: in test_list > assert len(data) == 2 E TypeError: object of type 'NoneType' has no len() =========================== 1 failed in 0.11 seconds ==========================
  38. JavaScript Test Config tests/karma.conf.js

  39. module.exports = function(config){ config.set({ basePath: '../', files: [ 'static/vendor/angular/angular.js', //

    'static/vendor/angular-route/angular-route.js', // 'static/vendor/angular-mocks/angular-mocks.js', 'static/js/**/*.js', 'tests/js/**/*.js' ], autoWatch: true, frameworks: ['jasmine'], browsers: ['Chrome'], plugins: [ 'karma-chrome-launcher', 'karma-jasmine' ] }); };
  40. $ npm run test

  41. INFO [karma]: Karma v0.10.10 server started at http://localhost:9876/ INFO [launcher]:

    Starting browser Chrome INFO [Chrome 35.0.1916 (Mac OS X 10.10.0)]: Connected on socket 0GvlxTukdDRVPKHoJxdB Chrome 35.0.1916 (Mac OS X 10.10.0) controllers encountered a declaration exception FAILED ReferenceError: module is not defined at null.<anonymous> (/Users/dcramer/Development/pyconsg-tutorial-bootstrap/ tests/js/controllersSpec.js:4:14) at /Users/dcramer/Development/pyconsg-tutorial-bootstrap/tests/js/ controllersSpec.js:3:1 Chrome 35.0.1916 (Mac OS X 10.10.0): Executed 1 of 1 (1 FAILED) (0 secs / 0.012 Chrome 35.0.1916 (Mac OS X 10.10.0): Executed 1 of 1 (1 FAILED) ERROR (0.153 secs / 0.012 secs)
  42. Basic JS Debugging Chrome Developer Tools

  43. View > Developer > JavaScript Console

  44. Error: [$injector:unpr] http://errors.angularjs.org/1.2.18/$injector/unpr? p0=NaNoobarProvider%20%3C-%20%24foobar at Error (native) at http://localhost:5000/static/vendor/angular/angular.min.js:6:450 at

    http://localhost:5000/static/vendor/angular/angular.min.js:36:145 at Object.c [as get] (http://localhost:5000/static/vendor/angular/ angular.min.js:34:236) at http://localhost:5000/static/vendor/angular/angular.min.js:36:213 at c (http://localhost:5000/static/vendor/angular/angular.min.js:34:236) at d (http://localhost:5000/static/vendor/angular/angular.min.js:34:453) at Object.instantiate (http://localhost:5000/static/vendor/angular/ angular.min.js:35:103) at http://localhost:5000/static/vendor/angular/angular.min.js:67:284 at link (http://localhost:5000/static/vendor/angular-route/angular- route.min.js:7:248)
  45. WARNING: Errors will be obscure!

  46. Let’s start building!

  47. # create the database $ bin/create-db # load sample data

    $ bin/load-mocks # run the webserver $ bin/web
  48. None
  49. The Post List

  50. $ less blog/api/post_index.py from blog.api.base import Resource from blog.models import

    Post class PostIndexResource(Resource): def get(self): """ Return a list of posts. """ post_list = Post.query.order_by( Post.pub_date.desc() )[:10] results = [] for post in post_list: results.append({ 'id': post.id, 'title': post.title, 'body': post.body, 'pubDate': post.pub_date.isoformat(), }) return results
  51. $ less tests/python/api/test_post_index.py import json from datetime import datetime from

    blog.config import db from blog.models import Post def test_list(client): post1 = Post( title='Hello world!', body='Lorem ipsum dolor sit amet, consectetur adipiscing elit.', pub_date=datetime(2013, 9, 19, 22, 15, 24), ) db.session.add(post1) post2 = Post( title='Hello world (again)!', body='Integer ullamcorper erat ac aliquam mollis.', pub_date=datetime(2013, 9, 20, 22, 15, 24), ) db.session.add(post2) db.session.commit() resp = client.get('/api/0/posts/') assert resp.status_code == 200 data = json.loads(resp.data.decode('utf-8')) assert len(data) == 2 assert data[0]['id'] == post2.id assert data[1]['id'] == post1.id
  52. $ py.test tests/

  53. $ vi static/js/controllers.js 'use strict'; angular.module('blog.controllers', []) .controller('PostListCtrl', function($scope, $http){

    $http.get('/api/0/posts/') .success(function(data){ $scope.postList = data; }); });
  54. A note on IE support

  55. // The preferred longer-version of dependency declaration. controller(['$http', function($http) {}]);

    // The shorter version we're going to be using, which // does not work with Internet Explorer. controller(function($http) {});
  56. $ vi static/partials/post-list.html <ul> <li ng-repeat="post in postList"> <a href="/#/posts/{{

    post.id }}">{{ post.title }}</a> </li> </ul>
  57. $ vi static/js/app.js 'use strict'; angular.module('blog', [ // add ngRoute

    dependency 'ngRoute', // add our controller dependency 'blog.controllers' ]).config(function($routeProvider) { $routeProvider .when('/', { templateUrl: 'post-list.html', controller: 'PostListCtrl' }); });
  58. # we need ngRoute! $ node_modules/.bin/bower install "angular- route#1.2.17" --save

  59. $ less bower.json { // ... "dependencies": { "angular": "~1.2.17",

    "bootstrap": "~3.1.1", "angular-route": "1.2.17" } }
  60. $ ls static/vendor/angular-route README.md angular-route.js angular-route.min.js angular-route.min.js.map bower.json

  61. $ less templates/index.html <!-- ... --> <script src="{{ url_for('static', filename='vendor/angular/angular.js')

    }}"></script> <script src="{{ url_for('static', filename='vendor/angular-route/angular-route.js') }}"></script> <script src="{{ url_for('static', filename='js/app.js') }}"></script> <script src="{{ url_for('static', filename='js/controllers.js') }}"></script> <!-- ... -->
  62. None
  63. The Post Details

  64. $ less blog/api/post_details.py from blog.api.base import Resource from blog.models import

    Post class PostDetailsResource(Resource): def get(self, post_id): """ Return information about a given post. """ post = Post.query.get(post_id) if post is None: return '', 404 return { 'id': post.id, 'title': post.title, 'body': post.body, 'pubDate': post.pub_date.isoformat(), }
  65. $ vi tests/python/api/test_post_details.py import json from blog.config import db from

    blog.models import Post def test_valid_post_on_get(client): post = Post( title='Hello world!', body='Lorem ipsum dolor sit amet, consectetur adipiscing elit.', ) db.session.add(post) db.session.commit() resp = client.get('/api/0/posts/{0}/'.format(post.id)) assert resp.status_code == 200 assert resp.headers['Content-Type'] == 'application/json', resp.data assert json.loads(resp.data.decode('utf-8')) == { 'id': post.id, 'title': post.title, 'body': post.body, 'pubDate': post.pub_date.isoformat(), }
  66. $ py.test tests/

  67. $ vi static/js/controllers.js angular.module('blog.controllers', ['ngRoute']) .controller('PostListCtrl', function($scope, $http){ $http.get('/api/0/posts/') .success(function(data){

    $scope.postList = data; }); }); .controller('PostDetailsCtrl', function($scope, $routeParams, $http){ $http.get('/api/0/posts/' + $routeParams.post_id + '/') .success(function(data){ $scope.post = data; }); });
  68. $ vi static/partials/post-details.html <h1> {{ post.title }} </h1> {{ post.body

    }}
  69. $ vi static/js/app.js $routeProvider .when('/', { templateUrl: 'post-list.html', controller: 'PostListCtrl'

    }) .when('/posts/:post_id', { templateUrl: 'post-details.html', controller: 'PostDetailsCtrl' });
  70. None
  71. Let’s refactor a bit..

  72. Using Resolvers Preload Dependencies

  73. $ vi static/js/app.js .when('/', { templateUrl: 'post-list.html', controller: 'PostListCtrl', resolve:

    { postListResponse: function($http) { return $http.get('/api/0/posts/'); } } })
  74. $ vi static/js/controllers.js angular.module('blog.controllers', []) .controller('PostListCtrl', function($scope, postListResponse){ $scope.postList =

    postListResponse.data; })
  75. $ vi static/js/app.js .when('/posts/:post_id', { templateUrl: 'post-details.html', controller: 'PostDetailsCtrl', resolve:

    { postDetailsResponse: function($http, $route) { var postId = $route.current.params.post_id; return $http.get('/api/0/posts/' + postId + '/'); } } });
  76. $ vi static/js/controllers.js angular.module('blog.controllers', []) .controller('PostListCtrl', function($scope, postListResponse){ $scope.postList =

    postListResponse.data; }) .controller('PostDetailsCtrl', function($scope, postDetailsResponse){ $scope.post = postDetailsResponse.data; });
  77. Testing Controllers

  78. $ vi tests/js/controllersSpec.js 'use strict'; describe('controllers', function(){ beforeEach(module('blog.controllers')); describe('PostListCtrl', function(){

    it('should bind postList', function(){ // TODO }); }); });
  79. # we need ngMock! $ node_modules/.bin/bower install "angular- mocks#1.2.17" --save

  80. $ vi tests/js/controllersSpec.js it('should bind postList', inject(function($controller){ var samplePost =

    {id: 1, title: 'Test', body: 'Foo bar'}; var $scope = {}; var ctrl = $controller('PostListCtrl', { $scope: $scope, postListResponse: {data: [samplePost]} }); expect($scope.postList.length).toBe(1); expect($scope.postList[0].id).toBe(samplePost.id); }));
  81. $ npm run test

  82. $ vi tests/js/controllersSpec.js describe('PostDetailsCtrl', function(){ it('should bind post', inject(function($controller){ var

    samplePost = {id: 1, title: 'Test', body: 'Foo bar'}; var $scope = {}; var ctrl = $controller('PostDetailsCtrl', { $scope: $scope, postDetailsResponse: {data: samplePost} }); expect($scope.post.id).toBe(samplePost.id); })); });
  83. $ npm run test

  84. The Post Create

  85. $ vi blog/api/post_index.py from flask.ext.restful.reqparse import RequestParser from blog.api.base import

    Resource from blog.config import db from blog.models import Post class PostIndexResource(Resource): def get(self): # ... def post(self): """ Create a new post. """ parser = RequestParser() parser.add_argument('title', required=True) parser.add_argument('body', required=True) args = parser.parse_args() post = Post( title=args.title, body=args.body, ) db.session.add(post) db.session.commit() return { 'id': post.id, 'title': post.title, 'body': post.body, 'pubDate': post.pub_date.isoformat(), }, 201
  86. $ vi tests/python/api/test_post_index.py def test_list(client): # ... def test_create_with_valid_params(client): title

    = 'Foo' body = 'Bar' # valid params resp = client.post('/api/0/posts/', data={ 'title': title, 'body': body, }, follow_redirects=True) assert resp.status_code == 201 data = json.loads(resp.data.decode('utf-8')) assert data['title'] == title assert data['body'] == body
  87. $ py.test tests/

  88. $ vi static/js/controllers.js angular.module('blog.controllers', ['ngRoute']) .controller('PostListCtrl', function($scope, postListResponse){ $scope.postList =

    postListResponse.data; }); .controller('PostDetailsCtrl', function($scope, postDetailsResponse){ $scope.post = postDetailsResponse.data; }) .controller('NewPostCtrl', function($location, $scope, $http){ $scope.formData = {}; $scope.saveForm = function(){ $http.post('/api/0/posts/' $scope.formData) .success(function(data){ $location.path('/posts/' + data.id); }); }; });
  89. $ vi tests/js/controllersSpec.js describe('NewPostCtrl', function(){ var $httpBackend, samplePost; beforeEach(inject(function($injector){ samplePost

    = {id: 1, title: 'Test', body: 'Foo bar'}; $httpBackend = $injector.get('$httpBackend'); $httpBackend.when('POST', '/api/0/posts/').respond(samplePost); })); it('should support saveForm', inject(function($controller){ // TODO })); afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); });
  90. $ vi tests/js/controllersSpec.js it('should support saveForm', inject(function($controller){ var $scope =

    {formData: samplePost}; var ctrl = $controller('NewPostCtrl', { $scope: $scope }); $httpBackend.expectPOST('/api/0/posts/', $scope.formData) .respond(201, samplePost); $scope.saveForm(); $httpBackend.flush(); }));
  91. $ npm run test

  92. $ vi static/partials/new-post.html <form role="role" name="postForm" ng-submit="saveForm()"> <input type="text" name="title"

    ng-model="formData.title" ng-class="{error: !postForm.title.$valid}" class="form-control" required> <textarea name="body" ng-model="formData.body" ng-class="{error: !postForm.body.$valid}" class="form-control" required></textarea> <button class="btn" type="submit" ng-disabled="postForm.$invalid">Publish</button> </form>
  93. $ vi static/js/app.js $routeProvider .when('/', { // ... }) .when('/posts/:post_id',

    { // ... }) .when('/new/post', { templateUrl: 'new-post.html', controller: 'NewPostCtrl' });
  94. None
  95. Directives DOM Manipulation in Angular

  96. <a directive-name="foo">

  97. # element-based directives # do not work in Internet #

    Explorer <directive-name foo="bar">
  98. <div ng-repeat="i in iList">

  99. ng-app ng-view ng-show ng-hide ng-if ng-repeat ng-class ng-click ng-submit

  100. Applying Markdown

  101. $ vi static/partials/post-details.html <h1> {{ post.title }} </h1> <div markdown="post.body"></div>

  102. $ node_modules/.bin/bower install "angular- sanitize#1.2.17" --save

  103. $ node_modules/.bin/bower install showdown --save

  104. $ less templates/index.html <!-- ... --> <script src="{{ url_for('static', filename='vendor/angular/angular.js')

    }}"></script> <script src="{{ url_for('static', filename='vendor/angular-route/angular-route.js') }}"></script> <script src="{{ url_for('static', filename='vendor/angular-sanitize/angular-sanitize.js') }}"></script> <script src="{{ url_for('static', filename='vendor/showdown/src/showdown.js') }}"></script> <!-- ... -->
  105. $ vi static/js/directives.js angular.module('blog.directives.markdown', ['ngSanitize']). directive('markdown', function($sanitize) { var markdownConverter

    = new Showdown.converter(); return { restrict: 'A', link: function (scope, element, attrs) { scope.$watch(attrs.markdown, function(newVal) { var html = $sanitize(markdownConverter.makeHtml(newVal)); element.html(html); }); } }; });
  106. $ vi static/js/app.js 'use strict'; angular.module('blog', [ 'ngRoute', 'blog.controllers', 'blog.directives.markdown'

    ]).config(function($routeProvider) { // ... });
  107. $ less templates/index.html <!-- ... --> <script src="{{ url_for('static', filename='js/app.js')

    }}"></script> <script src="{{ url_for('static', filename='js/controllers.js') }}"></script> <script src="{{ url_for('static', filename='js/directives.js') }}"></script> <!-- ... -->
  108. The Publish Date

  109. $ vi static/partials/post-list.html <ul> <li ng-repeat="post in postList"> <a href="/#/posts/{{

    post.id }}">{{ post.title }}</a> &mdash; <span time-since="post.pubDate"></span> </li> </ul>
  110. $ vi static/js/directives.js 'use strict'; angular.module('blog.directives.timeSince', []) .directive('timeSince', function($timeout) {

    return function(scope, element, attrs) { var $element = angular.element(element), timeout_id; function updateValue(){ var value = scope.$eval(attrs.timeSince); if (value) { element.text(moment.utc(value).fromNow()); } else { element.text(''); } timeout_id = $timeout(updateValue, 1000); } element.bind('$destroy', function() { $timeout.cancel(timeout_id); }); updateValue(); }; });
  111. $ node_modules/.bin/bower install moment -- save

  112. $ less templates/index.html <!-- ... --> <script src="{{ url_for('static', filename='vendor/angular/angular.js')

    }}"></script> <script src="{{ url_for('static', filename='vendor/angular-route/angular-route.js') }}"></script> <script src="{{ url_for('static', filename='vendor/angular-sanitize/angular-sanitize.js') }}"></script> <script src="{{ url_for('static', filename='vendor/showdown/src/showdown.js') }}"></script> <script src="{{ url_for('static', filename='vendor/moment/moment.js') }}"></script> <!-- ... -->
  113. $ vi static/js/app.js 'use strict'; angular.module('blog', [ 'ngRoute', 'blog.controllers', 'blog.directives.markdown'

    'blog.directives.timeSince' ]).config(function($routeProvider) { // ... });
  114. A word of caution! The $digest dilemma

  115. Things that cause a digest: $timeout $watch $digest $scope.$apply filters

    (likely another 100 things)
  116. Minimize the # of $digests!

  117. $ vi static/js/directives.js .directive('timeSince', function($timeout) { return function(scope, element, attrs)

    { // ... function timeUntilTick(age) { if (age < 1) { return 1; } else if (age < 60) { return 30; } else if (age < 180) { return 300; } else { return 3600; } } function updateValue(){ // ... var age = moment().diff(moment.utc(value), 'minute'); timeout_id = $timeout(updateValue, timeUntilTick(age) * 1000); }
  118. One step further would be a single shared $timeout

  119. Refactoring the API Services in Angular

  120. $ vi static/js/services.js angular.module('blog.services.api', []) .factory('api', function($http){ var api =

    {}, urlBase = '/api/0'; api.listPosts = function(){ return $http.get(urlBase + '/posts/'); }; api.createPost = function(data){ return $http.post(urlBase + '/posts/', data); }; api.getPost = function(id){ return $http.get(urlBase + '/posts/' + id + '/'); }; api.updatePost = function(id, data){ return $http.post(urlBase + '/posts/' + id + '/', data); }; return api; });
  121. $ less templates/index.html <!-- ... --> <script src="{{ url_for('static', filename='js/app.js')

    }}"></script> <script src="{{ url_for('static', filename='js/controllers.js') }}"></script> <script src="{{ url_for('static', filename='js/directives.js') }}"></script> <script src="{{ url_for('static', filename='js/services.js') }}"></script> <!-- ... -->
  122. $ vi static/js/app.js 'use strict'; angular.module('blog', [ 'ngRoute', 'blog.controllers', 'blog.directives.timeSince',

    'blog.services.api' ]).config(function($routeProvider) { // ... });
  123. $ vi static/js/controllers.js angular.module('blog.controllers', ['ngRoute']) .controller('PostListCtrl', function($scope, postListResponse){ $scope.postList =

    postListResponse.data; }); .controller('PostDetailsCtrl', function($scope, postDetailsResponse){ $scope.post = postDetailsResponse.data; }) .controller('NewPostCtrl', function($location, $scope, api){ $scope.formData = {}; $scope.saveForm = function(){ api.createPost($scope.formData) .success(function(data){ $location.path('/posts/' + data.id); }); }; });
  124. The Illusion of Realtime Long polling at it's best

  125. $ vi static/js/controllers.js angular.module('blog.controllers', []) .controller('PostListCtrl', function($scope, $timeout, api, postListResponse){

    var timeout_id; $scope.postList = postListResponse.data; $timeout(function(){ api.listPosts().success(function(data){ $scope.postList = data; }); }, 5000); $scope.$on('$destroy', function(){ $timeout.cancel(timeout_id); }); }) .controller('PostDetailsCtrl', function($scope, postDetailsResponse){ $scope.post = postDetailsResponse.data; });
  126. A Loading Indicator An intro to $http interceptors

  127. $ vi static/js/services.js angular.module('blog.services.loadingIndicator', []) .config(function($httpProvider){ var interceptor = function($q,

    $rootScope) { var reqsTotal = 0; var reqsCompleted = 0; return { request: function(config) { $rootScope.loading = true; reqsTotal++; return config; }, response: function(response) { reqsCompleted++; if (reqsCompleted >= reqsTotal) { $rootScope.loading = false; } return response; }, responseError: function(rejection) { reqsCompleted++; if (reqsCompleted >= reqsTotal) { $rootScope.loading = false; } return $q.reject(rejection); } }; }; $httpProvider.interceptors.push(interceptor); });
  128. $ vi static/js/app.js 'use strict'; angular.module('blog', [ 'ngRoute', 'blog.controllers', 'blog.directives.timeSince',

    'blog.services.api', 'blog.services.loadingIndicator' ]).config(function($routeProvider) { // ... });
  129. $ vi templates/index.html <header> <h1><a href="/#/">My Blog</a></h1> <div class="loading" ng-show="loading">

    <div class="spinner-icon"></div> </div> </header>
  130. A much more complex implementation: http://chieffancypants.github.io/angular- loading-bar/

  131. Selenium Testing The Basics

  132. $ vi tests/protractor.conf.js exports.config = { allScriptsTimeout: 11000, specs: [

    'e2e/*.js' ], capabilities: { 'browserName': 'chrome' }, baseUrl: 'http://localhost:5000/', framework: 'jasmine', jasmineNodeOpts: { defaultTimeoutInterval: 30000 } };
  133. $ vi tests/e2e/scenarios.js 'use strict'; describe('blog', function() { browser.get('/'); describe('index',

    function() { beforeEach(function() { browser.get('/#/'); }); it('should render the post list', function() { expect(element.all(by.css('[ng-view] a')).first().getText()). toMatch(/Hello world \(again\)\!/); }); }); });
  134. $ vi package.json { // ... "scripts": { "postinstall": "bower

    install", "pretest": "npm install", "test": "karma start tests/karma.conf.js", "preupdate-webdriver": "npm install", "update-webdriver": "webdriver-manager update", "preprotractor": "npm run update-webdriver", "protractor": "protractor tests/protractor.conf.js" } }
  135. $ npm install protractor — save-dev

  136. # we need the service running $ bin/web

  137. $ npm run protractor

  138. Looking Back Things to Remember

  139. Scoped Context

  140. Basic Dependency Injection

  141. DOM via Directives

  142. Digests are Expensive!

  143. Building Modular Components using Modules/Services

  144. $ git checkout finished

  145. Additional Reading

  146. https://github.com/dcramer/pyconsg- tutorial-example

  147. https://github.com/angular-ui/ui-router

  148. http://angular-ui.github.io/bootstrap/

  149. https://github.com/witoldsz/angular-http- auth

  150. http://requirejs.org/

  151. https://github.com/dropbox/changes