diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js
index 7f31a8f99dc8..2e67aa89e330 100644
--- a/src/ngAnimate/animate.js
+++ b/src/ngAnimate/animate.js
@@ -56,8 +56,22 @@
*
* ```
*
- * Keep in mind that if an animation is running, any child elements cannot be animated until the parent element's
- * animation has completed.
+ * Keep in mind that, by default, if an animation is running, any child elements cannot be animated
+ * until the parent element's animation has completed. This blocking feature can be overridden by
+ * placing the `ng-animate-children` attribute on a parent container tag.
+ *
+ * ```html
+ *
+ * ```
+ *
+ * When the `on` expression value changes and an animation is triggered then each of the elements within
+ * will all animate without the block being applied to child elements.
*
* CSS-defined Animations
* 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'])
* Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application.
*
*/
+ .directive('ngAnimateChildren', function() {
+ var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren';
+ return function(scope, element, attrs) {
+ var val = attrs.ngAnimateChildren;
+ if(angular.isString(val) && val.length === 0) { //empty attribute
+ element.data(NG_ANIMATE_CHILDREN, true);
+ } else {
+ scope.$watch(val, function(value) {
+ element.data(NG_ANIMATE_CHILDREN, !!value);
+ });
+ }
+ };
+ })
//this private service is only used within CSS-enabled animations
//IE8 + IE9 do not support rAF natively, but that is fine since they
@@ -342,6 +369,7 @@ angular.module('ngAnimate', ['ng'])
var ELEMENT_NODE = 1;
var NG_ANIMATE_STATE = '$$ngAnimateState';
+ var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren';
var NG_ANIMATE_CLASS_NAME = 'ng-animate';
var rootAnimateState = {running: true};
@@ -391,6 +419,12 @@ angular.module('ngAnimate', ['ng'])
return classNameFilter.test(className);
};
+ function blockElementAnimations(element) {
+ var data = element.data(NG_ANIMATE_STATE) || {};
+ data.running = true;
+ element.data(NG_ANIMATE_STATE, data);
+ }
+
function lookup(name) {
if (name) {
var matches = [],
@@ -620,7 +654,7 @@ angular.module('ngAnimate', ['ng'])
parentElement = prepareElement(parentElement);
afterElement = prepareElement(afterElement);
- this.enabled(false, element);
+ blockElementAnimations(element);
$delegate.enter(element, parentElement, afterElement);
$rootScope.$$postDigest(function() {
element = stripCommentsFromElement(element);
@@ -661,7 +695,7 @@ angular.module('ngAnimate', ['ng'])
leave : function(element, doneCallback) {
element = angular.element(element);
cancelChildAnimations(element);
- this.enabled(false, element);
+ blockElementAnimations(element);
$rootScope.$$postDigest(function() {
performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() {
$delegate.leave(element);
@@ -708,7 +742,7 @@ angular.module('ngAnimate', ['ng'])
afterElement = prepareElement(afterElement);
cancelChildAnimations(element);
- this.enabled(false, element);
+ blockElementAnimations(element);
$delegate.move(element, parentElement, afterElement);
$rootScope.$$postDigest(function() {
element = stripCommentsFromElement(element);
@@ -895,9 +929,12 @@ angular.module('ngAnimate', ['ng'])
//only allow animations if the currently running animation is not structural
//or if there is no animation running at all
- var skipAnimations = runner.isClassBased
- ? ngAnimateState.disabled || (lastAnimation && !lastAnimation.isClassBased)
- : false;
+ var skipAnimations;
+ if (runner.isClassBased) {
+ skipAnimations = ngAnimateState.running ||
+ ngAnimateState.disabled ||
+ (lastAnimation && !lastAnimation.isClassBased);
+ }
//skip the animation if animations are disabled, a parent is already being animated,
//the element is not currently attached to the document body or then completely close
@@ -1114,33 +1151,49 @@ angular.module('ngAnimate', ['ng'])
}
function animationsDisabled(element, parentElement) {
- if (rootAnimateState.disabled) return true;
+ if (rootAnimateState.disabled) {
+ return true;
+ }
- if(isMatchingElement(element, $rootElement)) {
- return rootAnimateState.disabled || rootAnimateState.running;
+ if (isMatchingElement(element, $rootElement)) {
+ return rootAnimateState.running;
}
+ var allowChildAnimations, parentRunningAnimation, hasParent;
do {
//the element did not reach the root element which means that it
//is not apart of the DOM. Therefore there is no reason to do
//any animations on it
- if(parentElement.length === 0) break;
+ if (parentElement.length === 0) break;
var isRoot = isMatchingElement(parentElement, $rootElement);
var state = isRoot ? rootAnimateState : (parentElement.data(NG_ANIMATE_STATE) || {});
- var result = state.disabled || state.running
- ? true
- : state.last && !state.last.isClassBased;
+ if (state.disabled) {
+ return true;
+ }
- if(isRoot || result) {
- return result;
+ //no matter what, for an animation to work it must reach the root element
+ //this implies that the element is attached to the DOM when the animation is run
+ if (isRoot) {
+ hasParent = true;
}
- if(isRoot) return true;
+ //once a flag is found that is strictly false then everything before
+ //it will be discarded and all child animations will be restricted
+ if (allowChildAnimations !== false) {
+ var animateChildrenFlag = parentElement.data(NG_ANIMATE_CHILDREN);
+ if(angular.isDefined(animateChildrenFlag)) {
+ allowChildAnimations = animateChildrenFlag;
+ }
+ }
+
+ parentRunningAnimation = parentRunningAnimation ||
+ state.running ||
+ (state.last && !state.last.isClassBased);
}
while(parentElement = parentElement.parent());
- return true;
+ return !hasParent || (!allowChildAnimations && parentRunningAnimation);
}
}]);
diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js
index 56fcd7f5968c..737d2f1d0323 100644
--- a/test/ngAnimate/animateSpec.js
+++ b/test/ngAnimate/animateSpec.js
@@ -3986,6 +3986,112 @@ describe("ngAnimate", function() {
expect(element.children().length).toBe(0);
}));
+ describe('ngAnimateChildren', function() {
+ var spy;
+
+ beforeEach(module(function($animateProvider) {
+ spy = jasmine.createSpy();
+ $animateProvider.register('.parent', mockAnimate);
+ $animateProvider.register('.child', mockAnimate);
+ return function($animate) {
+ $animate.enabled(true);
+ };
+
+ function mockAnimate() {
+ return {
+ enter : spy,
+ leave : spy,
+ addClass : spy,
+ removeClass : spy
+ };
+ }
+ }));
+
+ it('should animate based on a boolean flag', inject(function($animate, $sniffer, $rootScope, $compile) {
+ var html = '';
+
+ var element = $compile(html)($rootScope);
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ var scope = $rootScope;
+
+ scope.bool = true;
+ scope.$digest();
+
+ scope.on1 = true;
+ scope.on2 = true;
+ scope.$digest();
+
+ $animate.triggerReflow();
+
+ expect(spy).toHaveBeenCalled();
+ expect(spy.callCount).toBe(2);
+
+ scope.bool = false;
+ scope.$digest();
+
+ scope.on1 = false;
+ scope.$digest();
+
+ scope.on2 = false;
+ scope.$digest();
+
+ $animate.triggerReflow();
+
+ expect(spy.callCount).toBe(3);
+ }));
+
+ it('should default to true when no expression is provided',
+ inject(function($animate, $sniffer, $rootScope, $compile) {
+
+ var html = '';
+
+ var element = $compile(html)($rootScope);
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ $rootScope.on1 = true;
+ $rootScope.$digest();
+
+ $rootScope.on2 = true;
+ $rootScope.$digest();
+
+ $animate.triggerReflow();
+
+ expect(spy).toHaveBeenCalled();
+ expect(spy.callCount).toBe(2);
+ }));
+
+ it('should not perform inherited animations if any parent restricts it',
+ inject(function($animate, $sniffer, $rootScope, $compile) {
+
+ var html = '';
+
+ var element = $compile(html)($rootScope);
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ $rootScope.$digest();
+
+ $rootScope.on = true;
+ $rootScope.$digest();
+
+ $animate.triggerReflow();
+
+ expect(spy).toHaveBeenCalled();
+ expect(spy.callCount).toBe(1);
+ }));
+ });
+
describe('SVG', function() {
it('should properly apply transitions on an SVG element',
inject(function($animate, $rootScope, $compile, $rootElement, $sniffer) {