Skip to content

Commit 772e320

Browse files
committed
fix($animate): make CSS blocking optional for class-based animations
$animate attempts places a `transition: none 0s` block on the element when the first CSS class is applied if a transition animation is underway. This works fine for structural animations (enter, leave and move), however, for class-based animations, this poses a big problem. As of this patch, instead of $animate placing the block, it is now the responsibility of the user to place `transition: 0s none` into their class-based transition setup CSS class. This way the animation will avoid all snapping and any will allow $animate to play nicely with class-based transitions that are defined outside of ngAnimate. Closes angular#6674 Closes angular#6739 BREAKING CHANGE: Any class-based animation code that makes use of transitions and uses the setup CSS classes (such as class-add and class-remove) must now provide a empty transition value to ensure that its styling is applied right away. In other words if your animation code is expecting any styling to be applied that is defined in the setup class then it will not be applied "instantly" default unless a `transition:0s none` value is present in the styling for that CSS class. This situation is only the case if a transition is already present on the base CSS class once the animation kicks off.
1 parent 3f609f9 commit 772e320

File tree

3 files changed

+148
-143
lines changed

3 files changed

+148
-143
lines changed

css/angular.css

-5
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,3 @@
99
ng\:form {
1010
display: block;
1111
}
12-
13-
.ng-animate-block-transitions {
14-
transition:0s all!important;
15-
-webkit-transition:0s all!important;
16-
}

src/ngAnimate/animate.js

+62-130
Original file line numberDiff line numberDiff line change
@@ -1020,8 +1020,11 @@ angular.module('ngAnimate', ['ng'])
10201020
if(parentElement.length === 0) break;
10211021

10221022
var isRoot = isMatchingElement(parentElement, $rootElement);
1023-
var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE);
1024-
var result = state && (!!state.disabled || state.running || state.totalActive > 0);
1023+
var state = isRoot ? rootAnimateState : (parentElement.data(NG_ANIMATE_STATE) || {});
1024+
var result = state.disabled || state.running
1025+
? true
1026+
: state.last && !state.last.isClassBased;
1027+
10251028
if(isRoot || result) {
10261029
return result;
10271030
}
@@ -1071,7 +1074,6 @@ angular.module('ngAnimate', ['ng'])
10711074
var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
10721075
var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey';
10731076
var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data';
1074-
var NG_ANIMATE_BLOCK_CLASS_NAME = 'ng-animate-block-transitions';
10751077
var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
10761078
var CLOSING_TIME_BUFFER = 1.5;
10771079
var ONE_SECOND = 1000;
@@ -1211,7 +1213,9 @@ angular.module('ngAnimate', ['ng'])
12111213
return parentID + '-' + extractElementNode(element).className;
12121214
}
12131215

1214-
function animateSetup(animationEvent, element, className, calculationDecorator) {
1216+
function animateSetup(animationEvent, element, className) {
1217+
var structural = ['ng-enter','ng-leave','ng-move'].indexOf(className) >= 0;
1218+
12151219
var cacheKey = getCacheKey(element);
12161220
var eventCacheKey = cacheKey + ' ' + className;
12171221
var itemIndex = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0;
@@ -1229,85 +1233,44 @@ angular.module('ngAnimate', ['ng'])
12291233
applyClasses && element.removeClass(staggerClassName);
12301234
}
12311235

1232-
/* the animation itself may need to add/remove special CSS classes
1233-
* before calculating the anmation styles */
1234-
calculationDecorator = calculationDecorator ||
1235-
function(fn) { return fn(); };
1236-
12371236
element.addClass(className);
12381237

12391238
var formerData = element.data(NG_ANIMATE_CSS_DATA_KEY) || {};
1240-
1241-
var timings = calculationDecorator(function() {
1242-
return getElementAnimationDetails(element, eventCacheKey);
1243-
});
1244-
1239+
var timings = getElementAnimationDetails(element, eventCacheKey);
12451240
var transitionDuration = timings.transitionDuration;
12461241
var animationDuration = timings.animationDuration;
1247-
if(transitionDuration === 0 && animationDuration === 0) {
1242+
1243+
if(structural && transitionDuration === 0 && animationDuration === 0) {
12481244
element.removeClass(className);
12491245
return false;
12501246
}
12511247

1248+
var blockTransition = structural && transitionDuration > 0;
1249+
var blockAnimation = animationDuration > 0 &&
1250+
stagger.animationDelay > 0 &&
1251+
stagger.animationDuration === 0;
1252+
12521253
element.data(NG_ANIMATE_CSS_DATA_KEY, {
1254+
stagger : stagger,
1255+
cacheKey : eventCacheKey,
12531256
running : formerData.running || 0,
12541257
itemIndex : itemIndex,
1255-
stagger : stagger,
1256-
timings : timings,
1258+
blockTransition : blockTransition,
1259+
blockAnimation : blockAnimation,
12571260
closeAnimationFn : noop
12581261
});
12591262

1260-
//temporarily disable the transition so that the enter styles
1261-
//don't animate twice (this is here to avoid a bug in Chrome/FF).
1262-
var isCurrentlyAnimating = formerData.running > 0 || animationEvent == 'setClass';
1263-
if(transitionDuration > 0) {
1264-
blockTransitions(element, className, isCurrentlyAnimating);
1265-
}
1266-
1267-
//staggering keyframe animations work by adjusting the `animation-delay` CSS property
1268-
//on the given element, however, the delay value can only calculated after the reflow
1269-
//since by that time $animate knows how many elements are being animated. Therefore,
1270-
//until the reflow occurs the element needs to be blocked (where the keyframe animation
1271-
//is set to `none 0s`). This blocking mechanism should only be set for when a stagger
1272-
//animation is detected and when the element item index is greater than 0.
1273-
if(animationDuration > 0 && stagger.animationDelay > 0 && stagger.animationDuration === 0) {
1274-
blockKeyframeAnimations(element);
1275-
}
1276-
1277-
return true;
1278-
}
1279-
1280-
function isStructuralAnimation(className) {
1281-
return className == 'ng-enter' || className == 'ng-move' || className == 'ng-leave';
1282-
}
1263+
var node = extractElementNode(element);
12831264

1284-
function blockTransitions(element, className, isAnimating) {
1285-
if(isStructuralAnimation(className) || !isAnimating) {
1286-
extractElementNode(element).style[TRANSITION_PROP + PROPERTY_KEY] = 'none';
1287-
} else {
1288-
element.addClass(NG_ANIMATE_BLOCK_CLASS_NAME);
1265+
if(blockTransition) {
1266+
node.style[TRANSITION_PROP + PROPERTY_KEY] = 'none';
12891267
}
1290-
}
1291-
1292-
function blockKeyframeAnimations(element) {
1293-
extractElementNode(element).style[ANIMATION_PROP] = 'none 0s';
1294-
}
12951268

1296-
function unblockTransitions(element, className) {
1297-
var prop = TRANSITION_PROP + PROPERTY_KEY;
1298-
var node = extractElementNode(element);
1299-
if(node.style[prop] && node.style[prop].length > 0) {
1300-
node.style[prop] = '';
1269+
if(blockAnimation) {
1270+
node.style[ANIMATION_PROP] = 'none 0s';
13011271
}
1302-
element.removeClass(NG_ANIMATE_BLOCK_CLASS_NAME);
1303-
}
13041272

1305-
function unblockKeyframeAnimations(element) {
1306-
var prop = ANIMATION_PROP;
1307-
var node = extractElementNode(element);
1308-
if(node.style[prop] && node.style[prop].length > 0) {
1309-
node.style[prop] = '';
1310-
}
1273+
return true;
13111274
}
13121275

13131276
function animateRun(animationEvent, element, className, activeAnimationComplete) {
@@ -1318,21 +1281,36 @@ angular.module('ngAnimate', ['ng'])
13181281
return;
13191282
}
13201283

1284+
if(elementData.blockTransition) {
1285+
node.style[TRANSITION_PROP + PROPERTY_KEY] = '';
1286+
}
1287+
1288+
if(elementData.blockAnimation) {
1289+
node.style[ANIMATION_PROP] = '';
1290+
}
1291+
13211292
var activeClassName = '';
13221293
forEach(className.split(' '), function(klass, i) {
13231294
activeClassName += (i > 0 ? ' ' : '') + klass + '-active';
13241295
});
13251296

1326-
var stagger = elementData.stagger;
1327-
var timings = elementData.timings;
1328-
var itemIndex = elementData.itemIndex;
1297+
element.addClass(activeClassName);
1298+
var eventCacheKey = elementData.eventCacheKey + ' ' + activeClassName;
1299+
var timings = getElementAnimationDetails(element, eventCacheKey);
1300+
13291301
var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration);
1302+
if(maxDuration === 0) {
1303+
element.removeClass(activeClassName);
1304+
animateClose(element, className);
1305+
activeAnimationComplete();
1306+
return;
1307+
}
1308+
13301309
var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay);
1310+
var stagger = elementData.stagger;
1311+
var itemIndex = elementData.itemIndex;
13311312
var maxDelayTime = maxDelay * ONE_SECOND;
13321313

1333-
var startTime = Date.now();
1334-
var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT;
1335-
13361314
var style = '', appliedStyles = [];
13371315
if(timings.transitionDuration > 0) {
13381316
var propertyStyle = timings.transitionPropertyStyle;
@@ -1367,8 +1345,10 @@ angular.module('ngAnimate', ['ng'])
13671345
node.setAttribute('style', oldStyle + ' ' + style);
13681346
}
13691347

1348+
var startTime = Date.now();
1349+
var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT;
1350+
13701351
element.on(css3AnimationEvents, onAnimationProgress);
1371-
element.addClass(activeClassName);
13721352
elementData.closeAnimationFn = function() {
13731353
onEnd();
13741354
activeAnimationComplete();
@@ -1460,8 +1440,6 @@ angular.module('ngAnimate', ['ng'])
14601440
//happen in the first place
14611441
var cancel = preReflowCancellation;
14621442
afterReflow(element, function() {
1463-
unblockTransitions(element, className);
1464-
unblockKeyframeAnimations(element);
14651443
//once the reflow is complete then we point cancel to
14661444
//the new cancellation function which will remove all of the
14671445
//animation properties from the active animation
@@ -1502,49 +1480,27 @@ angular.module('ngAnimate', ['ng'])
15021480
beforeSetClass : function(element, add, remove, animationCompleted) {
15031481
var className = suffixClasses(remove, '-remove') + ' ' +
15041482
suffixClasses(add, '-add');
1505-
var cancellationMethod = animateBefore('setClass', element, className, function(fn) {
1506-
/* when classes are removed from an element then the transition style
1507-
* that is applied is the transition defined on the element without the
1508-
* CSS class being there. This is how CSS3 functions outside of ngAnimate.
1509-
* http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */
1510-
var klass = element.attr('class');
1511-
element.removeClass(remove);
1512-
element.addClass(add);
1513-
var timings = fn();
1514-
element.attr('class', klass);
1515-
return timings;
1516-
});
1517-
1483+
var cancellationMethod = animateBefore('setClass', element, className);
15181484
if(cancellationMethod) {
1519-
afterReflow(element, function() {
1520-
unblockTransitions(element, className);
1521-
unblockKeyframeAnimations(element);
1522-
animationCompleted();
1523-
});
1485+
afterReflow(element, animationCompleted);
15241486
return cancellationMethod;
15251487
}
15261488
animationCompleted();
15271489
},
15281490

15291491
beforeAddClass : function(element, className, animationCompleted) {
1530-
var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'), function(fn) {
1531-
1532-
/* when a CSS class is added to an element then the transition style that
1533-
* is applied is the transition defined on the element when the CSS class
1534-
* is added at the time of the animation. This is how CSS3 functions
1535-
* outside of ngAnimate. */
1536-
element.addClass(className);
1537-
var timings = fn();
1538-
element.removeClass(className);
1539-
return timings;
1540-
});
1492+
var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'));
1493+
if(cancellationMethod) {
1494+
afterReflow(element, animationCompleted);
1495+
return cancellationMethod;
1496+
}
1497+
animationCompleted();
1498+
},
15411499

1500+
beforeRemoveClass : function(element, className, animationCompleted) {
1501+
var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'));
15421502
if(cancellationMethod) {
1543-
afterReflow(element, function() {
1544-
unblockTransitions(element, className);
1545-
unblockKeyframeAnimations(element);
1546-
animationCompleted();
1547-
});
1503+
afterReflow(element, animationCompleted);
15481504
return cancellationMethod;
15491505
}
15501506
animationCompleted();
@@ -1561,30 +1517,6 @@ angular.module('ngAnimate', ['ng'])
15611517
return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted);
15621518
},
15631519

1564-
beforeRemoveClass : function(element, className, animationCompleted) {
1565-
var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'), function(fn) {
1566-
/* when classes are removed from an element then the transition style
1567-
* that is applied is the transition defined on the element without the
1568-
* CSS class being there. This is how CSS3 functions outside of ngAnimate.
1569-
* http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */
1570-
var klass = element.attr('class');
1571-
element.removeClass(className);
1572-
var timings = fn();
1573-
element.attr('class', klass);
1574-
return timings;
1575-
});
1576-
1577-
if(cancellationMethod) {
1578-
afterReflow(element, function() {
1579-
unblockTransitions(element, className);
1580-
unblockKeyframeAnimations(element);
1581-
animationCompleted();
1582-
});
1583-
return cancellationMethod;
1584-
}
1585-
animationCompleted();
1586-
},
1587-
15881520
removeClass : function(element, className, animationCompleted) {
15891521
return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted);
15901522
}

0 commit comments

Comments
 (0)