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

Commit 4f84f6b

Browse files
matskoIgorMinar
authored andcommittedFeb 15, 2014
fix($animate): ensure $animate doesn't break natural CSS transitions
BREAKING CHANGE: ngClass and {{ class }} will now call the `setClass` animation callback instead of addClass / removeClass when both a addClass/removeClass operation is being executed on the element during the animation. Please include the setClass animation callback as well as addClass and removeClass within your JS animations to work with ngClass and {{ class }} directives. Closes #6019
1 parent cf5e463 commit 4f84f6b

File tree

9 files changed

+344
-280
lines changed

9 files changed

+344
-280
lines changed
 

‎css/angular.css

+5
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@
99
ng\:form {
1010
display: block;
1111
}
12+
13+
.ng-animate-block-transitions {
14+
transition:0s all!important;
15+
-webkit-transition:0s all!important;
16+
}

‎src/ng/animate.js

+23
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,29 @@ var $AnimateProvider = ['$provide', function($provide) {
222222
done && $timeout(done, 0, false);
223223
},
224224

225+
/**
226+
*
227+
* @ngdoc function
228+
* @name ng.$animate#setClass
229+
* @methodOf ng.$animate
230+
* @function
231+
* @description Adds and/or removes the given CSS classes to and from the element.
232+
* Once complete, the done() callback will be fired (if provided).
233+
* @param {jQuery/jqLite element} element the element which will it's CSS classes changed
234+
* removed from it
235+
* @param {string} add the CSS classes which will be added to the element
236+
* @param {string} remove the CSS class which will be removed from the element
237+
* @param {function=} done the callback function (if provided) that will be fired after the
238+
* CSS classes have been set on the element
239+
*/
240+
setClass : function(element, add, remove, done) {
241+
forEach(element, function (element) {
242+
jqLiteAddClass(element, add);
243+
jqLiteRemoveClass(element, remove);
244+
});
245+
done && $timeout(done, 0, false);
246+
},
247+
225248
enabled : noop
226249
};
227250
}];

‎src/ng/compile.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -690,8 +690,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
690690
* @param {string} oldClasses The former CSS className value
691691
*/
692692
$updateClass : function(newClasses, oldClasses) {
693-
this.$removeClass(tokenDifference(oldClasses, newClasses));
694-
this.$addClass(tokenDifference(newClasses, oldClasses));
693+
var toAdd = tokenDifference(newClasses, oldClasses);
694+
var toRemove = tokenDifference(oldClasses, newClasses);
695+
696+
if(toAdd.length === 0) {
697+
$animate.removeClass(this.$$element, toRemove);
698+
} else if(toRemove.length === 0) {
699+
$animate.addClass(this.$$element, toAdd);
700+
} else {
701+
$animate.setClass(this.$$element, toAdd, toRemove);
702+
}
695703
},
696704

697705
/**

‎src/ngAnimate/animate.js

+286-205
Large diffs are not rendered by default.

‎src/ngMock/angular-mocks.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,8 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
782782
}
783783
};
784784

785-
angular.forEach(['enter','leave','move','addClass','removeClass'], function(method) {
785+
angular.forEach(
786+
['enter','leave','move','addClass','removeClass','setClass'], function(method) {
786787
animate[method] = function() {
787788
animate.queue.push({
788789
event : method,

‎test/ng/compileSpec.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -4666,11 +4666,9 @@ describe('$compile', function() {
46664666
$rootScope.$digest();
46674667

46684668
data = $animate.queue.shift();
4669-
expect(data.event).toBe('removeClass');
4670-
expect(data.args[1]).toBe('rice');
4671-
data = $animate.queue.shift();
4672-
expect(data.event).toBe('addClass');
4669+
expect(data.event).toBe('setClass');
46734670
expect(data.args[1]).toBe('dice');
4671+
expect(data.args[2]).toBe('rice');
46744672

46754673
expect(element.hasClass('ice')).toBe(true);
46764674
expect(element.hasClass('dice')).toBe(true);

‎test/ng/directive/ngClassSpec.js

+3-7
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,7 @@ describe('ngClass animations', function() {
335335

336336
$rootScope.val = 'two';
337337
$rootScope.$digest();
338-
expect($animate.queue.shift().event).toBe('removeClass');
339-
expect($animate.queue.shift().event).toBe('addClass');
338+
expect($animate.queue.shift().event).toBe('setClass');
340339
expect($animate.queue.length).toBe(0);
341340
});
342341
});
@@ -450,12 +449,9 @@ describe('ngClass animations', function() {
450449
$rootScope.$digest();
451450

452451
item = $animate.queue.shift();
453-
expect(item.event).toBe('removeClass');
454-
expect(item.args[1]).toBe('two');
455-
456-
item = $animate.queue.shift();
457-
expect(item.event).toBe('addClass');
452+
expect(item.event).toBe('setClass');
458453
expect(item.args[1]).toBe('three');
454+
expect(item.args[2]).toBe('two');
459455

460456
expect($animate.queue.length).toBe(0);
461457
});

‎test/ngAnimate/animateSpec.js

+12-59
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ describe("ngAnimate", function() {
491491
$animate.triggerReflow();
492492

493493
//this is to verify that the existing style is appended with a semicolon automatically
494-
expect(child.attr('style')).toMatch(/width: 20px;.+?/i);
494+
expect(child.attr('style')).toMatch(/width: 20px;.*?/i);
495495
browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
496496
}
497497

@@ -564,15 +564,15 @@ describe("ngAnimate", function() {
564564
});
565565
});
566566

567-
it("should fire the cancel/end function with the correct flag in the parameters",
567+
it("should not apply a cancellation when addClass is done multiple times",
568568
inject(function($animate, $rootScope, $sniffer, $timeout) {
569569

570570
element.append(child);
571571

572572
$animate.addClass(child, 'custom-delay');
573573
$animate.addClass(child, 'custom-long-delay');
574574

575-
expect(child.hasClass('animation-cancelled')).toBe(true);
575+
expect(child.hasClass('animation-cancelled')).toBe(false);
576576
expect(child.hasClass('animation-ended')).toBe(false);
577577

578578
$timeout.flush();
@@ -764,7 +764,6 @@ describe("ngAnimate", function() {
764764
$animate.addClass(element, 'ng-hide');
765765
expect(element.hasClass('ng-hide-remove')).toBe(false); //added right away
766766

767-
768767
if($sniffer.animations) { //cleanup some pending animations
769768
$animate.triggerReflow();
770769
expect(element.hasClass('ng-hide-add')).toBe(true);
@@ -1472,6 +1471,8 @@ describe("ngAnimate", function() {
14721471

14731472
expect(flag).toBe(true);
14741473
expect(element.parent().id).toBe(parent2.id);
1474+
1475+
dealoc(element);
14751476
}));
14761477

14771478

@@ -1620,11 +1621,12 @@ describe("ngAnimate", function() {
16201621
var element = parent.find('span');
16211622

16221623
var flag = false;
1623-
$animate.removeClass(element, 'ng-hide', function() {
1624+
$animate.addClass(element, 'ng-hide', function() {
16241625
flag = true;
16251626
});
16261627

16271628
if($sniffer.transitions) {
1629+
$animate.triggerReflow();
16281630
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
16291631
}
16301632
$timeout.flush();
@@ -2734,42 +2736,6 @@ describe("ngAnimate", function() {
27342736
});
27352737

27362738

2737-
it("should cancel an ongoing class-based animation only if the new class contains transition/animation CSS code",
2738-
inject(function($compile, $rootScope, $animate, $sniffer, $timeout) {
2739-
2740-
if (!$sniffer.transitions) return;
2741-
2742-
ss.addRule('.green-add', '-webkit-transition:1s linear all;' +
2743-
'transition:1s linear all;');
2744-
2745-
ss.addRule('.blue-add', 'background:blue;');
2746-
2747-
ss.addRule('.red-add', '-webkit-transition:1s linear all;' +
2748-
'transition:1s linear all;');
2749-
2750-
ss.addRule('.yellow-add', '-webkit-animation: some_animation 4s linear 1s 2 alternate;' +
2751-
'animation: some_animation 4s linear 1s 2 alternate;');
2752-
2753-
var element = $compile('<div></div>')($rootScope);
2754-
$rootElement.append(element);
2755-
jqLite($document[0].body).append($rootElement);
2756-
2757-
$animate.addClass(element, 'green');
2758-
expect(element.hasClass('green-add')).toBe(true);
2759-
2760-
$animate.addClass(element, 'blue');
2761-
expect(element.hasClass('blue')).toBe(true);
2762-
expect(element.hasClass('green-add')).toBe(true); //not cancelled
2763-
2764-
$animate.addClass(element, 'red');
2765-
expect(element.hasClass('green-add')).toBe(false);
2766-
expect(element.hasClass('red-add')).toBe(true);
2767-
2768-
$animate.addClass(element, 'yellow');
2769-
expect(element.hasClass('red-add')).toBe(false);
2770-
expect(element.hasClass('yellow-add')).toBe(true);
2771-
}));
2772-
27732739

27742740
it("should cancel and perform the dom operation only after the reflow has run",
27752741
inject(function($compile, $rootScope, $animate, $sniffer, $timeout) {
@@ -2837,7 +2803,7 @@ describe("ngAnimate", function() {
28372803
$animate.removeClass(element, 'on');
28382804
$animate.addClass(element, 'on');
28392805

2840-
expect(currentAnimation).toBe(null);
2806+
expect(currentAnimation).toBe('addClass');
28412807
});
28422808
});
28432809

@@ -3259,7 +3225,7 @@ describe("ngAnimate", function() {
32593225
expect(ready).toBe(true);
32603226
}));
32613227

3262-
it('should avoid skip animations if the same CSS class is added / removed synchronously before the reflow kicks in',
3228+
it('should immediately close the former animation if the same CSS class is added/removed',
32633229
inject(function($sniffer, $compile, $rootScope, $rootElement, $animate, $timeout) {
32643230

32653231
if (!$sniffer.transitions) return;
@@ -3281,28 +3247,15 @@ describe("ngAnimate", function() {
32813247
signature += 'B';
32823248
});
32833249

3284-
$timeout.flush(1);
3285-
expect(signature).toBe('AB');
3286-
3287-
signature = '';
3288-
$animate.removeClass(element, 'on', function() {
3289-
signature += 'A';
3290-
});
3291-
$animate.addClass(element, 'on', function() {
3292-
signature += 'B';
3293-
});
3294-
$animate.removeClass(element, 'on', function() {
3295-
signature += 'C';
3296-
});
3250+
$animate.triggerReflow();
32973251

32983252
$timeout.flush(1);
3299-
expect(signature).toBe('AB');
3253+
expect(signature).toBe('A');
33003254

3301-
$animate.triggerReflow();
33023255
browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2000 });
33033256
$timeout.flush(1);
33043257

3305-
expect(signature).toBe('ABC');
3258+
expect(signature).toBe('AB');
33063259
}));
33073260
});
33083261
});

‎test/ngRoute/directive/ngViewSpec.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -767,8 +767,7 @@ describe('ngView animations', function() {
767767
$rootScope.klass = 'boring';
768768
$rootScope.$digest();
769769

770-
expect($animate.queue.shift().event).toBe('removeClass');
771-
expect($animate.queue.shift().event).toBe('addClass');
770+
expect($animate.queue.shift().event).toBe('setClass');
772771

773772
expect(item.hasClass('classy')).toBe(false);
774773
expect(item.hasClass('boring')).toBe(true);

0 commit comments

Comments
 (0)
This repository has been archived.