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

feat(ngAnimate): conditionally allow child animations to run in parallel with parent animations #7946

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 72 additions & 19 deletions src/ngAnimate/animate.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,22 @@
* <ANY class="slide" ng-include="..."></ANY>
* ```
*
* 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
* <div class="slide-animation" ng-if="on" ng-animate-children>
* <div class="fade-animation" ng-if="on">
* <div class="explode-animation" ng-if="on">
* ...
* </div>
* </div>
* </div>
* ```
*
* 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.
*
* <h2>CSS-defined Animations</h2>
* The animate service will automatically apply two CSS classes to the animated element and these two CSS classes
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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};

Expand Down Expand Up @@ -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 = [],
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}]);

Expand Down
106 changes: 106 additions & 0 deletions test/ngAnimate/animateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<section class="parent" ng-if="on1" ng-animate-children="bool">' +
' <div class="child" ng-if="on2">...</div>' +
'</section>';

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 = '<section class="parent" ng-if="on1" ng-animate-children>' +
' <div class="child" ng-if="on2">...</div>' +
'</section>';

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 = '<section ng-animate-children="false">' +
' <aside class="parent" ng-if="on" ng-animate-children="true">' +
' <div class="child" ng-if="on">...</div>' +
' </aside>' +
'</section>';

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