Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 667183a

Browse files
committed
fix(ngAnimate): defer DOM operations for changing classes to postDigest
When ngAnimate is used, it will defer changes to classes until postDigest. Previously, AngularJS (when ngAnimate is not loaded) would always immediately perform these DOM operations. Now, even when the ngAnimate module is not used, if $rootScope is in the midst of a digest, class manipulation is deferred. This helps reduce jank in browsers such as IE11. BREAKING CHANGE: The $animate class API will always defer changes until the end of the next digest. This allows ngAnimate to coalesce class changes which occur over a short period of time into 1 or 2 DOM writes, rather than many. This prevents jank in browsers such as IE, and is generally a good thing. If you're finding that your classes are not being immediately applied, be sure to invoke $digest(). Closes #8234 Closes #9263
1 parent 35049be commit 667183a

File tree

6 files changed

+571
-11
lines changed

6 files changed

+571
-11
lines changed

src/ng/animate.js

+111-6
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,65 @@ var $AnimateProvider = ['$provide', function($provide) {
8181
return this.$$classNameFilter;
8282
};
8383

84-
this.$get = ['$$q', '$$asyncCallback', function($$q, $$asyncCallback) {
84+
this.$get = ['$$q', '$$asyncCallback', '$rootScope', function($$q, $$asyncCallback, $rootScope) {
8585

8686
var currentDefer;
87+
var ELEMENT_NODE = 1;
88+
89+
function extractElementNodes(element) {
90+
var elements = new Array(element.length);
91+
var count = 0;
92+
for(var i = 0; i < element.length; i++) {
93+
var elm = element[i];
94+
if (elm.nodeType == ELEMENT_NODE) {
95+
elements[count++] = elm;
96+
}
97+
}
98+
elements.length = count;
99+
return jqLite(elements);
100+
}
101+
102+
function runAnimationPostDigest(fn) {
103+
var cancelFn, defer = $$q.defer();
104+
defer.promise.$$cancelFn = function ngAnimateMaybeCancel() {
105+
cancelFn && cancelFn();
106+
};
107+
108+
$rootScope.$$postDigest(function ngAnimatePostDigest() {
109+
cancelFn = fn(function ngAnimateNotifyComplete() {
110+
defer.resolve();
111+
});
112+
});
113+
114+
return defer.promise;
115+
}
116+
117+
function resolveElementClasses(element, cache) {
118+
var toAdd = [], toRemove = [];
119+
forEach(cache.classes, function(status, className) {
120+
var hasClass = jqLiteHasClass(element[0], className);
121+
122+
// If the most recent class manipulation (via $animate) was to remove the class, and the
123+
// element currently has the class, the class is scheduled for removal. Otherwise, if
124+
// the most recent class manipulation (via $animate) was to add the class, and the
125+
// element does not currently have the class, the class is scheduled to be added.
126+
if (status === false && hasClass) {
127+
toRemove.push(className);
128+
} else if (status === true && !hasClass) {
129+
toAdd.push(className);
130+
}
131+
});
132+
133+
return (toAdd.length + toRemove.length) > 0 && [toAdd.join(' '), toRemove.join(' ')];
134+
}
135+
136+
function cachedClassManipulation(cache, classes, op) {
137+
for (var i=0, ii = classes.length; i < ii; ++i) {
138+
var className = classes[i];
139+
cache[className] = op;
140+
}
141+
}
142+
87143
function asyncPromise() {
88144
// only serve one instance of a promise in order to save CPU cycles
89145
if (!currentDefer) {
@@ -187,13 +243,17 @@ var $AnimateProvider = ['$provide', function($provide) {
187243
* @return {Promise} the animation callback promise
188244
*/
189245
addClass : function(element, className) {
246+
return this.setClass(element, className, []);
247+
},
248+
249+
$$addClassImmediately : function addClassImmediately(element, className) {
250+
element = jqLite(element);
190251
className = !isString(className)
191252
? (isArray(className) ? className.join(' ') : '')
192253
: className;
193254
forEach(element, function (element) {
194255
jqLiteAddClass(element, className);
195256
});
196-
return asyncPromise();
197257
},
198258

199259
/**
@@ -209,6 +269,11 @@ var $AnimateProvider = ['$provide', function($provide) {
209269
* @return {Promise} the animation callback promise
210270
*/
211271
removeClass : function(element, className) {
272+
return this.setClass(element, [], className);
273+
},
274+
275+
$$removeClassImmediately : function removeClassImmediately(element, className) {
276+
element = jqLite(element);
212277
className = !isString(className)
213278
? (isArray(className) ? className.join(' ') : '')
214279
: className;
@@ -231,10 +296,50 @@ var $AnimateProvider = ['$provide', function($provide) {
231296
* @param {string} remove the CSS class which will be removed from the element
232297
* @return {Promise} the animation callback promise
233298
*/
234-
setClass : function(element, add, remove) {
235-
this.addClass(element, add);
236-
this.removeClass(element, remove);
237-
return asyncPromise();
299+
setClass : function(element, add, remove, runSynchronously) {
300+
var self = this;
301+
var STORAGE_KEY = '$$animateClasses';
302+
element = extractElementNodes(jqLite(element));
303+
304+
if (runSynchronously) {
305+
self.$$addClassImmediately(element, add);
306+
self.$$removeClassImmediately(element, remove);
307+
return asyncPromise();
308+
}
309+
310+
var cache = element.data(STORAGE_KEY);
311+
if (!cache) {
312+
cache = {
313+
classes: {}
314+
};
315+
var createdCache = true;
316+
}
317+
318+
var classes = cache.classes;
319+
320+
add = isArray(add) ? add : add.split(' ');
321+
remove = isArray(remove) ? remove : remove.split(' ');
322+
cachedClassManipulation(classes, add, true);
323+
cachedClassManipulation(classes, remove, false);
324+
325+
if (createdCache) {
326+
cache.promise = runAnimationPostDigest(function(done) {
327+
var cache = element.data(STORAGE_KEY);
328+
element.removeData(STORAGE_KEY);
329+
330+
var classes = cache && resolveElementClasses(element, cache);
331+
332+
if (classes) {
333+
if (classes[0]) self.$$addClassImmediately(element, classes[0]);
334+
if (classes[1]) self.$$removeClassImmediately(element, classes[1]);
335+
}
336+
337+
done();
338+
});
339+
element.data(STORAGE_KEY, cache);
340+
}
341+
342+
return cache.promise;
238343
},
239344

240345
enabled : noop,

src/ngAnimate/animate.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -979,7 +979,7 @@ angular.module('ngAnimate', ['ng'])
979979
element = stripCommentsFromElement(element);
980980

981981
if (classBasedAnimationsBlocked(element)) {
982-
return $delegate.setClass(element, add, remove);
982+
return $delegate.setClass(element, add, remove, true);
983983
}
984984

985985
// we're using a combined array for both the add and remove
@@ -1033,7 +1033,8 @@ angular.module('ngAnimate', ['ng'])
10331033
return !classes
10341034
? done()
10351035
: performAnimation('setClass', classes, element, parentElement, null, function() {
1036-
$delegate.setClass(element, classes[0], classes[1]);
1036+
if (classes[0]) $delegate.$$addClassImmediately(element, classes[0]);
1037+
if (classes[1]) $delegate.$$removeClassImmediately(element, classes[1]);
10371038
}, done);
10381039
});
10391040
},

0 commit comments

Comments
 (0)