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

Commit cc58460

Browse files
matskomhevery
authored andcommitted
fix($animate): ensure structural animations skip all child animations even if no animation is present during compile
Closes #3215
1 parent 23c6988 commit cc58460

File tree

2 files changed

+223
-22
lines changed

2 files changed

+223
-22
lines changed

src/ngAnimate/animate.js

+56-19
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ angular.module('ngAnimate', ['ng'])
285285
* @param {function()=} done callback function that will be called once the animation is complete
286286
*/
287287
enter : function(element, parent, after, done) {
288+
this.enabled(false, element);
288289
$delegate.enter(element, parent, after);
289290
$rootScope.$$postDigest(function() {
290291
performAnimation('enter', 'ng-enter', element, parent, after, function() {
@@ -322,6 +323,7 @@ angular.module('ngAnimate', ['ng'])
322323
*/
323324
leave : function(element, done) {
324325
cancelChildAnimations(element);
326+
this.enabled(false, element);
325327
$rootScope.$$postDigest(function() {
326328
performAnimation('leave', 'ng-leave', element, null, null, function() {
327329
$delegate.leave(element, done);
@@ -361,6 +363,7 @@ angular.module('ngAnimate', ['ng'])
361363
*/
362364
move : function(element, parent, after, done) {
363365
cancelChildAnimations(element);
366+
this.enabled(false, element);
364367
$delegate.move(element, parent, after);
365368
$rootScope.$$postDigest(function() {
366369
performAnimation('move', 'ng-move', element, null, null, function() {
@@ -451,12 +454,30 @@ angular.module('ngAnimate', ['ng'])
451454
* Globally enables/disables animations.
452455
*
453456
*/
454-
enabled : function(value) {
455-
if (arguments.length) {
456-
rootAnimateState.running = !value;
457+
enabled : function(value, element) {
458+
switch(arguments.length) {
459+
case 2:
460+
if(value) {
461+
cleanup(element);
462+
}
463+
else {
464+
var data = element.data(NG_ANIMATE_STATE) || {};
465+
data.structural = true;
466+
data.running = true;
467+
element.data(NG_ANIMATE_STATE, data);
468+
}
469+
break;
470+
471+
case 1:
472+
rootAnimateState.running = !value;
473+
break;
474+
475+
default:
476+
value = !rootAnimateState.running
477+
break;
457478
}
458-
return !rootAnimateState.running;
459-
}
479+
return !!value;
480+
}
460481
};
461482

462483
/*
@@ -484,24 +505,29 @@ angular.module('ngAnimate', ['ng'])
484505
//skip the animation if animations are disabled, a parent is already being animated
485506
//or the element is not currently attached to the document body.
486507
if ((parent.inheritedData(NG_ANIMATE_STATE) || disabledAnimation).running || animations.length == 0) {
487-
//avoid calling done() since there is no need to remove any
488-
//data or className values since this happens earlier than that
489-
//and also use a timeout so that it won't be asynchronous
490-
onComplete && onComplete();
508+
done();
491509
return;
492510
}
493511

494512
var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};
495513

496-
//if an animation is currently running on the element then lets take the steps
497-
//to cancel that animation and fire any required callbacks
514+
var isClassBased = event == 'addClass' || event == 'removeClass';
498515
if(ngAnimateState.running) {
516+
if(isClassBased && ngAnimateState.structural) {
517+
onComplete && onComplete();
518+
return;
519+
}
520+
521+
//if an animation is currently running on the element then lets take the steps
522+
//to cancel that animation and fire any required callbacks
523+
$timeout.cancel(ngAnimateState.flagTimer);
499524
cancelAnimations(ngAnimateState.animations);
500-
ngAnimateState.done();
525+
(ngAnimateState.done || noop)();
501526
}
502527

503528
element.data(NG_ANIMATE_STATE, {
504529
running:true,
530+
structural:!isClassBased,
505531
animations:animations,
506532
done:done
507533
});
@@ -516,17 +542,14 @@ angular.module('ngAnimate', ['ng'])
516542
};
517543

518544
if(animation.start) {
519-
if(event == 'addClass' || event == 'removeClass') {
520-
animation.endFn = animation.start(element, className, fn);
521-
} else {
522-
animation.endFn = animation.start(element, fn);
523-
}
545+
animation.endFn = isClassBased ?
546+
animation.start(element, className, fn) :
547+
animation.start(element, fn);
524548
} else {
525549
fn();
526550
}
527551
});
528552

529-
530553
function progress(index) {
531554
animations[index].done = true;
532555
(animations[index].endFn || noop)();
@@ -539,7 +562,21 @@ angular.module('ngAnimate', ['ng'])
539562
function done() {
540563
if(!done.hasBeenRun) {
541564
done.hasBeenRun = true;
542-
cleanup(element);
565+
var data = element.data(NG_ANIMATE_STATE);
566+
if(data) {
567+
/* only structural animations wait for reflow before removing an
568+
animation, but class-based animations don't. An example of this
569+
failing would be when a parent HTML tag has a ng-class attribute
570+
causing ALL directives below to skip animations during the digest */
571+
if(isClassBased) {
572+
cleanup(element);
573+
} else {
574+
data.flagTimer = $timeout(function() {
575+
cleanup(element);
576+
}, 0, false);
577+
element.data(NG_ANIMATE_STATE, data);
578+
}
579+
}
543580
(onComplete || noop)();
544581
}
545582
}

test/ngAnimate/animateSpec.js

+167-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe("ngAnimate", function() {
3636

3737
describe("enable / disable", function() {
3838

39-
it("should disable and enable the animations", function() {
39+
it("should work for all animations", function() {
4040
var $animate, initialState = null;
4141

4242
angular.bootstrap(body, ['ngAnimate',function() {
@@ -56,7 +56,6 @@ describe("ngAnimate", function() {
5656
expect($animate.enabled(1)).toBe(true);
5757
expect($animate.enabled()).toBe(true);
5858
});
59-
6059
});
6160

6261
describe("with polyfill", function() {
@@ -229,6 +228,7 @@ describe("ngAnimate", function() {
229228
expect(child.attr('class')).toContain('ng-enter');
230229
expect(child.attr('class')).toContain('ng-enter-active');
231230
browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 });
231+
$timeout.flush();
232232

233233
//move
234234
element.append(after);
@@ -239,6 +239,7 @@ describe("ngAnimate", function() {
239239
expect(child.attr('class')).toContain('ng-move');
240240
expect(child.attr('class')).toContain('ng-move-active');
241241
browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 });
242+
$timeout.flush();
242243

243244
//hide
244245
$animate.addClass(child, 'ng-hide');
@@ -261,6 +262,7 @@ describe("ngAnimate", function() {
261262
expect(child.attr('class')).toContain('ng-leave');
262263
expect(child.attr('class')).toContain('ng-leave-active');
263264
browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 });
265+
$timeout.flush();
264266
}));
265267

266268
it("should not run if animations are disabled",
@@ -330,6 +332,29 @@ describe("ngAnimate", function() {
330332
expect(child.hasClass('animation-cancelled')).toBe(true);
331333
}));
332334

335+
it("should skip a class-based animation if the same element already has an ongoing structural animation",
336+
inject(function($animate, $rootScope, $sniffer, $timeout) {
337+
338+
var completed = false;
339+
$animate.enter(child, element, null, function() {
340+
completed = true;
341+
});
342+
$rootScope.$digest();
343+
344+
expect(completed).toBe(false);
345+
346+
$animate.addClass(child, 'green');
347+
expect(element.hasClass('green'));
348+
349+
expect(completed).toBe(false);
350+
if($sniffer.transitions) {
351+
$timeout.flush();
352+
browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 });
353+
}
354+
$timeout.flush();
355+
356+
expect(completed).toBe(true);
357+
}));
333358

334359
it("should fire the cancel/end function with the correct flag in the parameters",
335360
inject(function($animate, $rootScope, $sniffer, $timeout) {
@@ -722,6 +747,7 @@ describe("ngAnimate", function() {
722747
expect(element.hasClass('ng-enter-active')).toBe(true);
723748
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 22000, elapsedTime: 22000 });
724749
}
750+
$timeout.flush();
725751
expect(element.hasClass('abc')).toBe(true);
726752

727753
$rootScope.klass = 'xyz';
@@ -735,6 +761,7 @@ describe("ngAnimate", function() {
735761
expect(element.hasClass('ng-enter-active')).toBe(true);
736762
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 11000, elapsedTime: 11000 });
737763
}
764+
$timeout.flush();
738765
expect(element.hasClass('xyz')).toBe(true);
739766
}));
740767

@@ -767,7 +794,8 @@ describe("ngAnimate", function() {
767794
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 3000, elapsedTime: 3000 });
768795
}
769796

770-
expect(element.hasClass('one two')).toBe(true);
797+
expect(element.hasClass('one')).toBe(true);
798+
expect(element.hasClass('two')).toBe(true);
771799
}));
772800
});
773801

@@ -1670,6 +1698,7 @@ describe("ngAnimate", function() {
16701698

16711699
expect(animationState).toBe('enter-cancel');
16721700
$rootScope.$digest();
1701+
$timeout.flush();
16731702

16741703
$animate.addClass(child, 'something');
16751704
expect(animationState).toBe('addClass');
@@ -1711,4 +1740,139 @@ describe("ngAnimate", function() {
17111740
expect(element[0].querySelectorAll('.ng-enter-active').length).toBe(0);
17121741
}));
17131742

1743+
1744+
it("should work to disable all child animations for an element", function() {
1745+
var childAnimated = false,
1746+
containerAnimated = false;
1747+
module(function($animateProvider) {
1748+
$animateProvider.register('.child', function() {
1749+
return {
1750+
addClass : function(element, className, done) {
1751+
childAnimated = true;
1752+
done();
1753+
}
1754+
}
1755+
});
1756+
$animateProvider.register('.container', function() {
1757+
return {
1758+
leave : function(element, done) {
1759+
containerAnimated = true;
1760+
done();
1761+
}
1762+
}
1763+
});
1764+
});
1765+
1766+
inject(function($compile, $rootScope, $animate, $timeout, $rootElement) {
1767+
$animate.enabled(true);
1768+
1769+
var element = $compile('<div class="container"></div>')($rootScope);
1770+
jqLite($document[0].body).append($rootElement);
1771+
$rootElement.append(element);
1772+
1773+
var child = $compile('<div class="child"></div>')($rootScope);
1774+
element.append(child);
1775+
1776+
$animate.enabled(true, element);
1777+
1778+
$animate.addClass(child, 'awesome');
1779+
expect(childAnimated).toBe(true);
1780+
1781+
childAnimated = false;
1782+
$animate.enabled(false, element);
1783+
1784+
$animate.addClass(child, 'super');
1785+
expect(childAnimated).toBe(false);
1786+
1787+
$animate.leave(element);
1788+
$rootScope.$digest();
1789+
expect(containerAnimated).toBe(true);
1790+
});
1791+
});
1792+
1793+
it("should disable all child animations on structural animations until the first reflow has passed", function() {
1794+
var intercepted;
1795+
module(function($animateProvider) {
1796+
$animateProvider.register('.animated', function() {
1797+
return {
1798+
enter : ani('enter'),
1799+
leave : ani('leave'),
1800+
move : ani('move'),
1801+
addClass : ani('addClass'),
1802+
removeClass : ani('removeClass')
1803+
};
1804+
1805+
function ani(type) {
1806+
return function(element, className, done) {
1807+
intercepted = type;
1808+
(done || className)();
1809+
}
1810+
}
1811+
});
1812+
});
1813+
1814+
inject(function($animate, $rootScope, $sniffer, $timeout, $compile, _$rootElement_) {
1815+
$rootElement = _$rootElement_;
1816+
1817+
$animate.enabled(true);
1818+
$rootScope.$digest();
1819+
1820+
var element = $compile('<div class="element animated">...</div>')($rootScope);
1821+
var child1 = $compile('<div class="child1 animated">...</div>')($rootScope);
1822+
var child2 = $compile('<div class="child2 animated">...</div>')($rootScope);
1823+
var container = $compile('<div class="container">...</div>')($rootScope);
1824+
1825+
jqLite($document[0].body).append($rootElement);
1826+
$rootElement.append(container);
1827+
element.append(child1);
1828+
element.append(child2);
1829+
1830+
$animate.move(element, null, container);
1831+
$rootScope.$digest();
1832+
1833+
expect(intercepted).toBe('move');
1834+
1835+
$animate.addClass(child1, 'test');
1836+
expect(child1.hasClass('test')).toBe(true);
1837+
1838+
expect(intercepted).toBe('move');
1839+
$animate.leave(child1);
1840+
$rootScope.$digest();
1841+
1842+
expect(intercepted).toBe('move');
1843+
1844+
//reflow has passed
1845+
$timeout.flush();
1846+
1847+
$animate.leave(child2);
1848+
$rootScope.$digest();
1849+
expect(intercepted).toBe('leave');
1850+
});
1851+
});
1852+
1853+
it("should not disable any child animations when any parent class-based animations are run", function() {
1854+
var intercepted;
1855+
module(function($animateProvider) {
1856+
$animateProvider.register('.animated', function() {
1857+
return {
1858+
enter : function(element, done) {
1859+
intercepted = true;
1860+
done();
1861+
}
1862+
}
1863+
});
1864+
});
1865+
1866+
inject(function($animate, $rootScope, $sniffer, $timeout, $compile, $document, $rootElement) {
1867+
$animate.enabled(true);
1868+
1869+
var element = $compile('<div ng-class="{klass:bool}"> <div ng-if="bool" class="animated">value</div></div>')($rootScope);
1870+
$rootElement.append(element);
1871+
jqLite($document[0].body).append($rootElement);
1872+
1873+
$rootScope.bool = true;
1874+
$rootScope.$digest();
1875+
expect(intercepted).toBe(true);
1876+
});
1877+
});
17141878
});

0 commit comments

Comments
 (0)