├── 01-digest-cycle ├── index.html ├── js │ ├── app.js │ ├── components │ │ └── performance.component.js │ └── source.js └── libs │ └── angular.js ├── 02-digest-apply ├── index.html ├── js │ ├── app.js │ ├── components │ │ └── performance.component.js │ └── source.js └── libs │ └── angular.js ├── 03-rootscope-inheritance ├── index.html ├── js │ ├── app.js │ └── source.js └── libs │ └── angular.js ├── 04-watchers ├── index.html ├── js │ ├── app.js │ ├── components │ │ └── performance.component.js │ ├── services │ │ └── utils.service.js │ └── source.js └── libs │ └── angular.js ├── 05-watch-watchcollection ├── index.html ├── js │ ├── app.js │ ├── components │ │ └── performance.component.js │ └── source.js └── libs │ └── angular.js ├── 06-apply-eval-async ├── index.html ├── js │ ├── app.js │ ├── components │ │ └── performance.component.js │ └── source.js └── libs │ └── angular.js ├── 07-one-time-bindings ├── index.html ├── js │ ├── _DS_Store │ ├── app.js │ ├── components │ │ ├── todo-form.component.js │ │ ├── todo-list.component.js │ │ ├── todo.component.js │ │ └── todos.component.js │ ├── services │ │ ├── _DS_Store │ │ └── todo.service.js │ └── source.js └── libs │ └── angular.js ├── 08-ng-repeat-batching ├── index.html ├── js │ ├── app.js │ ├── components │ │ ├── todo-form.component.js │ │ ├── todo-list.component.js │ │ ├── todo.component.js │ │ └── todos.component.js │ └── services │ │ └── todo.service.js └── libs │ └── angular.js ├── 09-repeat-filtering ├── index.html ├── js │ ├── app.js │ ├── components │ │ ├── todo-form.component.js │ │ ├── todo-list.component.js │ │ ├── todo-search.component.js │ │ ├── todo.component.js │ │ └── todos.component.js │ ├── filters │ │ └── test.filter.js │ └── services │ │ └── todo.service.js └── libs │ └── angular.js ├── 10-track-by ├── index.html ├── js │ ├── app.js │ ├── components │ │ ├── todo-list.component.js │ │ ├── todo.component.js │ │ └── todos.component.js │ ├── services │ │ └── todo.service.js │ └── source.js └── libs │ └── angular.js ├── 11-ng-if-show ├── index.html ├── js │ ├── app.js │ ├── components │ │ ├── content.component.js │ │ ├── header.component.js │ │ └── performance.component.js │ └── source.js └── libs │ └── angular.js ├── 12-ng-model-options ├── index.html ├── js │ ├── app.js │ ├── components │ │ └── performance.component.js │ ├── directives │ │ └── track-digest.directive.js │ └── source.js └── libs │ └── angular.js ├── 13-limit-expressions ├── index.html ├── js │ ├── app.js │ └── performance.component.js └── libs │ └── angular.js ├── 14-batch-http ├── index.html ├── js │ ├── app.js │ ├── components │ │ ├── todo-form.component.js │ │ ├── todo-list.component.js │ │ ├── todo.component.js │ │ └── todos.component.js │ ├── services │ │ └── todo.service.js │ └── source.js └── libs │ └── angular.js ├── 15-strict-di ├── index.html ├── js │ ├── app.js │ ├── components │ │ └── counter.component.js │ ├── services │ │ └── counter.service.js │ └── source.js └── libs │ └── angular.js ├── 16-disable-debug ├── index.html ├── js │ ├── app.js │ ├── components │ │ ├── todo-form.component.js │ │ ├── todo-list.component.js │ │ ├── todo.component.js │ │ └── todos.component.js │ ├── services │ │ └── todo.service.js │ └── source.js └── libs │ └── angular.js ├── 17-destroy-unbinding ├── index.html ├── js │ ├── app.js │ ├── components │ │ ├── content.component.js │ │ └── performance.component.js │ └── source.js └── libs │ └── angular.js └── README.md /01-digest-cycle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /01-digest-cycle/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []) 3 | .run(function ($rootScope, $timeout) { 4 | var count = 0; 5 | $rootScope.$watch(function () { 6 | console.log('$digest count', ++count); 7 | }); 8 | 9 | $rootScope.$watch('enabled', function(val) { 10 | console.log('You are now: ' + (val ? 'enabled' : 'disabled')); 11 | }); 12 | 13 | $rootScope.enabled = true; 14 | $timeout(function() { $rootScope.enabled = false }, 2000); 15 | $timeout(function() { $rootScope.enabled = true }, 4000); 16 | }); 17 | -------------------------------------------------------------------------------- /01-digest-cycle/js/components/performance.component.js: -------------------------------------------------------------------------------- 1 | var performance = { 2 | bindings: {}, 3 | template: ` 4 |

Understanding the Angular digest {{$ctrl.bottleneck}}

5 | 6 | `, 7 | controller: function PerformanceController($scope) { 8 | var ctrl = this; 9 | ctrl.bottleneck = 'cycle'; 10 | 11 | $scope.$watch( 12 | function() { 13 | return ctrl.bottleneck; 14 | }, 15 | function(newValue, oldValue) { 16 | console.log('ctrl.bottleneck', newValue); 17 | } 18 | ) 19 | } 20 | }; 21 | 22 | angular 23 | .module('app') 24 | .component('performance', performance); 25 | -------------------------------------------------------------------------------- /01-digest-cycle/js/source.js: -------------------------------------------------------------------------------- 1 | $digest: function() { 2 | var watch, value, last, fn, get, 3 | watchers, 4 | length, 5 | dirty, ttl = TTL, 6 | next, current, target = this, 7 | watchLog = [], 8 | logIdx, asyncTask; 9 | 10 | beginPhase('$digest'); 11 | // Check for changes to browser url that happened in sync before the call to $digest 12 | $browser.$$checkUrlChange(); 13 | 14 | if (this === $rootScope && applyAsyncId !== null) { 15 | // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then 16 | // cancel the scheduled $apply and flush the queue of expressions to be evaluated. 17 | $browser.defer.cancel(applyAsyncId); 18 | flushApplyAsync(); 19 | } 20 | 21 | lastDirtyWatch = null; 22 | 23 | do { // "while dirty" loop 24 | dirty = false; 25 | current = target; 26 | 27 | // It's safe for asyncQueuePosition to be a local variable here because this loop can't 28 | // be reentered recursively. Calling $digest from a function passed to $applyAsync would 29 | // lead to a '$digest already in progress' error. 30 | for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) { 31 | try { 32 | asyncTask = asyncQueue[asyncQueuePosition]; 33 | asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals); 34 | } catch (e) { 35 | $exceptionHandler(e); 36 | } 37 | lastDirtyWatch = null; 38 | } 39 | asyncQueue.length = 0; 40 | 41 | traverseScopesLoop: 42 | do { // "traverse the scopes" loop 43 | if ((watchers = current.$$watchers)) { 44 | // process our watches 45 | length = watchers.length; 46 | while (length--) { 47 | try { 48 | watch = watchers[length]; 49 | // Most common watches are on primitives, in which case we can short 50 | // circuit it with === operator, only when === fails do we use .equals 51 | if (watch) { 52 | get = watch.get; 53 | if ((value = get(current)) !== (last = watch.last) && 54 | !(watch.eq 55 | ? equals(value, last) 56 | : (isNumberNaN(value) && isNumberNaN(last)))) { 57 | dirty = true; 58 | lastDirtyWatch = watch; 59 | watch.last = watch.eq ? copy(value, null) : value; 60 | fn = watch.fn; 61 | fn(value, ((last === initWatchVal) ? value : last), current); 62 | if (ttl < 5) { 63 | logIdx = 4 - ttl; 64 | if (!watchLog[logIdx]) watchLog[logIdx] = []; 65 | watchLog[logIdx].push({ 66 | msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp, 67 | newVal: value, 68 | oldVal: last 69 | }); 70 | } 71 | } else if (watch === lastDirtyWatch) { 72 | // If the most recently dirty watcher is now clean, short circuit since the remaining watchers 73 | // have already been tested. 74 | dirty = false; 75 | break traverseScopesLoop; 76 | } 77 | } 78 | } catch (e) { 79 | $exceptionHandler(e); 80 | } 81 | } 82 | } 83 | 84 | // Insanity Warning: scope depth-first traversal 85 | // yes, this code is a bit crazy, but it works and we have tests to prove it! 86 | // this piece should be kept in sync with the traversal in $broadcast 87 | if (!(next = ((current.$$watchersCount && current.$$childHead) || 88 | (current !== target && current.$$nextSibling)))) { 89 | while (current !== target && !(next = current.$$nextSibling)) { 90 | current = current.$parent; 91 | } 92 | } 93 | } while ((current = next)); 94 | 95 | // `break traverseScopesLoop;` takes us to here 96 | 97 | if ((dirty || asyncQueue.length) && !(ttl--)) { 98 | clearPhase(); 99 | throw $rootScopeMinErr('infdig', 100 | '{0} $digest() iterations reached. Aborting!\n' + 101 | 'Watchers fired in the last 5 iterations: {1}', 102 | TTL, watchLog); 103 | } 104 | 105 | } while (dirty || asyncQueue.length); 106 | 107 | clearPhase(); 108 | 109 | // postDigestQueuePosition isn't local here because this loop can be reentered recursively. 110 | while (postDigestQueuePosition < postDigestQueue.length) { 111 | try { 112 | postDigestQueue[postDigestQueuePosition++](); 113 | } catch (e) { 114 | $exceptionHandler(e); 115 | } 116 | } 117 | postDigestQueue.length = postDigestQueuePosition = 0; 118 | } 119 | 120 | $watch: function(watchExp, listener, objectEquality, prettyPrintExpression) { 121 | var get = $parse(watchExp); 122 | 123 | if (get.$$watchDelegate) { 124 | return get.$$watchDelegate(this, listener, objectEquality, get, watchExp); 125 | } 126 | var scope = this, 127 | array = scope.$$watchers, 128 | watcher = { 129 | fn: listener, 130 | last: initWatchVal, 131 | get: get, 132 | exp: prettyPrintExpression || watchExp, 133 | eq: !!objectEquality 134 | }; 135 | 136 | lastDirtyWatch = null; 137 | 138 | if (!isFunction(listener)) { 139 | watcher.fn = noop; 140 | } 141 | 142 | if (!array) { 143 | array = scope.$$watchers = []; 144 | } 145 | // we use unshift since we use a while loop in $digest for speed. 146 | // the while loop reads in reverse order. 147 | array.unshift(watcher); 148 | incrementWatchersCount(this, 1); 149 | 150 | return function deregisterWatch() { 151 | if (arrayRemove(array, watcher) >= 0) { 152 | incrementWatchersCount(scope, -1); 153 | } 154 | lastDirtyWatch = null; 155 | }; 156 | } -------------------------------------------------------------------------------- /02-digest-apply/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /02-digest-apply/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []); 3 | -------------------------------------------------------------------------------- /02-digest-apply/js/components/performance.component.js: -------------------------------------------------------------------------------- 1 | var performance = { 2 | bindings: {}, 3 | template: ` 4 |

Count: {{$ctrl.count}}

5 |
6 | Using ng-click: 7 | 8 |
9 |
10 | Without $apply: 11 | 12 |
13 |
14 | With $apply: 15 | 16 |
17 | `, 18 | controller: function PerformanceController($scope, $timeout) { 19 | var ctrl = this; 20 | ctrl.count = 0; 21 | ctrl.increment = function() { 22 | this.count++; 23 | console.log('ON INCREMENT', this.count); 24 | } 25 | 26 | $scope.$watch( 27 | function () { 28 | return ctrl.count 29 | }, 30 | function (newValue, oldValue) { 31 | console.log('ctrl.count', newValue) 32 | } 33 | ); 34 | 35 | document.getElementById('secondBtn').addEventListener('click', function() { 36 | ctrl.increment(); 37 | }); 38 | 39 | document.getElementById('thirdBtn').addEventListener('click', function() { 40 | $scope.$apply(function() { 41 | ctrl.increment(); 42 | }); 43 | }); 44 | 45 | } 46 | } 47 | ; 48 | 49 | angular 50 | .module('app') 51 | .component('performance', performance); 52 | -------------------------------------------------------------------------------- /02-digest-apply/js/source.js: -------------------------------------------------------------------------------- 1 | $digest: function() { 2 | var watch, value, last, fn, get, 3 | watchers, 4 | length, 5 | dirty, ttl = TTL, 6 | next, current, target = this, 7 | watchLog = [], 8 | logIdx, asyncTask; 9 | 10 | beginPhase('$digest'); 11 | // Check for changes to browser url that happened in sync before the call to $digest 12 | $browser.$$checkUrlChange(); 13 | 14 | if (this === $rootScope && applyAsyncId !== null) { 15 | // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then 16 | // cancel the scheduled $apply and flush the queue of expressions to be evaluated. 17 | $browser.defer.cancel(applyAsyncId); 18 | flushApplyAsync(); 19 | } 20 | 21 | lastDirtyWatch = null; 22 | 23 | do { // "while dirty" loop 24 | dirty = false; 25 | current = target; 26 | 27 | // It's safe for asyncQueuePosition to be a local variable here because this loop can't 28 | // be reentered recursively. Calling $digest from a function passed to $applyAsync would 29 | // lead to a '$digest already in progress' error. 30 | for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) { 31 | try { 32 | asyncTask = asyncQueue[asyncQueuePosition]; 33 | asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals); 34 | } catch (e) { 35 | $exceptionHandler(e); 36 | } 37 | lastDirtyWatch = null; 38 | } 39 | asyncQueue.length = 0; 40 | 41 | traverseScopesLoop: 42 | do { // "traverse the scopes" loop 43 | if ((watchers = current.$$watchers)) { 44 | // process our watches 45 | length = watchers.length; 46 | while (length--) { 47 | try { 48 | watch = watchers[length]; 49 | // Most common watches are on primitives, in which case we can short 50 | // circuit it with === operator, only when === fails do we use .equals 51 | if (watch) { 52 | get = watch.get; 53 | if ((value = get(current)) !== (last = watch.last) && 54 | !(watch.eq 55 | ? equals(value, last) 56 | : (isNumberNaN(value) && isNumberNaN(last)))) { 57 | dirty = true; 58 | lastDirtyWatch = watch; 59 | watch.last = watch.eq ? copy(value, null) : value; 60 | fn = watch.fn; 61 | fn(value, ((last === initWatchVal) ? value : last), current); 62 | if (ttl < 5) { 63 | logIdx = 4 - ttl; 64 | if (!watchLog[logIdx]) watchLog[logIdx] = []; 65 | watchLog[logIdx].push({ 66 | msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp, 67 | newVal: value, 68 | oldVal: last 69 | }); 70 | } 71 | } else if (watch === lastDirtyWatch) { 72 | // If the most recently dirty watcher is now clean, short circuit since the remaining watchers 73 | // have already been tested. 74 | dirty = false; 75 | break traverseScopesLoop; 76 | } 77 | } 78 | } catch (e) { 79 | $exceptionHandler(e); 80 | } 81 | } 82 | } 83 | 84 | // Insanity Warning: scope depth-first traversal 85 | // yes, this code is a bit crazy, but it works and we have tests to prove it! 86 | // this piece should be kept in sync with the traversal in $broadcast 87 | if (!(next = ((current.$$watchersCount && current.$$childHead) || 88 | (current !== target && current.$$nextSibling)))) { 89 | while (current !== target && !(next = current.$$nextSibling)) { 90 | current = current.$parent; 91 | } 92 | } 93 | } while ((current = next)); 94 | 95 | // `break traverseScopesLoop;` takes us to here 96 | 97 | if ((dirty || asyncQueue.length) && !(ttl--)) { 98 | clearPhase(); 99 | throw $rootScopeMinErr('infdig', 100 | '{0} $digest() iterations reached. Aborting!\n' + 101 | 'Watchers fired in the last 5 iterations: {1}', 102 | TTL, watchLog); 103 | } 104 | 105 | } while (dirty || asyncQueue.length); 106 | 107 | clearPhase(); 108 | 109 | // postDigestQueuePosition isn't local here because this loop can be reentered recursively. 110 | while (postDigestQueuePosition < postDigestQueue.length) { 111 | try { 112 | postDigestQueue[postDigestQueuePosition++](); 113 | } catch (e) { 114 | $exceptionHandler(e); 115 | } 116 | } 117 | postDigestQueue.length = postDigestQueuePosition = 0; 118 | } 119 | 120 | $watch: function(watchExp, listener, objectEquality, prettyPrintExpression) { 121 | var get = $parse(watchExp); 122 | 123 | if (get.$$watchDelegate) { 124 | return get.$$watchDelegate(this, listener, objectEquality, get, watchExp); 125 | } 126 | var scope = this, 127 | array = scope.$$watchers, 128 | watcher = { 129 | fn: listener, 130 | last: initWatchVal, 131 | get: get, 132 | exp: prettyPrintExpression || watchExp, 133 | eq: !!objectEquality 134 | }; 135 | 136 | lastDirtyWatch = null; 137 | 138 | if (!isFunction(listener)) { 139 | watcher.fn = noop; 140 | } 141 | 142 | if (!array) { 143 | array = scope.$$watchers = []; 144 | } 145 | // we use unshift since we use a while loop in $digest for speed. 146 | // the while loop reads in reverse order. 147 | array.unshift(watcher); 148 | incrementWatchersCount(this, 1); 149 | 150 | return function deregisterWatch() { 151 | if (arrayRemove(array, watcher) >= 0) { 152 | incrementWatchersCount(scope, -1); 153 | } 154 | lastDirtyWatch = null; 155 | }; 156 | } 157 | 158 | $apply: function(expr) { 159 | try { 160 | beginPhase('$apply'); 161 | try { 162 | return this.$eval(expr); 163 | } finally { 164 | clearPhase(); 165 | } 166 | } catch (e) { 167 | $exceptionHandler(e); 168 | } finally { 169 | try { 170 | $rootScope.$digest(); 171 | } catch (e) { 172 | $exceptionHandler(e); 173 | // eslint-disable-next-line no-unsafe-finally 174 | throw e; 175 | } 176 | } 177 | }, -------------------------------------------------------------------------------- /03-rootscope-inheritance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 5 | 6 |
7 |

First Count: {{count}}

8 |
9 | 10 |
11 |

Second Count: {{count}}

12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /03-rootscope-inheritance/js/app.js: -------------------------------------------------------------------------------- 1 | function FirstCtrl($scope) { 2 | $scope.$watch('count', 3 | function (newValue, oldValue) { 4 | console.log('=> firstCtrl.count', newValue) 5 | } 6 | ); 7 | } 8 | 9 | function SecondCtrl($scope) { 10 | $scope.$watch('count', 11 | function (newValue, oldValue) { 12 | console.log('=> secondCtrl.count', newValue) 13 | } 14 | ); 15 | } 16 | 17 | angular 18 | .module('app', []) 19 | .run(function($rootScope, $timeout) { 20 | $rootScope.count = 10; 21 | 22 | $rootScope.$watch('count', function (newValue, oldValue) { 23 | console.log('$rootScope.count', newValue) 24 | }); 25 | 26 | $timeout(function() { $rootScope.count = 100 }, 2000); 27 | $timeout(function() { $rootScope.count = 1000 }, 4000); 28 | $timeout(function() { $rootScope.count = 10000 }, 6000); 29 | }) 30 | .controller('firstCtrl', FirstCtrl) 31 | .controller('secondCtrl', SecondCtrl); 32 | -------------------------------------------------------------------------------- /03-rootscope-inheritance/js/source.js: -------------------------------------------------------------------------------- 1 | $new: function(isolate, parent) { 2 | var child; 3 | 4 | parent = parent || this; 5 | 6 | if (isolate) { 7 | child = new Scope(); 8 | child.$root = this.$root; 9 | } else { 10 | // Only create a child scope class if somebody asks for one, 11 | // but cache it to allow the VM to optimize lookups. 12 | if (!this.$$ChildScope) { 13 | this.$$ChildScope = createChildScopeClass(this); 14 | } 15 | child = new this.$$ChildScope(); 16 | } 17 | child.$parent = parent; 18 | child.$$prevSibling = parent.$$childTail; 19 | if (parent.$$childHead) { 20 | parent.$$childTail.$$nextSibling = child; 21 | parent.$$childTail = child; 22 | } else { 23 | parent.$$childHead = parent.$$childTail = child; 24 | } 25 | 26 | // When the new scope is not isolated or we inherit from `this`, and 27 | // the parent scope is destroyed, the property `$$destroyed` is inherited 28 | // prototypically. In all other cases, this property needs to be set 29 | // when the parent scope is destroyed. 30 | // The listener needs to be added after the parent is set 31 | if (isolate || parent !== this) child.$on('$destroy', destroyChildScope); 32 | 33 | return child; 34 | } 35 | 36 | function createChildScopeClass(parent) { 37 | function ChildScope() { 38 | this.$$watchers = this.$$nextSibling = 39 | this.$$childHead = this.$$childTail = null; 40 | this.$$listeners = {}; 41 | this.$$listenerCount = {}; 42 | this.$$watchersCount = 0; 43 | this.$id = nextUid(); 44 | this.$$ChildScope = null; 45 | } 46 | ChildScope.prototype = parent; 47 | return ChildScope; 48 | } 49 | -------------------------------------------------------------------------------- /04-watchers/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /04-watchers/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []); 3 | -------------------------------------------------------------------------------- /04-watchers/js/components/performance.component.js: -------------------------------------------------------------------------------- 1 | var performance = { 2 | bindings: {}, 3 | template: ` 4 |

Watcher Count: {{$ctrl.watcherCount}}

5 | 6 |
{{item.title}}
7 | `, 8 | controller: function PerformanceController($scope, Utils) { 9 | var ctrl = this; 10 | ctrl.watcherCount = 0; 11 | ctrl.collection = []; 12 | 13 | ctrl.$onInit = function() { 14 | ctrl.watcherCount = Utils.getWatchers().length; 15 | } 16 | 17 | ctrl.add = function() { 18 | var id = Math.random().toString(16).slice(10); 19 | ctrl.collection.push({ 20 | "userId": 1, 21 | "id": id, 22 | "title": "(#" + id + ") Todo item", 23 | "completed": false 24 | }); 25 | } 26 | 27 | $scope.$watch( 28 | function() { 29 | return Utils.getWatchers().length; 30 | }, 31 | function (newValue, oldValue) { 32 | ctrl.watcherCount = newValue; 33 | console.log('ctrl.watcherCount', newValue); 34 | console.log($scope.$$watchers); 35 | } 36 | ); 37 | } 38 | }; 39 | 40 | angular 41 | .module('app') 42 | .component('performance', performance); 43 | -------------------------------------------------------------------------------- /04-watchers/js/services/utils.service.js: -------------------------------------------------------------------------------- 1 | function Utils() { 2 | this.getWatchers = function (root) { 3 | root = angular.element(root || document.documentElement); 4 | 5 | function getElemWatchers(element) { 6 | var isolateWatchers = getWatchersFromScope(element.data().$isolateScope); 7 | var scopeWatchers = getWatchersFromScope(element.data().$scope); 8 | var watchers = scopeWatchers.concat(isolateWatchers); 9 | angular.forEach(element.children(), childElement => { 10 | watchers = watchers.concat(getElemWatchers(angular.element(childElement))); 11 | }); 12 | return watchers; 13 | } 14 | 15 | function getWatchersFromScope(scope) { 16 | if (scope) { 17 | return scope.$$watchers || []; 18 | } else { 19 | return []; 20 | } 21 | } 22 | 23 | return getElemWatchers(root); 24 | } 25 | } 26 | 27 | angular 28 | .module('app') 29 | .service('Utils', Utils); -------------------------------------------------------------------------------- /04-watchers/js/source.js: -------------------------------------------------------------------------------- 1 | function Scope() { 2 | this.$id = nextUid(); 3 | this.$$phase = this.$parent = this.$$watchers = 4 | this.$$nextSibling = this.$$prevSibling = 5 | this.$$childHead = this.$$childTail = null; 6 | this.$root = this; 7 | this.$$destroyed = false; 8 | this.$$listeners = {}; 9 | this.$$listenerCount = {}; 10 | this.$$watchersCount = 0; 11 | this.$$isolateBindings = null; 12 | } 13 | 14 | function incrementWatchersCount(current, count) { 15 | do { 16 | current.$$watchersCount += count; 17 | } while ((current = current.$parent)); 18 | } 19 | 20 | // Insanity Warning: scope depth-first traversal 21 | // yes, this code is a bit crazy, but it works and we have tests to prove it! 22 | // this piece should be kept in sync with the traversal in $broadcast 23 | if (!(next = ((current.$$watchersCount && current.$$childHead) || 24 | (current !== target && current.$$nextSibling)))) { 25 | while (current !== target && !(next = current.$$nextSibling)) { 26 | current = current.$parent; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /05-watch-watchcollection/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /05-watch-watchcollection/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []); 3 | -------------------------------------------------------------------------------- /05-watch-watchcollection/js/components/performance.component.js: -------------------------------------------------------------------------------- 1 | var performance = { 2 | bindings: {}, 3 | template: ` 4 |

5 | Scope $watch() vs. $watchCollection() In AngularJS 6 |

7 | 8 |
9 | 10 | - 11 | 12 | - 13 | 14 | - 15 | 16 |
17 | 18 |

19 | $watchCollection( collection ) Log 20 |

21 | 22 | 27 | 28 |

29 | $watch( collection ) Log 30 |

31 | 32 | 37 | 38 |

39 | $watch( collection, [ Equality = true ] ) Log 40 |

41 | 42 | 47 | `, 48 | controller: function PerformanceController($scope) { 49 | var ctrl = this; 50 | 51 | //////////////////////////////////////////////////////// 52 | // Create and rebuild collection we are watching // 53 | //////////////////////////////////////////////////////// 54 | ctrl.collection = [{ 55 | id: 1, 56 | value: 0 57 | }]; 58 | 59 | ctrl.rebuild = function() { 60 | ctrl.collection = [{ 61 | id: 1, 62 | value: 0 63 | }]; 64 | } 65 | 66 | //////////////////////////////////////////////////////// 67 | // Helper functions for updating values in collection // 68 | //////////////////////////////////////////////////////// 69 | ctrl.now = function() { 70 | return (new Date()).getTime(); 71 | } 72 | 73 | ctrl.changeDeepValue = function() { 74 | ctrl.collection[0].value = ctrl.now(); 75 | } 76 | 77 | ctrl.changeShallowValue = function() { 78 | ctrl.collection.push({ 79 | id: ctrl.collection.length + 1, 80 | value: ctrl.now() 81 | }); 82 | } 83 | 84 | //////////////////////////////////////////////////////// 85 | // Manage logging for our various watchers // 86 | //////////////////////////////////////////////////////// 87 | ctrl.watchCollectionLog = []; 88 | ctrl.watchLog = []; 89 | ctrl.watchEqualityLog = []; 90 | 91 | ctrl.clear = function() { 92 | ctrl.watchCollectionLog = []; 93 | ctrl.watchLog = []; 94 | ctrl.watchEqualityLog = []; 95 | } 96 | 97 | ctrl.addLogItem = function(log) { 98 | var logItem = ( 99 | 'Executed: ' + ctrl.now() + '( length: ' + ctrl.collection.length +')' 100 | ); 101 | 102 | log.splice(0, 0, logItem); 103 | } 104 | 105 | //////////////////////////////////////////////////////// 106 | // Initialize our watchers in the $onInit method // 107 | //////////////////////////////////////////////////////// 108 | ctrl.$onInit = function() { 109 | 110 | $scope.$watchCollection( 111 | function() { 112 | return ctrl.collection; 113 | }, 114 | function(newValue, oldValue) { 115 | ctrl.addLogItem(ctrl.watchCollectionLog); 116 | } 117 | ) 118 | 119 | $scope.$watch( 120 | function() { 121 | return ctrl.collection; 122 | }, 123 | function(newValue, oldValue) { 124 | ctrl.addLogItem(ctrl.watchLog); 125 | } 126 | ) 127 | 128 | $scope.$watch( 129 | function() { 130 | return ctrl.collection; 131 | }, 132 | function(newValue, oldValue) { 133 | ctrl.addLogItem(ctrl.watchEqualityLog); 134 | }, 135 | true 136 | ) 137 | 138 | } 139 | } 140 | }; 141 | 142 | angular 143 | .module('app') 144 | .component('performance', performance); 145 | -------------------------------------------------------------------------------- /05-watch-watchcollection/js/source.js: -------------------------------------------------------------------------------- 1 | $watchCollection: function(obj, listener) { 2 | $watchCollectionInterceptor.$stateful = true; 3 | 4 | var self = this; 5 | // the current value, updated on each dirty-check run 6 | var newValue; 7 | // a shallow copy of the newValue from the last dirty-check run, 8 | // updated to match newValue during dirty-check run 9 | var oldValue; 10 | // a shallow copy of the newValue from when the last change happened 11 | var veryOldValue; 12 | // only track veryOldValue if the listener is asking for it 13 | var trackVeryOldValue = (listener.length > 1); 14 | var changeDetected = 0; 15 | var changeDetector = $parse(obj, $watchCollectionInterceptor); 16 | var internalArray = []; 17 | var internalObject = {}; 18 | var initRun = true; 19 | var oldLength = 0; 20 | 21 | function $watchCollectionInterceptor(_value) { 22 | newValue = _value; 23 | var newLength, key, bothNaN, newItem, oldItem; 24 | 25 | // If the new value is undefined, then return undefined as the watch may be a one-time watch 26 | if (isUndefined(newValue)) return; 27 | 28 | if (!isObject(newValue)) { // if primitive 29 | if (oldValue !== newValue) { 30 | oldValue = newValue; 31 | changeDetected++; 32 | } 33 | } else if (isArrayLike(newValue)) { 34 | if (oldValue !== internalArray) { 35 | // we are transitioning from something which was not an array into array. 36 | oldValue = internalArray; 37 | oldLength = oldValue.length = 0; 38 | changeDetected++; 39 | } 40 | 41 | newLength = newValue.length; 42 | 43 | if (oldLength !== newLength) { 44 | // if lengths do not match we need to trigger change notification 45 | changeDetected++; 46 | oldValue.length = oldLength = newLength; 47 | } 48 | // copy the items to oldValue and look for changes. 49 | for (var i = 0; i < newLength; i++) { 50 | oldItem = oldValue[i]; 51 | newItem = newValue[i]; 52 | 53 | // eslint-disable-next-line no-self-compare 54 | bothNaN = (oldItem !== oldItem) && (newItem !== newItem); 55 | if (!bothNaN && (oldItem !== newItem)) { 56 | changeDetected++; 57 | oldValue[i] = newItem; 58 | } 59 | } 60 | } else { 61 | if (oldValue !== internalObject) { 62 | // we are transitioning from something which was not an object into object. 63 | oldValue = internalObject = {}; 64 | oldLength = 0; 65 | changeDetected++; 66 | } 67 | // copy the items to oldValue and look for changes. 68 | newLength = 0; 69 | for (key in newValue) { 70 | if (hasOwnProperty.call(newValue, key)) { 71 | newLength++; 72 | newItem = newValue[key]; 73 | oldItem = oldValue[key]; 74 | 75 | if (key in oldValue) { 76 | // eslint-disable-next-line no-self-compare 77 | bothNaN = (oldItem !== oldItem) && (newItem !== newItem); 78 | if (!bothNaN && (oldItem !== newItem)) { 79 | changeDetected++; 80 | oldValue[key] = newItem; 81 | } 82 | } else { 83 | oldLength++; 84 | oldValue[key] = newItem; 85 | changeDetected++; 86 | } 87 | } 88 | } 89 | if (oldLength > newLength) { 90 | // we used to have more keys, need to find them and destroy them. 91 | changeDetected++; 92 | for (key in oldValue) { 93 | if (!hasOwnProperty.call(newValue, key)) { 94 | oldLength--; 95 | delete oldValue[key]; 96 | } 97 | } 98 | } 99 | } 100 | return changeDetected; 101 | } 102 | 103 | function $watchCollectionAction() { 104 | if (initRun) { 105 | initRun = false; 106 | listener(newValue, newValue, self); 107 | } else { 108 | listener(newValue, veryOldValue, self); 109 | } 110 | 111 | // make a copy for the next time a collection is changed 112 | if (trackVeryOldValue) { 113 | if (!isObject(newValue)) { 114 | //primitive 115 | veryOldValue = newValue; 116 | } else if (isArrayLike(newValue)) { 117 | veryOldValue = new Array(newValue.length); 118 | for (var i = 0; i < newValue.length; i++) { 119 | veryOldValue[i] = newValue[i]; 120 | } 121 | } else { // if object 122 | veryOldValue = {}; 123 | for (var key in newValue) { 124 | if (hasOwnProperty.call(newValue, key)) { 125 | veryOldValue[key] = newValue[key]; 126 | } 127 | } 128 | } 129 | } 130 | } 131 | 132 | return this.$watch(changeDetector, $watchCollectionAction); 133 | } 134 | 135 | $watch: function(watchExp, listener, objectEquality, prettyPrintExpression) { 136 | var get = $parse(watchExp); 137 | 138 | if (get.$$watchDelegate) { 139 | return get.$$watchDelegate(this, listener, objectEquality, get, watchExp); 140 | } 141 | var scope = this, 142 | array = scope.$$watchers, 143 | watcher = { 144 | fn: listener, 145 | last: initWatchVal, 146 | get: get, 147 | exp: prettyPrintExpression || watchExp, 148 | eq: !!objectEquality 149 | }; 150 | 151 | lastDirtyWatch = null; 152 | 153 | if (!isFunction(listener)) { 154 | watcher.fn = noop; 155 | } 156 | 157 | if (!array) { 158 | array = scope.$$watchers = []; 159 | } 160 | // we use unshift since we use a while loop in $digest for speed. 161 | // the while loop reads in reverse order. 162 | array.unshift(watcher); 163 | incrementWatchersCount(this, 1); 164 | 165 | return function deregisterWatch() { 166 | if (arrayRemove(array, watcher) >= 0) { 167 | incrementWatchersCount(scope, -1); 168 | } 169 | lastDirtyWatch = null; 170 | }; 171 | } -------------------------------------------------------------------------------- /06-apply-eval-async/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /06-apply-eval-async/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []); 3 | -------------------------------------------------------------------------------- /06-apply-eval-async/js/components/performance.component.js: -------------------------------------------------------------------------------- 1 | var performance = { 2 | bindings: {}, 3 | template: ` 4 |

$applyAsync vs $evalAsync

5 | `, 6 | controller: function PerformanceController($scope) { 7 | var ctrl = this; 8 | ctrl.name = 'Motto!'; 9 | // $scope.$apply(function() { console.log('APPLY FIRED!')}); 10 | $scope.$applyAsync(function() { console.log('APPLY ASYNC FIRED!')}); 11 | $scope.$evalAsync(function() { console.log('EVAL ASYNC FIRED!')}); 12 | 13 | $scope.$watch( 14 | function() { 15 | return ctrl.name 16 | }, 17 | function(newValue, oldValue, scope) { 18 | 19 | $scope.$evalAsync(function(scope) { 20 | console.log('$evalAsync executed'); 21 | ctrl.name = 'Todd!'; 22 | }); 23 | 24 | 25 | $scope.$applyAsync(function(scope) { 26 | console.log('$applyAsync executed'); 27 | ctrl.name = 'Todd!'; 28 | }); 29 | } 30 | ); 31 | 32 | $scope.$$postDigest(function() { 33 | console.log('$$postDigest executed. Digest completed'); 34 | console.log(ctrl.name); 35 | }); 36 | } 37 | }; 38 | 39 | angular 40 | .module('app') 41 | .component('performance', performance); 42 | -------------------------------------------------------------------------------- /06-apply-eval-async/js/source.js: -------------------------------------------------------------------------------- 1 | $evalAsync: function(expr, locals) { 2 | // if we are outside of an $digest loop and this is the first time we are scheduling async 3 | // task also schedule async auto-flush 4 | if (!$rootScope.$$phase && !asyncQueue.length) { 5 | $browser.defer(function() { 6 | if (asyncQueue.length) { 7 | $rootScope.$digest(); 8 | } 9 | }); 10 | } 11 | 12 | asyncQueue.push({scope: this, expression: $parse(expr), locals: locals}); 13 | } 14 | 15 | $applyAsync: function(expr) { 16 | var scope = this; 17 | if (expr) { 18 | applyAsyncQueue.push($applyAsyncExpression); 19 | } 20 | expr = $parse(expr); 21 | scheduleApplyAsync(); 22 | 23 | function $applyAsyncExpression() { 24 | scope.$eval(expr); 25 | } 26 | } 27 | 28 | function scheduleApplyAsync() { 29 | if (applyAsyncId === null) { 30 | applyAsyncId = $browser.defer(function() { 31 | $rootScope.$apply(flushApplyAsync); 32 | }); 33 | } 34 | } 35 | 36 | $$postDigest: function(fn) { 37 | postDigestQueue.push(fn); 38 | } 39 | 40 | // Inside $digest 41 | // postDigestQueuePosition isn't local here because this loop can be reentered recursively. 42 | while (postDigestQueuePosition < postDigestQueue.length) { 43 | try { 44 | postDigestQueue[postDigestQueuePosition++](); 45 | } catch (e) { 46 | $exceptionHandler(e); 47 | } 48 | } 49 | postDigestQueue.length = postDigestQueuePosition = 0; 50 | -------------------------------------------------------------------------------- /07-one-time-bindings/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /07-one-time-bindings/js/_DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ultimatecourses/angular-1-performance-src/293902722c3d684d4adee60c6743abe32465b147/07-one-time-bindings/js/_DS_Store -------------------------------------------------------------------------------- /07-one-time-bindings/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []); -------------------------------------------------------------------------------- /07-one-time-bindings/js/components/todo-form.component.js: -------------------------------------------------------------------------------- 1 | var todoForm = { 2 | bindings: { 3 | onAdd: '&' 4 | }, 5 | template: ` 6 |
7 | 8 | 9 |
10 | `, 11 | controller: function () { 12 | this.submit = function () { 13 | if (!this.label) return; 14 | this.onAdd({ 15 | $event: { 16 | label: this.label 17 | } 18 | }); 19 | this.label = ''; 20 | }; 21 | } 22 | }; 23 | 24 | angular 25 | .module('app') 26 | .component('todoForm', todoForm); 27 | -------------------------------------------------------------------------------- /07-one-time-bindings/js/components/todo-list.component.js: -------------------------------------------------------------------------------- 1 | var todoList = { 2 | bindings: { 3 | todos: '<', 4 | onComplete: '&', 5 | onDelete: '&' 6 | }, 7 | template: ` 8 | 17 | ` 18 | }; 19 | 20 | angular 21 | .module('app') 22 | .component('todoList', todoList); 23 | -------------------------------------------------------------------------------- /07-one-time-bindings/js/components/todo.component.js: -------------------------------------------------------------------------------- 1 | var todo = { 2 | bindings: { 3 | item: '<', 4 | onChange: '&', 5 | onRemove: '&' 6 | }, 7 | template: ` 8 |
9 | 10 | {{ ::$ctrl.item.label }} 11 | 12 | 13 | 16 | 19 |
20 | ` 21 | }; 22 | 23 | angular 24 | .module('app') 25 | .component('todo', todo); 26 | -------------------------------------------------------------------------------- /07-one-time-bindings/js/components/todos.component.js: -------------------------------------------------------------------------------- 1 | var todos = { 2 | template: ` 3 |
4 | 6 | 7 | 11 | 12 |
13 | `, 14 | controller: function (TodoService) { 15 | this.$onInit = function () { 16 | this.todos = TodoService.getTodos(); 17 | } 18 | this.addTodo = function ({ label }) { 19 | this.todos = [{ label, id: this.todos.length + 1 }, ...this.todos]; 20 | console.log('Add', this.todos); 21 | } 22 | this.completeTodo = function ({ todo }) { 23 | console.log('Complete', todo); 24 | this.todos = this.todos.map( 25 | item => item.id === todo.id ? Object.assign({}, item, { complete: true }) : item 26 | ); 27 | } 28 | this.removeTodo = function ({ todo }) { 29 | this.todos = this.todos.filter(({ id }) => id !== todo.id); 30 | } 31 | } 32 | }; 33 | 34 | angular 35 | .module('app') 36 | .component('todos', todos); 37 | -------------------------------------------------------------------------------- /07-one-time-bindings/js/services/_DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ultimatecourses/angular-1-performance-src/293902722c3d684d4adee60c6743abe32465b147/07-one-time-bindings/js/services/_DS_Store -------------------------------------------------------------------------------- /07-one-time-bindings/js/services/todo.service.js: -------------------------------------------------------------------------------- 1 | function TodoService() { 2 | this.getTodos = function () { 3 | return [{ 4 | label: 'Eat pizza', 5 | id: 0, 6 | complete: true 7 | },{ 8 | label: 'Do some coding', 9 | id: 1, 10 | complete: true 11 | },{ 12 | label: 'Sleep', 13 | id: 2, 14 | complete: false 15 | },{ 16 | label: 'Print tickets', 17 | id: 3, 18 | complete: true 19 | }]; 20 | } 21 | } 22 | 23 | 24 | angular 25 | .module('app') 26 | .service('TodoService', TodoService); 27 | -------------------------------------------------------------------------------- /07-one-time-bindings/js/source.js: -------------------------------------------------------------------------------- 1 | function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { 2 | var unwatch, lastValue; 3 | return unwatch = scope.$watch(function oneTimeWatch(scope) { 4 | return parsedExpression(scope); 5 | }, function oneTimeListener(value, old, scope) { 6 | lastValue = value; 7 | if (isFunction(listener)) { 8 | listener.apply(this, arguments); 9 | } 10 | if (isDefined(value)) { 11 | scope.$$postDigest(function() { 12 | if (isDefined(lastValue)) { 13 | unwatch(); 14 | } 15 | }); 16 | } 17 | }, objectEquality); 18 | } 19 | 20 | function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { 21 | var unwatch, lastValue; 22 | return unwatch = scope.$watch(function oneTimeWatch(scope) { 23 | return parsedExpression(scope); 24 | }, function oneTimeListener(value, old, scope) { 25 | lastValue = value; 26 | if (isFunction(listener)) { 27 | listener.call(this, value, old, scope); 28 | } 29 | if (isAllDefined(value)) { 30 | scope.$$postDigest(function() { 31 | if (isAllDefined(lastValue)) unwatch(); 32 | }); 33 | } 34 | }, objectEquality); 35 | 36 | function isAllDefined(value) { 37 | var allDefined = true; 38 | forEach(value, function(val) { 39 | if (!isDefined(val)) allDefined = false; 40 | }); 41 | return allDefined; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /08-ng-repeat-batching/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /08-ng-repeat-batching/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []); -------------------------------------------------------------------------------- /08-ng-repeat-batching/js/components/todo-form.component.js: -------------------------------------------------------------------------------- 1 | var todoForm = { 2 | bindings: { 3 | onAdd: '&' 4 | }, 5 | template: ` 6 |
7 | 8 | 9 |
10 | `, 11 | controller: function () { 12 | this.submit = function () { 13 | if (!this.label) return; 14 | this.onAdd({ 15 | $event: { 16 | label: this.label 17 | } 18 | }); 19 | this.label = ''; 20 | }; 21 | } 22 | }; 23 | 24 | angular 25 | .module('app') 26 | .component('todoForm', todoForm); 27 | -------------------------------------------------------------------------------- /08-ng-repeat-batching/js/components/todo-list.component.js: -------------------------------------------------------------------------------- 1 | var todoList = { 2 | bindings: { 3 | todos: '<', 4 | onComplete: '&', 5 | onDelete: '&' 6 | }, 7 | template: ` 8 | 17 | ` 18 | }; 19 | 20 | angular 21 | .module('app') 22 | .component('todoList', todoList); 23 | -------------------------------------------------------------------------------- /08-ng-repeat-batching/js/components/todo.component.js: -------------------------------------------------------------------------------- 1 | var todo = { 2 | bindings: { 3 | item: '<', 4 | onChange: '&', 5 | onRemove: '&' 6 | }, 7 | template: ` 8 |
9 | {{ $ctrl.item.label }} 10 | 13 | 16 |
17 | ` 18 | }; 19 | 20 | angular 21 | .module('app') 22 | .component('todo', todo); 23 | -------------------------------------------------------------------------------- /08-ng-repeat-batching/js/components/todos.component.js: -------------------------------------------------------------------------------- 1 | var todos = { 2 | template: ` 3 |
4 | 7 | 10 | 13 | 14 | 18 | 19 |
20 | `, 21 | controller: function (TodoService, $q, $timeout) { 22 | this.$onInit = function () { 23 | this.todos = []; 24 | }; 25 | this.renderBatched = function () { 26 | this.chunkAndBatchRender('todos', TodoService.getTodos(), 25); 27 | }; 28 | this.renderAll = function () { 29 | this.todos = TodoService.getTodos(); 30 | }; 31 | this.addTodo = function ({ label }) { 32 | this.todos = [{ label, id: this.todos.length + 1 }, ...this.todos]; 33 | }; 34 | this.completeTodo = function ({ todo }) { 35 | this.todos = this.todos.map( 36 | item => item.id === todo.id ? Object.assign({}, item, { complete: true }) : item 37 | ); 38 | }; 39 | this.removeTodo = function ({ todo }) { 40 | this.todos = this.todos.filter(({ id }) => id !== todo.id); 41 | }; 42 | this.chunkAndBatchRender = function (hash, collection, size) { 43 | 44 | var _this = this; 45 | var promise = $q.resolve(); 46 | 47 | function chunkCollection(collection, size) { 48 | var chunks = []; 49 | for (var i = 0; i < collection.length; i += size) { 50 | chunks.push(collection.slice(i, i + size)); 51 | } 52 | return chunks; 53 | } 54 | 55 | function scheduleRender(chunk) { 56 | Array.prototype.push.apply(_this[hash], chunk); 57 | return $timeout(function () {}, 0); 58 | } 59 | 60 | var chunked = chunkCollection(collection, size); 61 | 62 | var nextBatch; 63 | chunked.forEach(function(chunk, index) { 64 | nextBatch = scheduleRender.bind(null, chunk); 65 | promise = promise.then(nextBatch); 66 | }); 67 | 68 | promise.then(function() { 69 | console.log('Rendered.'); 70 | }); 71 | 72 | }; 73 | } 74 | }; 75 | 76 | angular 77 | .module('app') 78 | .component('todos', todos); 79 | -------------------------------------------------------------------------------- /09-repeat-filtering/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 7 | 8 | 9 | 10 |

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /09-repeat-filtering/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []); 3 | -------------------------------------------------------------------------------- /09-repeat-filtering/js/components/todo-form.component.js: -------------------------------------------------------------------------------- 1 | var todoForm = { 2 | bindings: { 3 | onAdd: '&' 4 | }, 5 | template: ` 6 |
7 | 8 | 9 |
10 | `, 11 | controller: function () { 12 | this.submit = function () { 13 | if (!this.label) return; 14 | this.onAdd({ 15 | $event: { 16 | label: this.label 17 | } 18 | }); 19 | this.label = ''; 20 | }; 21 | } 22 | }; 23 | 24 | angular 25 | .module('app') 26 | .component('todoForm', todoForm); 27 | -------------------------------------------------------------------------------- /09-repeat-filtering/js/components/todo-list.component.js: -------------------------------------------------------------------------------- 1 | var todoList = { 2 | bindings: { 3 | todos: '<', 4 | search: '<', 5 | onComplete: '&', 6 | onDelete: '&' 7 | }, 8 | template: ` 9 | 18 | ` 19 | }; 20 | 21 | angular 22 | .module('app') 23 | .component('todoList', todoList); 24 | -------------------------------------------------------------------------------- /09-repeat-filtering/js/components/todo-search.component.js: -------------------------------------------------------------------------------- 1 | var todoSearch = { 2 | bindings: { 3 | onSearch: '&' 4 | }, 5 | template: ` 6 |
7 | 12 |
13 | ` 14 | }; 15 | 16 | angular 17 | .module('app') 18 | .component('todoSearch', todoSearch); 19 | -------------------------------------------------------------------------------- /09-repeat-filtering/js/components/todo.component.js: -------------------------------------------------------------------------------- 1 | var todo = { 2 | bindings: { 3 | item: '<', 4 | onChange: '&', 5 | onRemove: '&' 6 | }, 7 | template: ` 8 |
9 | 10 | {{ ::$ctrl.item.label }} 11 | 12 | 15 | 18 |
19 | ` 20 | }; 21 | 22 | angular 23 | .module('app') 24 | .component('todo', todo); 25 | -------------------------------------------------------------------------------- /09-repeat-filtering/js/components/todos.component.js: -------------------------------------------------------------------------------- 1 | var todos = { 2 | template: ` 3 |
4 | 6 | 7 | 9 | 10 | 15 | 16 |
17 | `, 18 | controller: function ($filter, $interval, TodoService) { 19 | this.$onInit = function () { 20 | this.todosFilter = ''; 21 | this.todosPayload = TodoService.getTodos(); 22 | this.todos = this.todosPayload; 23 | $interval(function() {}, 1000); 24 | }; 25 | this.addTodo = function ({ label }) { 26 | this.todos = [{ label, id: this.todos.length + 1 }, ...this.todos]; 27 | }; 28 | this.onSearch = function (search) { 29 | this.todos = $filter('filter')(this.todosPayload, search.term); 30 | // this.todosFilter = search; 31 | }; 32 | this.completeTodo = function ({ todo }) { 33 | this.todos = this.todos.map( 34 | item => item.id === todo.id ? Object.assign({}, item, { complete: true }) : item 35 | ); 36 | }; 37 | this.removeTodo = function ({ todo }) { 38 | this.todos = this.todos.filter(({ id }) => id !== todo.id); 39 | }; 40 | } 41 | }; 42 | 43 | angular 44 | .module('app') 45 | .component('todos', todos); 46 | -------------------------------------------------------------------------------- /09-repeat-filtering/js/filters/test.filter.js: -------------------------------------------------------------------------------- 1 | function testFilter() { 2 | var filterCount = 0; 3 | return function (values) { 4 | return values.map(function (value) { 5 | filterCount++; 6 | // don't do this, this is just a hack to inject 7 | // the filter count into the DOM 8 | // without forcing another $digest 9 | document.querySelector('.filterCount').innerHTML = ( 10 | 'Filter count: ' + filterCount 11 | ); 12 | return value; 13 | }); 14 | }; 15 | } 16 | 17 | angular 18 | .module('app') 19 | .filter('testFilter', testFilter); 20 | -------------------------------------------------------------------------------- /09-repeat-filtering/js/services/todo.service.js: -------------------------------------------------------------------------------- 1 | function TodoService() { 2 | this.getTodos = function () { 3 | return [{ 4 | label: 'Eat pizza', 5 | id: 0, 6 | complete: true 7 | },{ 8 | label: 'Do some coding', 9 | id: 1, 10 | complete: true 11 | },{ 12 | label: 'Sleep', 13 | id: 2, 14 | complete: false 15 | },{ 16 | label: 'Print tickets', 17 | id: 3, 18 | complete: true 19 | }]; 20 | } 21 | } 22 | 23 | 24 | angular 25 | .module('app') 26 | .service('TodoService', TodoService); 27 | -------------------------------------------------------------------------------- /10-track-by/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /10-track-by/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []); -------------------------------------------------------------------------------- /10-track-by/js/components/todo-list.component.js: -------------------------------------------------------------------------------- 1 | var todoList = { 2 | bindings: { 3 | todos: '<', 4 | onComplete: '&', 5 | onDelete: '&' 6 | }, 7 | template: ` 8 | 17 | ` 18 | }; 19 | 20 | angular 21 | .module('app') 22 | .component('todoList', todoList); 23 | -------------------------------------------------------------------------------- /10-track-by/js/components/todo.component.js: -------------------------------------------------------------------------------- 1 | var todo = { 2 | bindings: { 3 | item: '<', 4 | onChange: '&', 5 | onRemove: '&' 6 | }, 7 | controller: function() { 8 | this.$onInit = function() { console.log('Todo item created:', this.item) } 9 | }, 10 | template: ` 11 |
12 | {{ $ctrl.item.label }} 13 | 16 | 19 |
20 | ` 21 | }; 22 | 23 | angular 24 | .module('app') 25 | .component('todo', todo); 26 | -------------------------------------------------------------------------------- /10-track-by/js/components/todos.component.js: -------------------------------------------------------------------------------- 1 | var todos = { 2 | template: ` 3 |
4 | 5 | 9 | 10 |
11 | `, 12 | controller: function (TodoService) { 13 | this.$onInit = function () { 14 | this.todos = []; 15 | }; 16 | this.addTodo = function ({ label }) { 17 | this.todos = [{ label, id: this.todos.length + 1 }, ...this.todos]; 18 | }; 19 | this.completeTodo = function ({ todo }) { 20 | this.todos = this.todos.map( 21 | item => item.id === todo.id ? Object.assign({}, item, { complete: true }) : item 22 | ); 23 | }; 24 | this.removeTodo = function ({ todo }) { 25 | this.todos = this.todos.filter(({ id }) => id !== todo.id); 26 | }; 27 | this.reloadTodos = function() { 28 | this.todos = TodoService.getTodos(); 29 | }; 30 | } 31 | }; 32 | 33 | angular 34 | .module('app') 35 | .component('todos', todos); 36 | -------------------------------------------------------------------------------- /10-track-by/js/services/todo.service.js: -------------------------------------------------------------------------------- 1 | function TodoService() { 2 | this.getTodos = function () { 3 | return [{ 4 | label: 'Eat pizza', 5 | id: 0, 6 | complete: true 7 | },{ 8 | label: 'Do some coding', 9 | id: 1, 10 | complete: true 11 | },{ 12 | label: 'Sleep', 13 | id: 2, 14 | complete: false 15 | },{ 16 | label: 'Print tickets', 17 | id: 3, 18 | complete: true 19 | }]; 20 | } 21 | } 22 | 23 | 24 | angular 25 | .module('app') 26 | .service('TodoService', TodoService); 27 | -------------------------------------------------------------------------------- /10-track-by/js/source.js: -------------------------------------------------------------------------------- 1 | function ngRepeatCompile($element, $attr) { 2 | var expression = $attr.ngRepeat; 3 | var ngRepeatEndComment = $compile.$$createComment('end ngRepeat', expression); 4 | 5 | var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); 6 | 7 | if (!match) { 8 | throw ngRepeatMinErr('iexp', 'Expected expression in form of \'_item_ in _collection_[ track by _id_]\' but got \'{0}\'.', 9 | expression); 10 | } 11 | 12 | var lhs = match[1]; 13 | var rhs = match[2]; 14 | var aliasAs = match[3]; 15 | var trackByExp = match[4]; 16 | 17 | match = lhs.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/); 18 | 19 | if (!match) { 20 | throw ngRepeatMinErr('iidexp', '\'_item_\' in \'_item_ in _collection_\' should be an identifier or \'(_key_, _value_)\' expression, but got \'{0}\'.', 21 | lhs); 22 | } 23 | var valueIdentifier = match[3] || match[1]; 24 | var keyIdentifier = match[2]; 25 | 26 | if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) || 27 | /^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(aliasAs))) { 28 | throw ngRepeatMinErr('badident', 'alias \'{0}\' is invalid --- must be a valid JS identifier which is not a reserved name.', 29 | aliasAs); 30 | } 31 | 32 | var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn; 33 | var hashFnLocals = {$id: hashKey}; 34 | 35 | if (trackByExp) { 36 | trackByExpGetter = $parse(trackByExp); 37 | } else { 38 | trackByIdArrayFn = function(key, value) { 39 | return hashKey(value); 40 | }; 41 | trackByIdObjFn = function(key) { 42 | return key; 43 | }; 44 | } 45 | 46 | return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) { 47 | 48 | if (trackByExpGetter) { 49 | trackByIdExpFn = function(key, value, index) { 50 | // assign key, value, and $index to the locals so that they can be used in hash functions 51 | if (keyIdentifier) hashFnLocals[keyIdentifier] = key; 52 | hashFnLocals[valueIdentifier] = value; 53 | hashFnLocals.$index = index; 54 | return trackByExpGetter($scope, hashFnLocals); 55 | }; 56 | } 57 | 58 | // Store a list of elements from previous run. This is a hash where key is the item from the 59 | // iterator, and the value is objects with following properties. 60 | // - scope: bound scope 61 | // - element: previous element. 62 | // - index: position 63 | // 64 | // We are using no-proto object so that we don't need to guard against inherited props via 65 | // hasOwnProperty. 66 | var lastBlockMap = createMap(); 67 | 68 | //watch props 69 | $scope.$watchCollection(rhs, function ngRepeatAction(collection) { 70 | var index, length, 71 | previousNode = $element[0], // node that cloned nodes should be inserted after 72 | // initialized to the comment node anchor 73 | nextNode, 74 | // Same as lastBlockMap but it has the current state. It will become the 75 | // lastBlockMap on the next iteration. 76 | nextBlockMap = createMap(), 77 | collectionLength, 78 | key, value, // key/value of iteration 79 | trackById, 80 | trackByIdFn, 81 | collectionKeys, 82 | block, // last object information {scope, element, id} 83 | nextBlockOrder, 84 | elementsToRemove; 85 | 86 | if (aliasAs) { 87 | $scope[aliasAs] = collection; 88 | } 89 | 90 | if (isArrayLike(collection)) { 91 | collectionKeys = collection; 92 | trackByIdFn = trackByIdExpFn || trackByIdArrayFn; 93 | } else { 94 | trackByIdFn = trackByIdExpFn || trackByIdObjFn; 95 | // if object, extract keys, in enumeration order, unsorted 96 | collectionKeys = []; 97 | for (var itemKey in collection) { 98 | if (hasOwnProperty.call(collection, itemKey) && itemKey.charAt(0) !== '$') { 99 | collectionKeys.push(itemKey); 100 | } 101 | } 102 | } 103 | 104 | collectionLength = collectionKeys.length; 105 | nextBlockOrder = new Array(collectionLength); 106 | 107 | // locate existing items 108 | for (index = 0; index < collectionLength; index++) { 109 | key = (collection === collectionKeys) ? index : collectionKeys[index]; 110 | value = collection[key]; 111 | trackById = trackByIdFn(key, value, index); 112 | if (lastBlockMap[trackById]) { 113 | // found previously seen block 114 | block = lastBlockMap[trackById]; 115 | delete lastBlockMap[trackById]; 116 | nextBlockMap[trackById] = block; 117 | nextBlockOrder[index] = block; 118 | } else if (nextBlockMap[trackById]) { 119 | // if collision detected. restore lastBlockMap and throw an error 120 | forEach(nextBlockOrder, function(block) { 121 | if (block && block.scope) lastBlockMap[block.id] = block; 122 | }); 123 | throw ngRepeatMinErr('dupes', 124 | 'Duplicates in a repeater are not allowed. Use \'track by\' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}', 125 | expression, trackById, value); 126 | } else { 127 | // new never before seen block 128 | nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined}; 129 | nextBlockMap[trackById] = true; 130 | } 131 | } 132 | 133 | // remove leftover items 134 | for (var blockKey in lastBlockMap) { 135 | block = lastBlockMap[blockKey]; 136 | elementsToRemove = getBlockNodes(block.clone); 137 | $animate.leave(elementsToRemove); 138 | if (elementsToRemove[0].parentNode) { 139 | // if the element was not removed yet because of pending animation, mark it as deleted 140 | // so that we can ignore it later 141 | for (index = 0, length = elementsToRemove.length; index < length; index++) { 142 | elementsToRemove[index][NG_REMOVED] = true; 143 | } 144 | } 145 | block.scope.$destroy(); 146 | } 147 | 148 | // we are not using forEach for perf reasons (trying to avoid #call) 149 | for (index = 0; index < collectionLength; index++) { 150 | key = (collection === collectionKeys) ? index : collectionKeys[index]; 151 | value = collection[key]; 152 | block = nextBlockOrder[index]; 153 | 154 | if (block.scope) { 155 | // if we have already seen this object, then we need to reuse the 156 | // associated scope/element 157 | 158 | nextNode = previousNode; 159 | 160 | // skip nodes that are already pending removal via leave animation 161 | do { 162 | nextNode = nextNode.nextSibling; 163 | } while (nextNode && nextNode[NG_REMOVED]); 164 | 165 | if (getBlockStart(block) !== nextNode) { 166 | // existing item which got moved 167 | $animate.move(getBlockNodes(block.clone), null, previousNode); 168 | } 169 | previousNode = getBlockEnd(block); 170 | updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); 171 | } else { 172 | // new item which we don't know about 173 | $transclude(function ngRepeatTransclude(clone, scope) { 174 | block.scope = scope; 175 | // http://jsperf.com/clone-vs-createcomment 176 | var endNode = ngRepeatEndComment.cloneNode(false); 177 | clone[clone.length++] = endNode; 178 | 179 | $animate.enter(clone, null, previousNode); 180 | previousNode = endNode; 181 | // Note: We only need the first/last node of the cloned nodes. 182 | // However, we need to keep the reference to the jqlite wrapper as it might be changed later 183 | // by a directive with templateUrl when its template arrives. 184 | block.clone = clone; 185 | nextBlockMap[block.id] = block; 186 | updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); 187 | }); 188 | } 189 | } 190 | lastBlockMap = nextBlockMap; 191 | }); 192 | }; 193 | } 194 | -------------------------------------------------------------------------------- /11-ng-if-show/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /11-ng-if-show/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []); 3 | -------------------------------------------------------------------------------- /11-ng-if-show/js/components/content.component.js: -------------------------------------------------------------------------------- 1 | var content = { 2 | bindings: {}, 3 | template: `

Content Component (ng-if)

`, 4 | controller: function ContentController() { 5 | this.$onDestroy = function() { 6 | console.log('ON DESTROY FIRED! CONTENT COMPONENT!'); 7 | } 8 | } 9 | }; 10 | 11 | angular 12 | .module('app') 13 | .component('content', content) 14 | ; -------------------------------------------------------------------------------- /11-ng-if-show/js/components/header.component.js: -------------------------------------------------------------------------------- 1 | var header = { 2 | bindings: {}, 3 | template: `

Header Component (ng-show)

`, 4 | controller: function HeaderController() { 5 | this.$onDestroy = function() { 6 | console.log('ON DESTROY FIRED! HEADER COMPONENT!'); 7 | } 8 | } 9 | }; 10 | 11 | angular 12 | .module('app') 13 | .component('header', header) 14 | ; -------------------------------------------------------------------------------- /11-ng-if-show/js/components/performance.component.js: -------------------------------------------------------------------------------- 1 | var performance = { 2 | bindings: {}, 3 | template: ` 4 |

ng-if vs ng-show

5 |
6 |
7 | 8 | 9 |
10 |
11 | 12 |
13 | `, 14 | controller: function PerformanceController() { 15 | this.showTop = true; 16 | this.showBottom = true; 17 | 18 | this.toggleTop = function() { 19 | this.showTop = !this.showTop; 20 | } 21 | 22 | this.toggleBottom = function() { 23 | this.showBottom = !this.showBottom; 24 | } 25 | } 26 | }; 27 | 28 | angular 29 | .module('app') 30 | .component('performance', performance); 31 | -------------------------------------------------------------------------------- /11-ng-if-show/js/source.js: -------------------------------------------------------------------------------- 1 | var ngShowDirective = ['$animate', function($animate) { 2 | return { 3 | restrict: 'A', 4 | multiElement: true, 5 | link: function(scope, element, attr) { 6 | scope.$watch(attr.ngShow, function ngShowWatchAction(value) { 7 | // we're adding a temporary, animation-specific class for ng-hide since this way 8 | // we can control when the element is actually displayed on screen without having 9 | // to have a global/greedy CSS selector that breaks when other animations are run. 10 | // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845 11 | $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, { 12 | tempClasses: NG_HIDE_IN_PROGRESS_CLASS 13 | }); 14 | }); 15 | } 16 | }; 17 | }]; 18 | 19 | var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { 20 | return { 21 | multiElement: true, 22 | transclude: 'element', 23 | priority: 600, 24 | terminal: true, 25 | restrict: 'A', 26 | $$tlb: true, 27 | link: function($scope, $element, $attr, ctrl, $transclude) { 28 | var block, childScope, previousElements; 29 | $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { 30 | 31 | if (value) { 32 | if (!childScope) { 33 | $transclude(function(clone, newScope) { 34 | childScope = newScope; 35 | clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf); 36 | // Note: We only need the first/last node of the cloned nodes. 37 | // However, we need to keep the reference to the jqlite wrapper as it might be changed later 38 | // by a directive with templateUrl when its template arrives. 39 | block = { 40 | clone: clone 41 | }; 42 | $animate.enter(clone, $element.parent(), $element); 43 | }); 44 | } 45 | } else { 46 | if (previousElements) { 47 | previousElements.remove(); 48 | previousElements = null; 49 | } 50 | if (childScope) { 51 | childScope.$destroy(); 52 | childScope = null; 53 | } 54 | if (block) { 55 | previousElements = getBlockNodes(block.clone); 56 | $animate.leave(previousElements).then(function() { 57 | previousElements = null; 58 | }); 59 | block = null; 60 | } 61 | } 62 | }); 63 | } 64 | }; 65 | }]; -------------------------------------------------------------------------------- /12-ng-model-options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /12-ng-model-options/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []); 3 | -------------------------------------------------------------------------------- /12-ng-model-options/js/components/performance.component.js: -------------------------------------------------------------------------------- 1 | var performance = { 2 | bindings: {}, 3 | template: ` 4 |

5 |
Standard ngModel
6 | 7 |

8 |
Optimized with ngModelOptions
9 | 16 | `, 17 | controller: function PerformanceController($rootScope) { 18 | this.$onInit = function() { 19 | this.firstName = 'Todd'; 20 | this.lastName = 'Motto'; 21 | } 22 | } 23 | }; 24 | 25 | angular 26 | .module('app') 27 | .component('performance', performance); 28 | -------------------------------------------------------------------------------- /12-ng-model-options/js/directives/track-digest.directive.js: -------------------------------------------------------------------------------- 1 | function trackDigests($rootScope) { 2 | function link($scope, $element, $attrs) { 3 | var count = 0; 4 | function update() { 5 | count++; 6 | $element.text('$digests: ' + count); 7 | console.log('$digest', count); 8 | } 9 | // Use internal angular $$postDigest helper 10 | // purely to avoid using a $watch which 11 | // would cause a double increment in count 12 | (function registerPostDigestHook() { 13 | $rootScope.$$postDigest(function () { 14 | update(); 15 | setTimeout(registerPostDigestHook); 16 | }); 17 | })(); 18 | } 19 | return { 20 | restrict: 'EA', 21 | link: link 22 | }; 23 | } 24 | 25 | angular 26 | .module('app') 27 | .directive('trackDigests', trackDigests); 28 | -------------------------------------------------------------------------------- /12-ng-model-options/js/source.js: -------------------------------------------------------------------------------- 1 | var ngModelOptionsDirective = ['$modelOptions', function($modelOptions) { 2 | return { 3 | restrict: 'A', 4 | // ngModelOptions needs to run before ngModel and input directives 5 | priority: 10, 6 | require: ['ngModelOptions', '?^^ngModelOptions'], 7 | controller: function NgModelOptionsController() {}, 8 | link: { 9 | pre: function ngModelOptionsPreLinkFn(scope, element, attrs, ctrls) { 10 | var optionsCtrl = ctrls[0]; 11 | var parentOptions = ctrls[1] ? ctrls[1].$options : $modelOptions; 12 | optionsCtrl.$options = parentOptions.createChild(scope.$eval(attrs.ngModelOptions)); 13 | } 14 | } 15 | }; 16 | }]; 17 | 18 | // From ng/directive/ngModel.js 19 | post: function ngModelPostLink(scope, element, attr, ctrls) { 20 | var modelCtrl = ctrls[0]; 21 | if (modelCtrl.$options.getOption('updateOn')) { 22 | element.on(modelCtrl.$options.getOption('updateOn'), function(ev) { 23 | modelCtrl.$$debounceViewValueCommit(ev && ev.type); 24 | }); 25 | } 26 | // ...etc... 27 | } 28 | 29 | $$debounceViewValueCommit: function(trigger) { 30 | var debounceDelay = this.$options.getOption('debounce'); 31 | 32 | if (isNumber(debounceDelay[trigger])) { 33 | debounceDelay = debounceDelay[trigger]; 34 | } else if (isNumber(debounceDelay['default'])) { 35 | debounceDelay = debounceDelay['default']; 36 | } 37 | 38 | this.$$timeout.cancel(this.$$pendingDebounce); 39 | var that = this; 40 | if (debounceDelay > 0) { // this fails if debounceDelay is an object 41 | this.$$pendingDebounce = this.$$timeout(function() { 42 | that.$commitViewValue(); 43 | }, debounceDelay); 44 | } else if (this.$$scope.$root.$$phase) { 45 | this.$commitViewValue(); 46 | } else { 47 | this.$$scope.$apply(function() { 48 | that.$commitViewValue(); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /13-limit-expressions/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /13-limit-expressions/js/app.js: -------------------------------------------------------------------------------- 1 | angular.module('app', []); 2 | -------------------------------------------------------------------------------- /13-limit-expressions/js/performance.component.js: -------------------------------------------------------------------------------- 1 | let performance = { 2 | bindings: {}, 3 | template: ` 4 |

Limiting Expressions in Templates

5 |
6 | Too many expressions in the template! How do you test this? 7 |
8 | 9 |
10 | Extract to a method so that we are not coupled to the template 11 |
12 | 13 |
14 | Store method result so method does not get evaluated every digest cycle 15 |
16 | 17 |
18 | Bind once and remove from from digest cycle 19 |
20 | 21 | `, 22 | controller: function PerformanceController($scope) { 23 | this.$onInit = function() { 24 | this.isAdmin = true; 25 | this.favoriteColor = 'blue'; 26 | this.totalCount = 11; 27 | this.elementShown = false; 28 | 29 | this.elementShown = this.showElement(); 30 | } 31 | this.showElement = function() { 32 | return this.isAdmin && this.favoriteColor === 'blue' && this.totalCount > 10 33 | } 34 | } 35 | }; 36 | 37 | angular 38 | .module('app') 39 | .component('performance', performance) 40 | ; 41 | -------------------------------------------------------------------------------- /14-batch-http/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /14-batch-http/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []) 3 | .config(function ($httpProvider) { 4 | $httpProvider.useApplyAsync(true); 5 | }); 6 | -------------------------------------------------------------------------------- /14-batch-http/js/components/todo-form.component.js: -------------------------------------------------------------------------------- 1 | var todoForm = { 2 | bindings: { 3 | onAdd: '&' 4 | }, 5 | template: ` 6 |
7 | 8 | 9 |
10 | `, 11 | controller: function () { 12 | this.submit = function () { 13 | if (!this.label) return; 14 | this.onAdd({ 15 | $event: { 16 | label: this.label 17 | } 18 | }); 19 | this.label = ''; 20 | }; 21 | } 22 | }; 23 | 24 | angular 25 | .module('app') 26 | .component('todoForm', todoForm); 27 | -------------------------------------------------------------------------------- /14-batch-http/js/components/todo-list.component.js: -------------------------------------------------------------------------------- 1 | var todoList = { 2 | bindings: { 3 | todos: '<', 4 | onComplete: '&', 5 | onDelete: '&' 6 | }, 7 | template: ` 8 | 17 | ` 18 | }; 19 | 20 | angular 21 | .module('app') 22 | .component('todoList', todoList); 23 | -------------------------------------------------------------------------------- /14-batch-http/js/components/todo.component.js: -------------------------------------------------------------------------------- 1 | var todo = { 2 | bindings: { 3 | item: '<', 4 | onChange: '&', 5 | onRemove: '&' 6 | }, 7 | template: ` 8 |
9 | 10 | {{ ::$ctrl.item.label }} 11 | 12 | 15 | 18 |
19 | ` 20 | }; 21 | 22 | angular 23 | .module('app') 24 | .component('todo', todo); 25 | -------------------------------------------------------------------------------- /14-batch-http/js/components/todos.component.js: -------------------------------------------------------------------------------- 1 | var todos = { 2 | template: ` 3 |
4 | 6 | 7 | 11 | 12 |
13 | `, 14 | controller: function (TodoService) { 15 | this.$onInit = function () { 16 | this.todos = TodoService.getTodos(); 17 | }; 18 | this.addTodo = function ({ label }) { 19 | this.todos = [{ label, id: this.todos.length + 1 }, ...this.todos]; 20 | }; 21 | this.completeTodo = function ({ todo }) { 22 | this.todos = this.todos.map( 23 | item => item.id === todo.id ? Object.assign({}, item, { complete: true }) : item 24 | ); 25 | }; 26 | this.removeTodo = function ({ todo }) { 27 | this.todos = this.todos.filter(({ id }) => id !== todo.id); 28 | }; 29 | } 30 | }; 31 | 32 | angular 33 | .module('app') 34 | .component('todos', todos); 35 | -------------------------------------------------------------------------------- /14-batch-http/js/services/todo.service.js: -------------------------------------------------------------------------------- 1 | function TodoService() { 2 | this.getTodos = function () { 3 | return [{ 4 | label: 'Eat pizza', 5 | id: 0, 6 | complete: true 7 | },{ 8 | label: 'Do some coding', 9 | id: 1, 10 | complete: true 11 | },{ 12 | label: 'Sleep', 13 | id: 2, 14 | complete: false 15 | },{ 16 | label: 'Print tickets', 17 | id: 3, 18 | complete: true 19 | }]; 20 | } 21 | } 22 | 23 | 24 | angular 25 | .module('app') 26 | .service('TodoService', TodoService); 27 | -------------------------------------------------------------------------------- /14-batch-http/js/source.js: -------------------------------------------------------------------------------- 1 | var useApplyAsync = false; 2 | /** 3 | * @ngdoc method 4 | * @name $httpProvider#useApplyAsync 5 | * @description 6 | * 7 | * Configure $http service to combine processing of multiple http responses received at around 8 | * the same time via {@link ng.$rootScope.Scope#$applyAsync $rootScope.$applyAsync}. This can result in 9 | * significant performance improvement for bigger applications that make many HTTP requests 10 | * concurrently (common during application bootstrap). 11 | * 12 | * Defaults to false. If no value is specified, returns the current configured value. 13 | * 14 | * @param {boolean=} value If true, when requests are loaded, they will schedule a deferred 15 | * "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window 16 | * to load and share the same digest cycle. 17 | * 18 | * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. 19 | * otherwise, returns the current configured value. 20 | **/ 21 | this.useApplyAsync = function(value) { 22 | if (isDefined(value)) { 23 | useApplyAsync = !!value; 24 | return this; 25 | } 26 | return useApplyAsync; 27 | }; 28 | 29 | function createApplyHandlers(eventHandlers) { 30 | if (eventHandlers) { 31 | var applyHandlers = {}; 32 | forEach(eventHandlers, function(eventHandler, key) { 33 | applyHandlers[key] = function(event) { 34 | if (useApplyAsync) { 35 | $rootScope.$applyAsync(callEventHandler); 36 | } else if ($rootScope.$$phase) { 37 | callEventHandler(); 38 | } else { 39 | $rootScope.$apply(callEventHandler); 40 | } 41 | 42 | function callEventHandler() { 43 | eventHandler(event); 44 | } 45 | }; 46 | }); 47 | return applyHandlers; 48 | } 49 | } 50 | 51 | 52 | /** 53 | * Callback registered to $httpBackend(): 54 | * - caches the response if desired 55 | * - resolves the raw $http promise 56 | * - calls $apply 57 | */ 58 | function done(status, response, headersString, statusText) { 59 | if (cache) { 60 | if (isSuccess(status)) { 61 | cache.put(url, [status, response, parseHeaders(headersString), statusText]); 62 | } else { 63 | // remove promise from the cache 64 | cache.remove(url); 65 | } 66 | } 67 | 68 | function resolveHttpPromise() { 69 | resolvePromise(response, status, headersString, statusText); 70 | } 71 | 72 | if (useApplyAsync) { 73 | $rootScope.$applyAsync(resolveHttpPromise); 74 | } else { 75 | resolveHttpPromise(); 76 | if (!$rootScope.$$phase) $rootScope.$apply(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /15-strict-di/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /15-strict-di/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []); -------------------------------------------------------------------------------- /15-strict-di/js/components/counter.component.js: -------------------------------------------------------------------------------- 1 | var counter = { 2 | template: ` 3 |
4 | 5 | 6 | 7 |
8 | `, 9 | controller: ['CounterService', function (CounterService) { 10 | this.$onInit = function () { 11 | this.count = CounterService.getInitialCount(); 12 | }; 13 | this.increment = function () { 14 | this.count = CounterService.incrementCount(this.count); 15 | }; 16 | this.decrement = function () { 17 | this.count = CounterService.decrementCount(this.count); 18 | }; 19 | }] 20 | }; 21 | 22 | angular 23 | .module('app') 24 | .component('counter', counter); 25 | -------------------------------------------------------------------------------- /15-strict-di/js/services/counter.service.js: -------------------------------------------------------------------------------- 1 | function CounterService() { 2 | this.getInitialCount = function () { 3 | return 0; 4 | }; 5 | this.incrementCount = function (count) { 6 | return count + 1; 7 | }; 8 | this.decrementCount = function (count) { 9 | return count - 1; 10 | }; 11 | } 12 | 13 | angular 14 | .module('app') 15 | .service('CounterService', CounterService); 16 | -------------------------------------------------------------------------------- /15-strict-di/js/source.js: -------------------------------------------------------------------------------- 1 | var ARROW_ARG = /^([^\(]+?)=>/; 2 | var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m; 3 | var FN_ARG_SPLIT = /,/; 4 | var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; 5 | var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; 6 | var $injectorMinErr = minErr('$injector'); 7 | 8 | function stringifyFn(fn) { 9 | return Function.prototype.toString.call(fn) + ' '; 10 | } 11 | 12 | function extractArgs(fn) { 13 | var fnText = stringifyFn(fn).replace(STRIP_COMMENTS, ''), 14 | args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS); 15 | return args; 16 | } 17 | 18 | function anonFn(fn) { 19 | var args = extractArgs(fn); 20 | if (args) { 21 | return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')'; 22 | } 23 | return 'fn'; 24 | } 25 | 26 | function annotate(fn, strictDi, name) { 27 | var $inject, 28 | argDecl, 29 | last; 30 | 31 | if (typeof fn === 'function') { 32 | if (!($inject = fn.$inject)) { 33 | $inject = []; 34 | if (fn.length) { 35 | if (strictDi) { 36 | if (!isString(name) || !name) { 37 | name = fn.name || anonFn(fn); 38 | } 39 | throw $injectorMinErr('strictdi', 40 | '{0} is not using explicit annotation and cannot be invoked in strict mode', name); 41 | } 42 | argDecl = extractArgs(fn); 43 | forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { 44 | arg.replace(FN_ARG, function(all, underscore, name) { 45 | $inject.push(name); 46 | }); 47 | }); 48 | } 49 | fn.$inject = $inject; 50 | } 51 | } else if (isArray(fn)) { 52 | last = fn.length - 1; 53 | assertArgFn(fn[last], 'fn'); 54 | $inject = fn.slice(0, last); 55 | } else { 56 | assertArgFn(fn, 'fn', true); 57 | } 58 | return $inject; 59 | } 60 | -------------------------------------------------------------------------------- /16-disable-debug/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /16-disable-debug/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []) 3 | .config(function($compileProvider) { 4 | $compileProvider.debugInfoEnabled(false); 5 | }) 6 | ; 7 | -------------------------------------------------------------------------------- /16-disable-debug/js/components/todo-form.component.js: -------------------------------------------------------------------------------- 1 | var todoForm = { 2 | bindings: { 3 | onAdd: '&' 4 | }, 5 | template: ` 6 |
7 | 8 | 9 |
10 | `, 11 | controller: function () { 12 | this.submit = function () { 13 | if (!this.label) return; 14 | this.onAdd({ 15 | $event: { 16 | label: this.label 17 | } 18 | }); 19 | this.label = ''; 20 | }; 21 | } 22 | }; 23 | 24 | angular 25 | .module('app') 26 | .component('todoForm', todoForm); 27 | -------------------------------------------------------------------------------- /16-disable-debug/js/components/todo-list.component.js: -------------------------------------------------------------------------------- 1 | var todoList = { 2 | bindings: { 3 | todos: '<', 4 | onComplete: '&', 5 | onDelete: '&' 6 | }, 7 | template: ` 8 | 17 | ` 18 | }; 19 | 20 | angular 21 | .module('app') 22 | .component('todoList', todoList); 23 | -------------------------------------------------------------------------------- /16-disable-debug/js/components/todo.component.js: -------------------------------------------------------------------------------- 1 | var todo = { 2 | bindings: { 3 | item: '<', 4 | onChange: '&', 5 | onRemove: '&' 6 | }, 7 | template: ` 8 |
9 | 10 | {{ ::$ctrl.item.label }} 11 | 12 | 15 | 18 |
19 | ` 20 | }; 21 | 22 | angular 23 | .module('app') 24 | .component('todo', todo); 25 | -------------------------------------------------------------------------------- /16-disable-debug/js/components/todos.component.js: -------------------------------------------------------------------------------- 1 | var todos = { 2 | template: ` 3 |
4 | 6 | 7 | 11 | 12 |
13 | `, 14 | controller: function (TodoService) { 15 | this.$onInit = function () { 16 | this.todos = TodoService.getTodos(); 17 | }; 18 | this.addTodo = function ({ label }) { 19 | this.todos = [{ label, id: this.todos.length + 1 }, ...this.todos]; 20 | }; 21 | this.completeTodo = function ({ todo }) { 22 | this.todos = this.todos.map( 23 | item => item.id === todo.id ? Object.assign({}, item, { complete: true }) : item 24 | ); 25 | }; 26 | this.removeTodo = function ({ todo }) { 27 | this.todos = this.todos.filter(({ id }) => id !== todo.id); 28 | }; 29 | } 30 | }; 31 | 32 | angular 33 | .module('app') 34 | .component('todos', todos); 35 | -------------------------------------------------------------------------------- /16-disable-debug/js/services/todo.service.js: -------------------------------------------------------------------------------- 1 | function TodoService() { 2 | this.getTodos = function () { 3 | return [{ 4 | label: 'Eat pizza', 5 | id: 0, 6 | complete: true 7 | },{ 8 | label: 'Do some coding', 9 | id: 1, 10 | complete: true 11 | },{ 12 | label: 'Sleep', 13 | id: 2, 14 | complete: false 15 | },{ 16 | label: 'Print tickets', 17 | id: 3, 18 | complete: true 19 | }]; 20 | } 21 | } 22 | 23 | 24 | angular 25 | .module('app') 26 | .service('TodoService', TodoService); 27 | -------------------------------------------------------------------------------- /16-disable-debug/js/source.js: -------------------------------------------------------------------------------- 1 | var debugInfoEnabled = true; 2 | this.debugInfoEnabled = function(enabled) { 3 | if (isDefined(enabled)) { 4 | debugInfoEnabled = enabled; 5 | return this; 6 | } 7 | return debugInfoEnabled; 8 | }; 9 | 10 | if (config.debugInfoEnabled) { 11 | // Pushing so that this overrides `debugInfoEnabled` setting defined in user's `modules`. 12 | modules.push(['$compileProvider', function($compileProvider) { 13 | $compileProvider.debugInfoEnabled(true); 14 | }]); 15 | } 16 | 17 | compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) { 18 | var bindings = $element.data('$binding') || []; 19 | 20 | if (isArray(binding)) { 21 | bindings = bindings.concat(binding); 22 | } else { 23 | bindings.push(binding); 24 | } 25 | 26 | $element.data('$binding', bindings); 27 | } : noop; 28 | 29 | compile.$$addBindingClass = debugInfoEnabled ? function $$addBindingClass($element) { 30 | safeAddClass($element, 'ng-binding'); 31 | } : noop; 32 | 33 | compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo($element, scope, isolated, noTemplate) { 34 | var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; 35 | $element.data(dataName, scope); 36 | } : noop; 37 | 38 | compile.$$addScopeClass = debugInfoEnabled ? function $$addScopeClass($element, isolated) { 39 | safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); 40 | } : noop; 41 | 42 | compile.$$createComment = function(directiveName, comment) { 43 | var content = ''; 44 | if (debugInfoEnabled) { 45 | content = ' ' + (directiveName || '') + ': '; 46 | if (comment) content += comment + ' '; 47 | } 48 | return window.document.createComment(content); 49 | }; 50 | -------------------------------------------------------------------------------- /17-destroy-unbinding/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ultimate Angular 4 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /17-destroy-unbinding/js/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('app', []); 3 | -------------------------------------------------------------------------------- /17-destroy-unbinding/js/components/content.component.js: -------------------------------------------------------------------------------- 1 | var content = { 2 | bindings: {}, 3 | template: `

Content

`, 4 | controller: function ContentController() { 5 | var ctrl = this; 6 | function clickHandler() { 7 | ctrl.doExpensiveThing(); 8 | console.log('clicked'); 9 | } 10 | ctrl.$onInit = function() { 11 | ctrl.mem = []; 12 | document.addEventListener('click', clickHandler) 13 | } 14 | ctrl.doExpensiveThing = function() { 15 | ctrl.mem.push(new Array(1000000).join('angular')); 16 | } 17 | ctrl.$onDestroy = function() { 18 | document.removeEventListener('click', clickHandler); 19 | console.log('destroyed'); 20 | } 21 | } 22 | }; 23 | 24 | angular 25 | .module('app') 26 | .component('content', content) 27 | ; 28 | -------------------------------------------------------------------------------- /17-destroy-unbinding/js/components/performance.component.js: -------------------------------------------------------------------------------- 1 | var performance = { 2 | bindings: {}, 3 | template: ` 4 |

Using $onDestroy

5 |
6 |
7 | 8 |
9 | 10 |
11 | `, 12 | controller: function PerformanceController() { 13 | this.showContent = true; 14 | 15 | this.toggleContent = function() { 16 | this.showContent = !this.showContent; 17 | } 18 | } 19 | }; 20 | 21 | angular 22 | .module('app') 23 | .component('performance', performance); -------------------------------------------------------------------------------- /17-destroy-unbinding/js/source.js: -------------------------------------------------------------------------------- 1 | var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { 2 | return { 3 | multiElement: true, 4 | transclude: 'element', 5 | priority: 600, 6 | terminal: true, 7 | restrict: 'A', 8 | $$tlb: true, 9 | link: function($scope, $element, $attr, ctrl, $transclude) { 10 | var block, childScope, previousElements; 11 | $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { 12 | 13 | if (value) { 14 | if (!childScope) { 15 | $transclude(function(clone, newScope) { 16 | childScope = newScope; 17 | clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf); 18 | // Note: We only need the first/last node of the cloned nodes. 19 | // However, we need to keep the reference to the jqlite wrapper as it might be changed later 20 | // by a directive with templateUrl when its template arrives. 21 | block = { 22 | clone: clone 23 | }; 24 | $animate.enter(clone, $element.parent(), $element); 25 | }); 26 | } 27 | } else { 28 | if (previousElements) { 29 | previousElements.remove(); 30 | previousElements = null; 31 | } 32 | if (childScope) { 33 | childScope.$destroy(); 34 | childScope = null; 35 | } 36 | if (block) { 37 | previousElements = getBlockNodes(block.clone); 38 | $animate.leave(previousElements).then(function() { 39 | previousElements = null; 40 | }); 41 | block = null; 42 | } 43 | } 44 | }); 45 | } 46 | }; 47 | }]; 48 | 49 | // From ng/rootScope.js 50 | $destroy: function() { 51 | // We can't destroy a scope that has been already destroyed. 52 | if (this.$$destroyed) return; 53 | var parent = this.$parent; 54 | 55 | this.$broadcast('$destroy'); 56 | 57 | // ...etc... 58 | } 59 | 60 | // From node link function in ng/compile.js 61 | if (isFunction(controllerInstance.$onDestroy)) { 62 | controllerScope.$on('$destroy', function callOnDestroyHook() { 63 | controllerInstance.$onDestroy(); 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Ultimate Angular: Angular 1.x Performance Source code 4 | 5 | Source code for Ultimate Angular 1.x [Performance Course](https://ultimatecourses.com/learn/angularjs-performance). 6 | 7 | ### Running these projects 8 | 9 | Some of these may require a local server, any that do you can run the following: 10 | 11 | ``` 12 | cd 13 | python -m SimpleHTTPServer 14 | ``` 15 | 16 | If you're on Windows, you can also download and run python, or if it's easier you can setup a local server by using your favourite IDE. 17 | --------------------------------------------------------------------------------