mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-04-19 23:11:21 +08:00
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
This commit is contained in:
@@ -81,9 +81,65 @@ var $AnimateProvider = ['$provide', function($provide) {
|
||||
return this.$$classNameFilter;
|
||||
};
|
||||
|
||||
this.$get = ['$$q', '$$asyncCallback', function($$q, $$asyncCallback) {
|
||||
this.$get = ['$$q', '$$asyncCallback', '$rootScope', function($$q, $$asyncCallback, $rootScope) {
|
||||
|
||||
var currentDefer;
|
||||
var ELEMENT_NODE = 1;
|
||||
|
||||
function extractElementNodes(element) {
|
||||
var elements = new Array(element.length);
|
||||
var count = 0;
|
||||
for(var i = 0; i < element.length; i++) {
|
||||
var elm = element[i];
|
||||
if (elm.nodeType == ELEMENT_NODE) {
|
||||
elements[count++] = elm;
|
||||
}
|
||||
}
|
||||
elements.length = count;
|
||||
return jqLite(elements);
|
||||
}
|
||||
|
||||
function runAnimationPostDigest(fn) {
|
||||
var cancelFn, defer = $$q.defer();
|
||||
defer.promise.$$cancelFn = function ngAnimateMaybeCancel() {
|
||||
cancelFn && cancelFn();
|
||||
};
|
||||
|
||||
$rootScope.$$postDigest(function ngAnimatePostDigest() {
|
||||
cancelFn = fn(function ngAnimateNotifyComplete() {
|
||||
defer.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
function resolveElementClasses(element, cache) {
|
||||
var toAdd = [], toRemove = [];
|
||||
forEach(cache.classes, function(status, className) {
|
||||
var hasClass = jqLiteHasClass(element[0], className);
|
||||
|
||||
// If the most recent class manipulation (via $animate) was to remove the class, and the
|
||||
// element currently has the class, the class is scheduled for removal. Otherwise, if
|
||||
// the most recent class manipulation (via $animate) was to add the class, and the
|
||||
// element does not currently have the class, the class is scheduled to be added.
|
||||
if (status === false && hasClass) {
|
||||
toRemove.push(className);
|
||||
} else if (status === true && !hasClass) {
|
||||
toAdd.push(className);
|
||||
}
|
||||
});
|
||||
|
||||
return (toAdd.length + toRemove.length) > 0 && [toAdd.join(' '), toRemove.join(' ')];
|
||||
}
|
||||
|
||||
function cachedClassManipulation(cache, classes, op) {
|
||||
for (var i=0, ii = classes.length; i < ii; ++i) {
|
||||
var className = classes[i];
|
||||
cache[className] = op;
|
||||
}
|
||||
}
|
||||
|
||||
function asyncPromise() {
|
||||
// only serve one instance of a promise in order to save CPU cycles
|
||||
if (!currentDefer) {
|
||||
@@ -187,13 +243,17 @@ var $AnimateProvider = ['$provide', function($provide) {
|
||||
* @return {Promise} the animation callback promise
|
||||
*/
|
||||
addClass : function(element, className) {
|
||||
return this.setClass(element, className, []);
|
||||
},
|
||||
|
||||
$$addClassImmediately : function addClassImmediately(element, className) {
|
||||
element = jqLite(element);
|
||||
className = !isString(className)
|
||||
? (isArray(className) ? className.join(' ') : '')
|
||||
: className;
|
||||
forEach(element, function (element) {
|
||||
jqLiteAddClass(element, className);
|
||||
});
|
||||
return asyncPromise();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -209,6 +269,11 @@ var $AnimateProvider = ['$provide', function($provide) {
|
||||
* @return {Promise} the animation callback promise
|
||||
*/
|
||||
removeClass : function(element, className) {
|
||||
return this.setClass(element, [], className);
|
||||
},
|
||||
|
||||
$$removeClassImmediately : function removeClassImmediately(element, className) {
|
||||
element = jqLite(element);
|
||||
className = !isString(className)
|
||||
? (isArray(className) ? className.join(' ') : '')
|
||||
: className;
|
||||
@@ -231,10 +296,50 @@ var $AnimateProvider = ['$provide', function($provide) {
|
||||
* @param {string} remove the CSS class which will be removed from the element
|
||||
* @return {Promise} the animation callback promise
|
||||
*/
|
||||
setClass : function(element, add, remove) {
|
||||
this.addClass(element, add);
|
||||
this.removeClass(element, remove);
|
||||
return asyncPromise();
|
||||
setClass : function(element, add, remove, runSynchronously) {
|
||||
var self = this;
|
||||
var STORAGE_KEY = '$$animateClasses';
|
||||
element = extractElementNodes(jqLite(element));
|
||||
|
||||
if (runSynchronously) {
|
||||
self.$$addClassImmediately(element, add);
|
||||
self.$$removeClassImmediately(element, remove);
|
||||
return asyncPromise();
|
||||
}
|
||||
|
||||
var cache = element.data(STORAGE_KEY);
|
||||
if (!cache) {
|
||||
cache = {
|
||||
classes: {}
|
||||
};
|
||||
var createdCache = true;
|
||||
}
|
||||
|
||||
var classes = cache.classes;
|
||||
|
||||
add = isArray(add) ? add : add.split(' ');
|
||||
remove = isArray(remove) ? remove : remove.split(' ');
|
||||
cachedClassManipulation(classes, add, true);
|
||||
cachedClassManipulation(classes, remove, false);
|
||||
|
||||
if (createdCache) {
|
||||
cache.promise = runAnimationPostDigest(function(done) {
|
||||
var cache = element.data(STORAGE_KEY);
|
||||
element.removeData(STORAGE_KEY);
|
||||
|
||||
var classes = cache && resolveElementClasses(element, cache);
|
||||
|
||||
if (classes) {
|
||||
if (classes[0]) self.$$addClassImmediately(element, classes[0]);
|
||||
if (classes[1]) self.$$removeClassImmediately(element, classes[1]);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
element.data(STORAGE_KEY, cache);
|
||||
}
|
||||
|
||||
return cache.promise;
|
||||
},
|
||||
|
||||
enabled : noop,
|
||||
|
||||
@@ -979,7 +979,7 @@ angular.module('ngAnimate', ['ng'])
|
||||
element = stripCommentsFromElement(element);
|
||||
|
||||
if (classBasedAnimationsBlocked(element)) {
|
||||
return $delegate.setClass(element, add, remove);
|
||||
return $delegate.setClass(element, add, remove, true);
|
||||
}
|
||||
|
||||
// we're using a combined array for both the add and remove
|
||||
@@ -1033,7 +1033,8 @@ angular.module('ngAnimate', ['ng'])
|
||||
return !classes
|
||||
? done()
|
||||
: performAnimation('setClass', classes, element, parentElement, null, function() {
|
||||
$delegate.setClass(element, classes[0], classes[1]);
|
||||
if (classes[0]) $delegate.$$addClassImmediately(element, classes[0]);
|
||||
if (classes[1]) $delegate.$$removeClassImmediately(element, classes[1]);
|
||||
}, done);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -50,10 +50,12 @@ describe("$animate", function() {
|
||||
expect(element.text()).toBe('21');
|
||||
}));
|
||||
|
||||
it("should still perform DOM operations even if animations are disabled", inject(function($animate) {
|
||||
|
||||
it("should still perform DOM operations even if animations are disabled (post-digest)", inject(function($animate, $rootScope) {
|
||||
$animate.enabled(false);
|
||||
expect(element).toBeShown();
|
||||
$animate.addClass(element, 'ng-hide');
|
||||
$rootScope.$digest();
|
||||
expect(element).toBeHidden();
|
||||
}));
|
||||
|
||||
@@ -79,15 +81,17 @@ describe("$animate", function() {
|
||||
expect($animate.cancel()).toBeUndefined();
|
||||
}));
|
||||
|
||||
it("should add and remove classes on SVG elements", inject(function($animate) {
|
||||
it("should add and remove classes on SVG elements", inject(function($animate, $rootScope) {
|
||||
if (!window.SVGElement) return;
|
||||
var svg = jqLite('<svg><rect></rect></svg>');
|
||||
var rect = svg.children();
|
||||
$animate.enabled(false);
|
||||
expect(rect).toBeShown();
|
||||
$animate.addClass(rect, 'ng-hide');
|
||||
$rootScope.$digest();
|
||||
expect(rect).toBeHidden();
|
||||
$animate.removeClass(rect, 'ng-hide');
|
||||
$rootScope.$digest();
|
||||
expect(rect).not.toBeHidden();
|
||||
}));
|
||||
|
||||
@@ -100,4 +104,201 @@ describe("$animate", function() {
|
||||
inject();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CSS class DOM manipulation', function() {
|
||||
var element;
|
||||
var addClass;
|
||||
var removeClass;
|
||||
|
||||
beforeEach(module(provideLog));
|
||||
|
||||
afterEach(function() {
|
||||
dealoc(element);
|
||||
});
|
||||
|
||||
function setupClassManipulationSpies() {
|
||||
inject(function($animate) {
|
||||
addClass = spyOn($animate, '$$addClassImmediately').andCallThrough();
|
||||
removeClass = spyOn($animate, '$$removeClassImmediately').andCallThrough();
|
||||
});
|
||||
}
|
||||
|
||||
function setupClassManipulationLogger(log) {
|
||||
inject(function($animate) {
|
||||
var addClassImmediately = $animate.$$addClassImmediately;
|
||||
var removeClassImmediately = $animate.$$removeClassImmediately;
|
||||
addClass = spyOn($animate, '$$addClassImmediately').andCallFake(function(element, classes) {
|
||||
var names = classes;
|
||||
if (Object.prototype.toString.call(classes) === '[object Array]') names = classes.join( ' ');
|
||||
log('addClass(' + names + ')');
|
||||
return addClassImmediately.call($animate, element, classes);
|
||||
});
|
||||
removeClass = spyOn($animate, '$$removeClassImmediately').andCallFake(function(element, classes) {
|
||||
var names = classes;
|
||||
if (Object.prototype.toString.call(classes) === '[object Array]') names = classes.join( ' ');
|
||||
log('removeClass(' + names + ')');
|
||||
return removeClassImmediately.call($animate, element, classes);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('should defer class manipulation until end of digest', inject(function($rootScope, $animate, log) {
|
||||
setupClassManipulationLogger(log);
|
||||
element = jqLite('<p>test</p>');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$animate.addClass(element, 'test-class1');
|
||||
expect(element).not.toHaveClass('test-class1');
|
||||
|
||||
$animate.removeClass(element, 'test-class1');
|
||||
|
||||
$animate.addClass(element, 'test-class2');
|
||||
expect(element).not.toHaveClass('test-class2');
|
||||
|
||||
$animate.setClass(element, 'test-class3', 'test-class4');
|
||||
expect(element).not.toHaveClass('test-class3');
|
||||
expect(element).not.toHaveClass('test-class4');
|
||||
expect(log).toEqual([]);
|
||||
});
|
||||
|
||||
expect(element).not.toHaveClass('test-class1');
|
||||
expect(element).not.toHaveClass('test-class4');
|
||||
expect(element).toHaveClass('test-class2');
|
||||
expect(element).toHaveClass('test-class3');
|
||||
expect(log).toEqual(['addClass(test-class2 test-class3)']);
|
||||
expect(addClass.callCount).toBe(1);
|
||||
expect(removeClass.callCount).toBe(0);
|
||||
}));
|
||||
|
||||
|
||||
it('should defer class manipulation until postDigest when outside of digest', inject(function($rootScope, $animate, log) {
|
||||
setupClassManipulationLogger(log);
|
||||
element = jqLite('<p class="test-class4">test</p>');
|
||||
|
||||
$animate.addClass(element, 'test-class1');
|
||||
$animate.removeClass(element, 'test-class1');
|
||||
$animate.addClass(element, 'test-class2');
|
||||
$animate.setClass(element, 'test-class3', 'test-class4');
|
||||
|
||||
expect(log).toEqual([]);
|
||||
$rootScope.$digest();
|
||||
|
||||
|
||||
expect(log).toEqual(['addClass(test-class2 test-class3)', 'removeClass(test-class4)']);
|
||||
expect(element).not.toHaveClass('test-class1');
|
||||
expect(element).toHaveClass('test-class2');
|
||||
expect(element).toHaveClass('test-class3');
|
||||
expect(addClass.callCount).toBe(1);
|
||||
expect(removeClass.callCount).toBe(1);
|
||||
}));
|
||||
|
||||
|
||||
it('should perform class manipulation in expected order at end of digest', inject(function($rootScope, $animate, log) {
|
||||
element = jqLite('<p class="test-class3">test</p>');
|
||||
|
||||
setupClassManipulationLogger(log);
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$animate.addClass(element, 'test-class1');
|
||||
$animate.addClass(element, 'test-class2');
|
||||
$animate.removeClass(element, 'test-class1');
|
||||
$animate.removeClass(element, 'test-class3');
|
||||
$animate.addClass(element, 'test-class3');
|
||||
});
|
||||
expect(log).toEqual(['addClass(test-class2)']);
|
||||
}));
|
||||
|
||||
|
||||
it('should return a promise which is resolved on a different turn', inject(function(log, $animate, $browser, $rootScope) {
|
||||
element = jqLite('<p class="test2">test</p>');
|
||||
|
||||
$animate.addClass(element, 'test1').then(log.fn('addClass(test1)'));
|
||||
$animate.removeClass(element, 'test2').then(log.fn('removeClass(test2)'));
|
||||
|
||||
$rootScope.$digest();
|
||||
expect(log).toEqual([]);
|
||||
$browser.defer.flush();
|
||||
expect(log).toEqual(['addClass(test1)', 'removeClass(test2)']);
|
||||
|
||||
log.reset();
|
||||
element = jqLite('<p class="test4">test</p>');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$animate.addClass(element, 'test3').then(log.fn('addClass(test3)'));
|
||||
$animate.removeClass(element, 'test4').then(log.fn('removeClass(test4)'));
|
||||
expect(log).toEqual([]);
|
||||
});
|
||||
|
||||
$browser.defer.flush();
|
||||
expect(log).toEqual(['addClass(test3)', 'removeClass(test4)']);
|
||||
}));
|
||||
|
||||
|
||||
it('should defer class manipulation until end of digest for SVG', inject(function($rootScope, $animate) {
|
||||
if (!window.SVGElement) return;
|
||||
setupClassManipulationSpies();
|
||||
element = jqLite('<svg><g></g></svg>');
|
||||
var target = element.children().eq(0);
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$animate.addClass(target, 'test-class1');
|
||||
expect(target).not.toHaveClass('test-class1');
|
||||
|
||||
$animate.removeClass(target, 'test-class1');
|
||||
|
||||
$animate.addClass(target, 'test-class2');
|
||||
expect(target).not.toHaveClass('test-class2');
|
||||
|
||||
$animate.setClass(target, 'test-class3', 'test-class4');
|
||||
expect(target).not.toHaveClass('test-class3');
|
||||
expect(target).not.toHaveClass('test-class4');
|
||||
});
|
||||
|
||||
expect(target).not.toHaveClass('test-class1');
|
||||
expect(target).toHaveClass('test-class2');
|
||||
expect(addClass.callCount).toBe(1);
|
||||
expect(removeClass.callCount).toBe(0);
|
||||
}));
|
||||
|
||||
|
||||
it('should defer class manipulation until postDigest when outside of digest for SVG', inject(function($rootScope, $animate, log) {
|
||||
if (!window.SVGElement) return;
|
||||
setupClassManipulationLogger(log);
|
||||
element = jqLite('<svg><g class="test-class4"></g></svg>');
|
||||
var target = element.children().eq(0);
|
||||
|
||||
$animate.addClass(target, 'test-class1');
|
||||
$animate.removeClass(target, 'test-class1');
|
||||
$animate.addClass(target, 'test-class2');
|
||||
$animate.setClass(target, 'test-class3', 'test-class4');
|
||||
|
||||
expect(log).toEqual([]);
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(log).toEqual(['addClass(test-class2 test-class3)', 'removeClass(test-class4)']);
|
||||
expect(target).not.toHaveClass('test-class1');
|
||||
expect(target).toHaveClass('test-class2');
|
||||
expect(target).toHaveClass('test-class3');
|
||||
expect(addClass.callCount).toBe(1);
|
||||
expect(removeClass.callCount).toBe(1);
|
||||
}));
|
||||
|
||||
|
||||
it('should perform class manipulation in expected order at end of digest for SVG', inject(function($rootScope, $animate, log) {
|
||||
if (!window.SVGElement) return;
|
||||
element = jqLite('<svg><g class="test-class3"></g></svg>');
|
||||
var target = element.children().eq(0);
|
||||
|
||||
setupClassManipulationLogger(log);
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$animate.addClass(target, 'test-class1');
|
||||
$animate.addClass(target, 'test-class2');
|
||||
$animate.removeClass(target, 'test-class1');
|
||||
$animate.removeClass(target, 'test-class3');
|
||||
$animate.addClass(target, 'test-class3');
|
||||
});
|
||||
expect(log).toEqual(['addClass(test-class2)']);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -554,17 +554,20 @@ describe('form', function() {
|
||||
expect(doc).toBeValid();
|
||||
|
||||
control.$setValidity('error', false);
|
||||
scope.$digest();
|
||||
expect(doc).toBeInvalid();
|
||||
expect(doc.hasClass('ng-valid-error')).toBe(false);
|
||||
expect(doc.hasClass('ng-invalid-error')).toBe(true);
|
||||
|
||||
control.$setValidity('another', false);
|
||||
scope.$digest();
|
||||
expect(doc.hasClass('ng-valid-error')).toBe(false);
|
||||
expect(doc.hasClass('ng-invalid-error')).toBe(true);
|
||||
expect(doc.hasClass('ng-valid-another')).toBe(false);
|
||||
expect(doc.hasClass('ng-invalid-another')).toBe(true);
|
||||
|
||||
control.$setValidity('error', true);
|
||||
scope.$digest();
|
||||
expect(doc).toBeInvalid();
|
||||
expect(doc.hasClass('ng-valid-error')).toBe(true);
|
||||
expect(doc.hasClass('ng-invalid-error')).toBe(false);
|
||||
@@ -572,6 +575,7 @@ describe('form', function() {
|
||||
expect(doc.hasClass('ng-invalid-another')).toBe(true);
|
||||
|
||||
control.$setValidity('another', true);
|
||||
scope.$digest();
|
||||
expect(doc).toBeValid();
|
||||
expect(doc.hasClass('ng-valid-error')).toBe(true);
|
||||
expect(doc.hasClass('ng-invalid-error')).toBe(false);
|
||||
@@ -581,6 +585,7 @@ describe('form', function() {
|
||||
// validators are skipped, e.g. becuase of a parser error
|
||||
control.$setValidity('error', null);
|
||||
control.$setValidity('another', null);
|
||||
scope.$digest();
|
||||
expect(doc.hasClass('ng-valid-error')).toBe(false);
|
||||
expect(doc.hasClass('ng-invalid-error')).toBe(false);
|
||||
expect(doc.hasClass('ng-valid-another')).toBe(false);
|
||||
@@ -652,7 +657,9 @@ describe('form', function() {
|
||||
expect(input1).toBeDirty();
|
||||
expect(input2).toBeDirty();
|
||||
|
||||
|
||||
formCtrl.$setPristine();
|
||||
scope.$digest();
|
||||
expect(form).toBePristine();
|
||||
expect(formCtrl.$pristine).toBe(true);
|
||||
expect(formCtrl.$dirty).toBe(false);
|
||||
@@ -685,6 +692,7 @@ describe('form', function() {
|
||||
expect(input).toBeDirty();
|
||||
|
||||
formCtrl.$setPristine();
|
||||
scope.$digest();
|
||||
expect(form).toBePristine();
|
||||
expect(formCtrl.$pristine).toBe(true);
|
||||
expect(formCtrl.$dirty).toBe(false);
|
||||
@@ -719,7 +727,9 @@ describe('form', function() {
|
||||
expect(nestedInput).toBeDirty();
|
||||
|
||||
formCtrl.$setPristine();
|
||||
scope.$digest();
|
||||
expect(form).toBePristine();
|
||||
scope.$digest();
|
||||
expect(formCtrl.$pristine).toBe(true);
|
||||
expect(formCtrl.$dirty).toBe(false);
|
||||
expect(nestedForm).toBePristine();
|
||||
|
||||
@@ -892,6 +892,45 @@ describe('NgModelController', function() {
|
||||
dealoc(element);
|
||||
}));
|
||||
|
||||
|
||||
it('should minimize janky setting of classes during $validate() and ngModelWatch', inject(function($animate, $compile, $rootScope) {
|
||||
var addClass = $animate.$$addClassImmediately;
|
||||
var removeClass = $animate.$$removeClassImmediately;
|
||||
var addClassCallCount = 0;
|
||||
var removeClassCallCount = 0;
|
||||
var input;
|
||||
$animate.$$addClassImmediately = function(element, className) {
|
||||
if (input && element[0] === input[0]) ++addClassCallCount;
|
||||
return addClass.call($animate, element, className);
|
||||
};
|
||||
|
||||
$animate.$$removeClassImmediately = function(element, className) {
|
||||
if (input && element[0] === input[0]) ++removeClassCallCount;
|
||||
return removeClass.call($animate, element, className);
|
||||
};
|
||||
|
||||
dealoc(element);
|
||||
|
||||
$rootScope.value = "123456789";
|
||||
element = $compile(
|
||||
'<form name="form">' +
|
||||
'<input type="text" ng-model="value" name="alias" ng-maxlength="10">' +
|
||||
'</form>'
|
||||
)($rootScope);
|
||||
|
||||
var form = $rootScope.form;
|
||||
input = element.children().eq(0);
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(input).toBeValid();
|
||||
expect(input).not.toHaveClass('ng-invalid-maxlength');
|
||||
expect(input).toHaveClass('ng-valid-maxlength');
|
||||
expect(addClassCallCount).toBe(1);
|
||||
expect(removeClassCallCount).toBe(0);
|
||||
|
||||
dealoc(element);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
describe("ngAnimate", function() {
|
||||
|
||||
var $originalAnimate;
|
||||
beforeEach(module(function($provide) {
|
||||
$provide.decorator('$animate', function($delegate) {
|
||||
$originalAnimate = $delegate;
|
||||
return $delegate;
|
||||
});
|
||||
}));
|
||||
beforeEach(module('ngAnimate'));
|
||||
beforeEach(module('ngAnimateMock'));
|
||||
|
||||
@@ -4871,4 +4877,202 @@ describe("ngAnimate", function() {
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('CSS class DOM manipulation', function() {
|
||||
var element;
|
||||
var addClass;
|
||||
var removeClass;
|
||||
|
||||
beforeEach(module(provideLog));
|
||||
|
||||
afterEach(function() {
|
||||
dealoc(element);
|
||||
});
|
||||
|
||||
function setupClassManipulationSpies() {
|
||||
inject(function($animate) {
|
||||
addClass = spyOn($originalAnimate, '$$addClassImmediately').andCallThrough();
|
||||
removeClass = spyOn($originalAnimate, '$$removeClassImmediately').andCallThrough();
|
||||
});
|
||||
}
|
||||
|
||||
function setupClassManipulationLogger(log) {
|
||||
inject(function($animate) {
|
||||
var addClassImmediately = $originalAnimate.$$addClassImmediately;
|
||||
var removeClassImmediately = $originalAnimate.$$removeClassImmediately;
|
||||
addClass = spyOn($originalAnimate, '$$addClassImmediately').andCallFake(function(element, classes) {
|
||||
var names = classes;
|
||||
if (Object.prototype.toString.call(classes) === '[object Array]') names = classes.join( ' ');
|
||||
log('addClass(' + names + ')');
|
||||
return addClassImmediately.call($originalAnimate, element, classes);
|
||||
});
|
||||
removeClass = spyOn($originalAnimate, '$$removeClassImmediately').andCallFake(function(element, classes) {
|
||||
var names = classes;
|
||||
if (Object.prototype.toString.call(classes) === '[object Array]') names = classes.join( ' ');
|
||||
log('removeClass(' + names + ')');
|
||||
return removeClassImmediately.call($originalAnimate, element, classes);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
it('should defer class manipulation until end of digest', inject(function($rootScope, $animate, log) {
|
||||
setupClassManipulationLogger(log);
|
||||
element = jqLite('<p>test</p>');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$animate.addClass(element, 'test-class1');
|
||||
expect(element).not.toHaveClass('test-class1');
|
||||
|
||||
$animate.removeClass(element, 'test-class1');
|
||||
|
||||
$animate.addClass(element, 'test-class2');
|
||||
expect(element).not.toHaveClass('test-class2');
|
||||
|
||||
$animate.setClass(element, 'test-class3', 'test-class4');
|
||||
expect(element).not.toHaveClass('test-class3');
|
||||
expect(element).not.toHaveClass('test-class4');
|
||||
expect(log).toEqual([]);
|
||||
});
|
||||
|
||||
expect(element).not.toHaveClass('test-class1');
|
||||
expect(element).not.toHaveClass('test-class4');
|
||||
expect(element).toHaveClass('test-class2');
|
||||
expect(element).toHaveClass('test-class3');
|
||||
expect(log).toEqual(['addClass(test-class2 test-class3)']);
|
||||
expect(addClass.callCount).toBe(1);
|
||||
expect(removeClass.callCount).toBe(0);
|
||||
}));
|
||||
|
||||
|
||||
it('should defer class manipulation until postDigest when outside of digest', inject(function($rootScope, $animate, log) {
|
||||
setupClassManipulationLogger(log);
|
||||
element = jqLite('<p class="test-class4">test</p>');
|
||||
|
||||
$animate.addClass(element, 'test-class1');
|
||||
$animate.removeClass(element, 'test-class1');
|
||||
$animate.addClass(element, 'test-class2');
|
||||
$animate.setClass(element, 'test-class3', 'test-class4');
|
||||
|
||||
expect(log).toEqual([]);
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(log).toEqual(['addClass(test-class2 test-class3)', 'removeClass(test-class4)']);
|
||||
expect(element).not.toHaveClass('test-class1');
|
||||
expect(element).toHaveClass('test-class2');
|
||||
expect(element).toHaveClass('test-class3');
|
||||
expect(addClass.callCount).toBe(1);
|
||||
expect(removeClass.callCount).toBe(1);
|
||||
}));
|
||||
|
||||
|
||||
it('should perform class manipulation in expected order at end of digest', inject(function($rootScope, $animate, log) {
|
||||
element = jqLite('<p class="test-class3">test</p>');
|
||||
|
||||
setupClassManipulationLogger(log);
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$animate.addClass(element, 'test-class1');
|
||||
$animate.addClass(element, 'test-class2');
|
||||
$animate.removeClass(element, 'test-class1');
|
||||
$animate.removeClass(element, 'test-class3');
|
||||
$animate.addClass(element, 'test-class3');
|
||||
});
|
||||
expect(log).toEqual(['addClass(test-class2)']);
|
||||
}));
|
||||
|
||||
|
||||
it('should return a promise which is resolved on a different turn', inject(function(log, $animate, $browser, $rootScope) {
|
||||
element = jqLite('<p class="test2">test</p>');
|
||||
|
||||
$animate.addClass(element, 'test1').then(log.fn('addClass(test1)'));
|
||||
$animate.removeClass(element, 'test2').then(log.fn('removeClass(test2)'));
|
||||
|
||||
$rootScope.$digest();
|
||||
expect(log).toEqual([]);
|
||||
$browser.defer.flush();
|
||||
expect(log).toEqual(['addClass(test1)', 'removeClass(test2)']);
|
||||
|
||||
log.reset();
|
||||
element = jqLite('<p class="test4">test</p>');
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$animate.addClass(element, 'test3').then(log.fn('addClass(test3)'));
|
||||
$animate.removeClass(element, 'test4').then(log.fn('removeClass(test4)'));
|
||||
expect(log).toEqual([]);
|
||||
});
|
||||
|
||||
$browser.defer.flush();
|
||||
expect(log).toEqual(['addClass(test3)', 'removeClass(test4)']);
|
||||
}));
|
||||
|
||||
|
||||
it('should defer class manipulation until end of digest for SVG', inject(function($rootScope, $animate) {
|
||||
if (!window.SVGElement) return;
|
||||
setupClassManipulationSpies();
|
||||
element = jqLite('<svg><g></g></svg>');
|
||||
var target = element.children().eq(0);
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$animate.addClass(target, 'test-class1');
|
||||
expect(target).not.toHaveClass('test-class1');
|
||||
|
||||
$animate.removeClass(target, 'test-class1');
|
||||
|
||||
$animate.addClass(target, 'test-class2');
|
||||
expect(target).not.toHaveClass('test-class2');
|
||||
|
||||
$animate.setClass(target, 'test-class3', 'test-class4');
|
||||
expect(target).not.toHaveClass('test-class3');
|
||||
expect(target).not.toHaveClass('test-class4');
|
||||
});
|
||||
|
||||
expect(target).not.toHaveClass('test-class1');
|
||||
expect(target).toHaveClass('test-class2');
|
||||
expect(addClass.callCount).toBe(1);
|
||||
expect(removeClass.callCount).toBe(0);
|
||||
}));
|
||||
|
||||
|
||||
it('should defer class manipulation until postDigest when outside of digest for SVG', inject(function($rootScope, $animate, log) {
|
||||
if (!window.SVGElement) return;
|
||||
setupClassManipulationLogger(log);
|
||||
element = jqLite('<svg><g class="test-class4"></g></svg>');
|
||||
var target = element.children().eq(0);
|
||||
|
||||
$animate.addClass(target, 'test-class1');
|
||||
$animate.removeClass(target, 'test-class1');
|
||||
$animate.addClass(target, 'test-class2');
|
||||
$animate.setClass(target, 'test-class3', 'test-class4');
|
||||
|
||||
expect(log).toEqual([]);
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(log).toEqual(['addClass(test-class2 test-class3)', 'removeClass(test-class4)']);
|
||||
expect(target).not.toHaveClass('test-class1');
|
||||
expect(target).toHaveClass('test-class2');
|
||||
expect(target).toHaveClass('test-class3');
|
||||
expect(addClass.callCount).toBe(1);
|
||||
expect(removeClass.callCount).toBe(1);
|
||||
}));
|
||||
|
||||
|
||||
it('should perform class manipulation in expected order at end of digest for SVG', inject(function($rootScope, $animate, log) {
|
||||
if (!window.SVGElement) return;
|
||||
element = jqLite('<svg><g class="test-class3"></g></svg>');
|
||||
var target = element.children().eq(0);
|
||||
|
||||
setupClassManipulationLogger(log);
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$animate.addClass(target, 'test-class1');
|
||||
$animate.addClass(target, 'test-class2');
|
||||
$animate.removeClass(target, 'test-class1');
|
||||
$animate.removeClass(target, 'test-class3');
|
||||
$animate.addClass(target, 'test-class3');
|
||||
});
|
||||
expect(log).toEqual(['addClass(test-class2)']);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user