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

Commit 8252b8b

Browse files
committed
feat(ngAnimate): conditionally allow child animations to run in parallel with parent animations
By default ngAnimate prevents child animations from running when a parent is performing an animation. However there are a cases when an application should allow all child animations to run without blocking each other. By placing the `ng-animate-children` flag in the template, this effect can now be put to use within the template. Closes #7946
1 parent 2c7d085 commit 8252b8b

File tree

2 files changed

+178
-19
lines changed

2 files changed

+178
-19
lines changed

src/ngAnimate/animate.js

+72-19
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,22 @@
5656
* <ANY class="slide" ng-include="..."></ANY>
5757
* ```
5858
*
59-
* Keep in mind that if an animation is running, any child elements cannot be animated until the parent element's
60-
* animation has completed.
59+
* Keep in mind that, by default, if an animation is running, any child elements cannot be animated
60+
* until the parent element's animation has completed. This blocking feature can be overridden by
61+
* placing the `ng-animate-children` attribute on a parent container tag.
62+
*
63+
* ```html
64+
* <div class="slide-animation" ng-if="on" ng-animate-children>
65+
* <div class="fade-animation" ng-if="on">
66+
* <div class="explode-animation" ng-if="on">
67+
* ...
68+
* </div>
69+
* </div>
70+
* </div>
71+
* ```
72+
*
73+
* When the `on` expression value changes and an animation is triggered then each of the elements within
74+
* will all animate without the block being applied to child elements.
6175
*
6276
* <h2>CSS-defined Animations</h2>
6377
* The animate service will automatically apply two CSS classes to the animated element and these two CSS classes
@@ -314,6 +328,19 @@ angular.module('ngAnimate', ['ng'])
314328
* Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application.
315329
*
316330
*/
331+
.directive('ngAnimateChildren', function() {
332+
var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren';
333+
return function(scope, element, attrs) {
334+
var val = attrs.ngAnimateChildren;
335+
if(angular.isString(val) && val.length === 0) { //empty attribute
336+
element.data(NG_ANIMATE_CHILDREN, true);
337+
} else {
338+
scope.$watch(val, function(value) {
339+
element.data(NG_ANIMATE_CHILDREN, !!value);
340+
});
341+
}
342+
};
343+
})
317344

318345
//this private service is only used within CSS-enabled animations
319346
//IE8 + IE9 do not support rAF natively, but that is fine since they
@@ -342,6 +369,7 @@ angular.module('ngAnimate', ['ng'])
342369

343370
var ELEMENT_NODE = 1;
344371
var NG_ANIMATE_STATE = '$$ngAnimateState';
372+
var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren';
345373
var NG_ANIMATE_CLASS_NAME = 'ng-animate';
346374
var rootAnimateState = {running: true};
347375

@@ -391,6 +419,12 @@ angular.module('ngAnimate', ['ng'])
391419
return classNameFilter.test(className);
392420
};
393421

422+
function blockElementAnimations(element) {
423+
var data = element.data(NG_ANIMATE_STATE) || {};
424+
data.running = true;
425+
element.data(NG_ANIMATE_STATE, data);
426+
}
427+
394428
function lookup(name) {
395429
if (name) {
396430
var matches = [],
@@ -620,7 +654,7 @@ angular.module('ngAnimate', ['ng'])
620654
parentElement = prepareElement(parentElement);
621655
afterElement = prepareElement(afterElement);
622656

623-
this.enabled(false, element);
657+
blockElementAnimations(element);
624658
$delegate.enter(element, parentElement, afterElement);
625659
$rootScope.$$postDigest(function() {
626660
element = stripCommentsFromElement(element);
@@ -661,7 +695,7 @@ angular.module('ngAnimate', ['ng'])
661695
leave : function(element, doneCallback) {
662696
element = angular.element(element);
663697
cancelChildAnimations(element);
664-
this.enabled(false, element);
698+
blockElementAnimations(element);
665699
$rootScope.$$postDigest(function() {
666700
performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() {
667701
$delegate.leave(element);
@@ -708,7 +742,7 @@ angular.module('ngAnimate', ['ng'])
708742
afterElement = prepareElement(afterElement);
709743

710744
cancelChildAnimations(element);
711-
this.enabled(false, element);
745+
blockElementAnimations(element);
712746
$delegate.move(element, parentElement, afterElement);
713747
$rootScope.$$postDigest(function() {
714748
element = stripCommentsFromElement(element);
@@ -895,9 +929,12 @@ angular.module('ngAnimate', ['ng'])
895929

896930
//only allow animations if the currently running animation is not structural
897931
//or if there is no animation running at all
898-
var skipAnimations = runner.isClassBased
899-
? ngAnimateState.disabled || (lastAnimation && !lastAnimation.isClassBased)
900-
: false;
932+
var skipAnimations;
933+
if (runner.isClassBased) {
934+
skipAnimations = ngAnimateState.running ||
935+
ngAnimateState.disabled ||
936+
(lastAnimation && !lastAnimation.isClassBased);
937+
}
901938

902939
//skip the animation if animations are disabled, a parent is already being animated,
903940
//the element is not currently attached to the document body or then completely close
@@ -1114,33 +1151,49 @@ angular.module('ngAnimate', ['ng'])
11141151
}
11151152

11161153
function animationsDisabled(element, parentElement) {
1117-
if (rootAnimateState.disabled) return true;
1154+
if (rootAnimateState.disabled) {
1155+
return true;
1156+
}
11181157

1119-
if(isMatchingElement(element, $rootElement)) {
1120-
return rootAnimateState.disabled || rootAnimateState.running;
1158+
if (isMatchingElement(element, $rootElement)) {
1159+
return rootAnimateState.running;
11211160
}
11221161

1162+
var allowChildAnimations, parentRunningAnimation, hasParent;
11231163
do {
11241164
//the element did not reach the root element which means that it
11251165
//is not apart of the DOM. Therefore there is no reason to do
11261166
//any animations on it
1127-
if(parentElement.length === 0) break;
1167+
if (parentElement.length === 0) break;
11281168

11291169
var isRoot = isMatchingElement(parentElement, $rootElement);
11301170
var state = isRoot ? rootAnimateState : (parentElement.data(NG_ANIMATE_STATE) || {});
1131-
var result = state.disabled || state.running
1132-
? true
1133-
: state.last && !state.last.isClassBased;
1171+
if (state.disabled) {
1172+
return true;
1173+
}
11341174

1135-
if(isRoot || result) {
1136-
return result;
1175+
//no matter what, for an animation to work it must reach the root element
1176+
//this implies that the element is attached to the DOM when the animation is run
1177+
if (isRoot) {
1178+
hasParent = true;
11371179
}
11381180

1139-
if(isRoot) return true;
1181+
//once a flag is found that is strictly false then everything before
1182+
//it will be discarded and all child animations will be restricted
1183+
if (allowChildAnimations !== false) {
1184+
var animateChildrenFlag = parentElement.data(NG_ANIMATE_CHILDREN);
1185+
if(angular.isDefined(animateChildrenFlag)) {
1186+
allowChildAnimations = animateChildrenFlag;
1187+
}
1188+
}
1189+
1190+
parentRunningAnimation = parentRunningAnimation ||
1191+
state.running ||
1192+
(state.last && !state.last.isClassBased);
11401193
}
11411194
while(parentElement = parentElement.parent());
11421195

1143-
return true;
1196+
return !hasParent || (!allowChildAnimations && parentRunningAnimation);
11441197
}
11451198
}]);
11461199

test/ngAnimate/animateSpec.js

+106
Original file line numberDiff line numberDiff line change
@@ -3986,6 +3986,112 @@ describe("ngAnimate", function() {
39863986
expect(element.children().length).toBe(0);
39873987
}));
39883988

3989+
describe('ngAnimateChildren', function() {
3990+
var spy;
3991+
3992+
beforeEach(module(function($animateProvider) {
3993+
spy = jasmine.createSpy();
3994+
$animateProvider.register('.parent', mockAnimate);
3995+
$animateProvider.register('.child', mockAnimate);
3996+
return function($animate) {
3997+
$animate.enabled(true);
3998+
};
3999+
4000+
function mockAnimate() {
4001+
return {
4002+
enter : spy,
4003+
leave : spy,
4004+
addClass : spy,
4005+
removeClass : spy
4006+
};
4007+
}
4008+
}));
4009+
4010+
it('should animate based on a boolean flag', inject(function($animate, $sniffer, $rootScope, $compile) {
4011+
var html = '<section class="parent" ng-if="on1" ng-animate-children="bool">' +
4012+
' <div class="child" ng-if="on2">...</div>' +
4013+
'</section>';
4014+
4015+
var element = $compile(html)($rootScope);
4016+
$rootElement.append(element);
4017+
jqLite($document[0].body).append($rootElement);
4018+
4019+
var scope = $rootScope;
4020+
4021+
scope.bool = true;
4022+
scope.$digest();
4023+
4024+
scope.on1 = true;
4025+
scope.on2 = true;
4026+
scope.$digest();
4027+
4028+
$animate.triggerReflow();
4029+
4030+
expect(spy).toHaveBeenCalled();
4031+
expect(spy.callCount).toBe(2);
4032+
4033+
scope.bool = false;
4034+
scope.$digest();
4035+
4036+
scope.on1 = false;
4037+
scope.$digest();
4038+
4039+
scope.on2 = false;
4040+
scope.$digest();
4041+
4042+
$animate.triggerReflow();
4043+
4044+
expect(spy.callCount).toBe(3);
4045+
}));
4046+
4047+
it('should default to true when no expression is provided',
4048+
inject(function($animate, $sniffer, $rootScope, $compile) {
4049+
4050+
var html = '<section class="parent" ng-if="on1" ng-animate-children>' +
4051+
' <div class="child" ng-if="on2">...</div>' +
4052+
'</section>';
4053+
4054+
var element = $compile(html)($rootScope);
4055+
$rootElement.append(element);
4056+
jqLite($document[0].body).append($rootElement);
4057+
4058+
$rootScope.on1 = true;
4059+
$rootScope.$digest();
4060+
4061+
$rootScope.on2 = true;
4062+
$rootScope.$digest();
4063+
4064+
$animate.triggerReflow();
4065+
4066+
expect(spy).toHaveBeenCalled();
4067+
expect(spy.callCount).toBe(2);
4068+
}));
4069+
4070+
it('should not perform inherited animations if any parent restricts it',
4071+
inject(function($animate, $sniffer, $rootScope, $compile) {
4072+
4073+
var html = '<section ng-animate-children="false">' +
4074+
' <aside class="parent" ng-if="on" ng-animate-children="true">' +
4075+
' <div class="child" ng-if="on">...</div>' +
4076+
' </aside>' +
4077+
'</section>';
4078+
4079+
var element = $compile(html)($rootScope);
4080+
$rootElement.append(element);
4081+
jqLite($document[0].body).append($rootElement);
4082+
4083+
$rootScope.$digest();
4084+
4085+
$rootScope.on = true;
4086+
$rootScope.$digest();
4087+
4088+
$animate.triggerReflow();
4089+
4090+
expect(spy).toHaveBeenCalled();
4091+
expect(spy.callCount).toBe(1);
4092+
}));
4093+
});
4094+
39894095
describe('SVG', function() {
39904096
it('should properly apply transitions on an SVG element',
39914097
inject(function($animate, $rootScope, $compile, $rootElement, $sniffer) {

0 commit comments

Comments
 (0)