├── 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 |
23 | -
24 | {{ item }}
25 |
26 |
27 |
28 |
29 | $watch( collection ) Log
30 |
31 |
32 |
33 | -
34 | {{ item }}
35 |
36 |
37 |
38 |
39 | $watch( collection, [ Equality = true ] ) Log
40 |
41 |
42 |
43 | -
44 | {{ item }}
45 |
46 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------