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) {