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

Commit 931789e

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 9bc8077 commit 931789e

File tree

2 files changed

+180
-18
lines changed

2 files changed

+180
-18
lines changed

src/ngAnimate/animate.js

+74-18
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,22 @@
5858
* <ANY class="slide" ng-include="..."></ANY>
5959
* ```
6060
*
61-
* Keep in mind that if an animation is running, any child elements cannot be animated until the parent element's
62-
* animation has completed.
61+
* Keep in mind that, by default, if an animation is running, any child elements cannot be animated
62+
* until the parent element's animation has completed. This blocking feature can be overridden by
63+
* placing the `ng-animate-children` attribute on a parent container tag.
64+
*
65+
* ```html
66+
* <div class="slide-animation" ng-if="on" ng-animate-children>
67+
* <div class="fade-animation" ng-if="on">
68+
* <div class="explode-animation" ng-if="on">
69+
* ...
70+
* </div>
71+
* </div>
72+
* </div>
73+
* ```
74+
*
75+
* When the `on` expression value changes and an animation is triggered then each of the elements within
76+
* will all animate without the block being applied to child elements.
6377
*
6478
* <h2>CSS-defined Animations</h2>
6579
* The animate service will automatically apply two CSS classes to the animated element and these two CSS classes
@@ -249,6 +263,19 @@ angular.module('ngAnimate', ['ng'])
249263
* Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application.
250264
*
251265
*/
266+
.directive('ngAnimateChildren', function() {
267+
var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren';
268+
return function(scope, element, attrs) {
269+
var val = attrs.ngAnimateChildren;
270+
if(angular.isString(val) && val.length === 0) { //empty attribute
271+
element.data(NG_ANIMATE_CHILDREN, true);
272+
} else {
273+
scope.$watch(val, function(value) {
274+
element.data(NG_ANIMATE_CHILDREN, !!value);
275+
});
276+
}
277+
};
278+
})
252279

253280
//this private service is only used within CSS-enabled animations
254281
//IE8 + IE9 do not support rAF natively, but that is fine since they
@@ -277,6 +304,7 @@ angular.module('ngAnimate', ['ng'])
277304

278305
var ELEMENT_NODE = 1;
279306
var NG_ANIMATE_STATE = '$$ngAnimateState';
307+
var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren';
280308
var NG_ANIMATE_CLASS_NAME = 'ng-animate';
281309
var rootAnimateState = {running: true};
282310

@@ -326,6 +354,12 @@ angular.module('ngAnimate', ['ng'])
326354
return classNameFilter.test(className);
327355
};
328356

357+
function blockElementAnimations(element) {
358+
var data = element.data(NG_ANIMATE_STATE) || {};
359+
data.running = true;
360+
element.data(NG_ANIMATE_STATE, data);
361+
}
362+
329363
function lookup(name) {
330364
if (name) {
331365
var matches = [],
@@ -552,7 +586,7 @@ angular.module('ngAnimate', ['ng'])
552586
parentElement = prepareElement(parentElement);
553587
afterElement = prepareElement(afterElement);
554588

555-
this.enabled(false, element);
589+
blockElementAnimations(element);
556590
$delegate.enter(element, parentElement, afterElement);
557591
$rootScope.$$postDigest(function() {
558592
element = stripCommentsFromElement(element);
@@ -590,7 +624,7 @@ angular.module('ngAnimate', ['ng'])
590624
leave : function(element, doneCallback) {
591625
element = angular.element(element);
592626
cancelChildAnimations(element);
593-
this.enabled(false, element);
627+
blockElementAnimations(element);
594628
$rootScope.$$postDigest(function() {
595629
performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() {
596630
$delegate.leave(element);
@@ -634,7 +668,7 @@ angular.module('ngAnimate', ['ng'])
634668
afterElement = prepareElement(afterElement);
635669

636670
cancelChildAnimations(element);
637-
this.enabled(false, element);
671+
blockElementAnimations(element);
638672
$delegate.move(element, parentElement, afterElement);
639673
$rootScope.$$postDigest(function() {
640674
element = stripCommentsFromElement(element);
@@ -808,9 +842,12 @@ angular.module('ngAnimate', ['ng'])
808842

809843
//only allow animations if the currently running animation is not structural
810844
//or if there is no animation running at all
811-
var skipAnimations = runner.isClassBased ?
812-
ngAnimateState.disabled || (lastAnimation && !lastAnimation.isClassBased) :
813-
false;
845+
var skipAnimations;
846+
if (runner.isClassBased) {
847+
skipAnimations = ngAnimateState.running ||
848+
ngAnimateState.disabled ||
849+
(lastAnimation && !lastAnimation.isClassBased);
850+
}
814851

815852
//skip the animation if animations are disabled, a parent is already being animated,
816853
//the element is not currently attached to the document body or then completely close
@@ -1027,30 +1064,49 @@ angular.module('ngAnimate', ['ng'])
10271064
}
10281065

10291066
function animationsDisabled(element, parentElement) {
1030-
if (rootAnimateState.disabled) return true;
1067+
if (rootAnimateState.disabled) {
1068+
return true;
1069+
}
10311070

1032-
if(isMatchingElement(element, $rootElement)) {
1033-
return rootAnimateState.disabled || rootAnimateState.running;
1071+
if (isMatchingElement(element, $rootElement)) {
1072+
return rootAnimateState.running;
10341073
}
10351074

1075+
var allowChildAnimations, parentRunningAnimation, hasParent;
10361076
do {
10371077
//the element did not reach the root element which means that it
10381078
//is not apart of the DOM. Therefore there is no reason to do
10391079
//any animations on it
1040-
if(parentElement.length === 0) break;
1080+
if (parentElement.length === 0) break;
10411081

10421082
var isRoot = isMatchingElement(parentElement, $rootElement);
1043-
var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE);
1044-
var result = state && (!!state.disabled || state.running || state.totalActive > 0);
1045-
if(isRoot || result) {
1046-
return result;
1083+
var state = isRoot ? rootAnimateState : (parentElement.data(NG_ANIMATE_STATE) || {});
1084+
if (state.disabled) {
1085+
return true;
1086+
}
1087+
1088+
//no matter what, for an animation to work it must reach the root element
1089+
//this implies that the element is attached to the DOM when the animation is run
1090+
if (isRoot) {
1091+
hasParent = true;
10471092
}
10481093

1049-
if(isRoot) return true;
1094+
//once a flag is found that is strictly false then everything before
1095+
//it will be discarded and all child animations will be restricted
1096+
if (allowChildAnimations !== false) {
1097+
var animateChildrenFlag = parentElement.data(NG_ANIMATE_CHILDREN);
1098+
if(angular.isDefined(animateChildrenFlag)) {
1099+
allowChildAnimations = animateChildrenFlag;
1100+
}
1101+
}
1102+
1103+
parentRunningAnimation = parentRunningAnimation ||
1104+
state.running ||
1105+
(state.last && !state.last.isClassBased);
10501106
}
10511107
while(parentElement = parentElement.parent());
10521108

1053-
return true;
1109+
return !hasParent || (!allowChildAnimations && parentRunningAnimation);
10541110
}
10551111
}]);
10561112

test/ngAnimate/animateSpec.js

+106
Original file line numberDiff line numberDiff line change
@@ -3866,6 +3866,112 @@ describe("ngAnimate", function() {
38663866
expect(element.children().length).toBe(0);
38673867
}));
38683868

3869+
describe('ngAnimateChildren', function() {
3870+
var spy;
3871+
3872+
beforeEach(module(function($animateProvider) {
3873+
spy = jasmine.createSpy();
3874+
$animateProvider.register('.parent', mockAnimate);
3875+
$animateProvider.register('.child', mockAnimate);
3876+
return function($animate) {
3877+
$animate.enabled(true);
3878+
};
3879+
3880+
function mockAnimate() {
3881+
return {
3882+
enter : spy,
3883+
leave : spy,
3884+
addClass : spy,
3885+
removeClass : spy
3886+
};
3887+
}
3888+
}));
3889+
3890+
it('should animate based on a boolean flag', inject(function($animate, $sniffer, $rootScope, $compile) {
3891+
var html = '<section class="parent" ng-if="on1" ng-animate-children="bool">' +
3892+
' <div class="child" ng-if="on2">...</div>' +
3893+
'</section>';
3894+
3895+
var element = $compile(html)($rootScope);
3896+
$rootElement.append(element);
3897+
jqLite($document[0].body).append($rootElement);
3898+
3899+
var scope = $rootScope;
3900+
3901+
scope.bool = true;
3902+
scope.$digest();
3903+
3904+
scope.on1 = true;
3905+
scope.on2 = true;
3906+
scope.$digest();
3907+
3908+
$animate.triggerReflow();
3909+
3910+
expect(spy).toHaveBeenCalled();
3911+
expect(spy.callCount).toBe(2);
3912+
3913+
scope.bool = false;
3914+
scope.$digest();
3915+
3916+
scope.on1 = false;
3917+
scope.$digest();
3918+
3919+
scope.on2 = false;
3920+
scope.$digest();
3921+
3922+
$animate.triggerReflow();
3923+
3924+
expect(spy.callCount).toBe(3);
3925+
}));
3926+
3927+
it('should default to true when no expression is provided',
3928+
inject(function($animate, $sniffer, $rootScope, $compile) {
3929+
3930+
var html = '<section class="parent" ng-if="on1" ng-animate-children>' +
3931+
' <div class="child" ng-if="on2">...</div>' +
3932+
'</section>';
3933+
3934+
var element = $compile(html)($rootScope);
3935+
$rootElement.append(element);
3936+
jqLite($document[0].body).append($rootElement);
3937+
3938+
$rootScope.on1 = true;
3939+
$rootScope.$digest();
3940+
3941+
$rootScope.on2 = true;
3942+
$rootScope.$digest();
3943+
3944+
$animate.triggerReflow();
3945+
3946+
expect(spy).toHaveBeenCalled();
3947+
expect(spy.callCount).toBe(2);
3948+
}));
3949+
3950+
it('should not perform inherited animations if any parent restricts it',
3951+
inject(function($animate, $sniffer, $rootScope, $compile) {
3952+
3953+
var html = '<section ng-animate-children="false">' +
3954+
' <aside class="parent" ng-if="on" ng-animate-children="true">' +
3955+
' <div class="child" ng-if="on">...</div>' +
3956+
' </aside>' +
3957+
'</section>';
3958+
3959+
var element = $compile(html)($rootScope);
3960+
$rootElement.append(element);
3961+
jqLite($document[0].body).append($rootElement);
3962+
3963+
$rootScope.$digest();
3964+
3965+
$rootScope.on = true;
3966+
$rootScope.$digest();
3967+
3968+
$animate.triggerReflow();
3969+
3970+
expect(spy).toHaveBeenCalled();
3971+
expect(spy.callCount).toBe(1);
3972+
}));
3973+
});
3974+
38693975
describe('SVG', function() {
38703976
it('should properly apply transitions on an SVG element',
38713977
inject(function($animate, $rootScope, $compile, $rootElement, $sniffer) {

0 commit comments

Comments
 (0)