Skip to content

Commit

Permalink
fix($animate): make CSS blocking optional for class-based animations
Browse files Browse the repository at this point in the history
$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.
  • Loading branch information
matsko committed Mar 25, 2014
1 parent 3f609f9 commit 037d538
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 145 deletions.
5 changes: 0 additions & 5 deletions css/angular.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,3 @@
ng\:form {
display: block;
}

.ng-animate-block-transitions {
transition:0s all!important;
-webkit-transition:0s all!important;
}
196 changes: 64 additions & 132 deletions src/ngAnimate/animate.js
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,7 @@ angular.module('ngAnimate', ['ng'])
//the element is not currently attached to the document body or then completely close
//the animation if any matching animations are not found at all.
//NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case an animation was found.
if (skipAnimations || animationsDisabled(element, parentElement)) {
if (skipAnimations || animationsDisabled(element, parentElement, className)) {
fireDOMOperation();
fireBeforeCallbackAsync();
fireAfterCallbackAsync();
Expand Down Expand Up @@ -1020,8 +1020,11 @@ angular.module('ngAnimate', ['ng'])
if(parentElement.length === 0) break;

var isRoot = isMatchingElement(parentElement, $rootElement);
var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE);
var result = state && (!!state.disabled || state.running || state.totalActive > 0);
var state = isRoot ? rootAnimateState : (parentElement.data(NG_ANIMATE_STATE) || {});
var result = state.disabled || state.running
? true
: state.last && !state.last.isClassBased;

if(isRoot || result) {
return result;
}
Expand Down Expand Up @@ -1071,7 +1074,6 @@ angular.module('ngAnimate', ['ng'])
var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey';
var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data';
var NG_ANIMATE_BLOCK_CLASS_NAME = 'ng-animate-block-transitions';
var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
var CLOSING_TIME_BUFFER = 1.5;
var ONE_SECOND = 1000;
Expand Down Expand Up @@ -1211,7 +1213,9 @@ angular.module('ngAnimate', ['ng'])
return parentID + '-' + extractElementNode(element).className;
}

function animateSetup(animationEvent, element, className, calculationDecorator) {
function animateSetup(animationEvent, element, className) {
var structural = ['ng-enter','ng-leave','ng-move'].indexOf(className) >= 0;

var cacheKey = getCacheKey(element);
var eventCacheKey = cacheKey + ' ' + className;
var itemIndex = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0;
Expand All @@ -1229,85 +1233,44 @@ angular.module('ngAnimate', ['ng'])
applyClasses && element.removeClass(staggerClassName);
}

/* the animation itself may need to add/remove special CSS classes
* before calculating the anmation styles */
calculationDecorator = calculationDecorator ||
function(fn) { return fn(); };

element.addClass(className);

var formerData = element.data(NG_ANIMATE_CSS_DATA_KEY) || {};

var timings = calculationDecorator(function() {
return getElementAnimationDetails(element, eventCacheKey);
});

var timings = getElementAnimationDetails(element, eventCacheKey);
var transitionDuration = timings.transitionDuration;
var animationDuration = timings.animationDuration;
if(transitionDuration === 0 && animationDuration === 0) {

if(structural && transitionDuration == 0 && animationDuration == 0) {
element.removeClass(className);
return false;
}
};

var blockTransition = structural && transitionDuration > 0;
var blockAnimation = animationDuration > 0 &&
stagger.animationDelay > 0 &&
stagger.animationDuration === 0;

element.data(NG_ANIMATE_CSS_DATA_KEY, {
stagger : stagger,
cacheKey : eventCacheKey,
running : formerData.running || 0,
itemIndex : itemIndex,
stagger : stagger,
timings : timings,
blockTransition : blockTransition,
blockAnimation : blockAnimation,
closeAnimationFn : noop
});

//temporarily disable the transition so that the enter styles
//don't animate twice (this is here to avoid a bug in Chrome/FF).
var isCurrentlyAnimating = formerData.running > 0 || animationEvent == 'setClass';
if(transitionDuration > 0) {
blockTransitions(element, className, isCurrentlyAnimating);
}

//staggering keyframe animations work by adjusting the `animation-delay` CSS property
//on the given element, however, the delay value can only calculated after the reflow
//since by that time $animate knows how many elements are being animated. Therefore,
//until the reflow occurs the element needs to be blocked (where the keyframe animation
//is set to `none 0s`). This blocking mechanism should only be set for when a stagger
//animation is detected and when the element item index is greater than 0.
if(animationDuration > 0 && stagger.animationDelay > 0 && stagger.animationDuration === 0) {
blockKeyframeAnimations(element);
}

return true;
}

function isStructuralAnimation(className) {
return className == 'ng-enter' || className == 'ng-move' || className == 'ng-leave';
}
var node = extractElementNode(element);

function blockTransitions(element, className, isAnimating) {
if(isStructuralAnimation(className) || !isAnimating) {
extractElementNode(element).style[TRANSITION_PROP + PROPERTY_KEY] = 'none';
} else {
element.addClass(NG_ANIMATE_BLOCK_CLASS_NAME);
if(blockTransition) {
node.style[TRANSITION_PROP + PROPERTY_KEY] = 'none';
}
}

function blockKeyframeAnimations(element) {
extractElementNode(element).style[ANIMATION_PROP] = 'none 0s';
}

function unblockTransitions(element, className) {
var prop = TRANSITION_PROP + PROPERTY_KEY;
var node = extractElementNode(element);
if(node.style[prop] && node.style[prop].length > 0) {
node.style[prop] = '';
if(blockAnimation) {
node.style[ANIMATION_PROP] = 'none 0s';
}
element.removeClass(NG_ANIMATE_BLOCK_CLASS_NAME);
}

function unblockKeyframeAnimations(element) {
var prop = ANIMATION_PROP;
var node = extractElementNode(element);
if(node.style[prop] && node.style[prop].length > 0) {
node.style[prop] = '';
}
return true;
}

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

if(elementData.blockTransition) {
node.style[TRANSITION_PROP + PROPERTY_KEY] = '';
}

if(elementData.blockAnimation) {
node.style[ANIMATION_PROP] = '';
}

var activeClassName = '';
forEach(className.split(' '), function(klass, i) {
activeClassName += (i > 0 ? ' ' : '') + klass + '-active';
});

var stagger = elementData.stagger;
var timings = elementData.timings;
var itemIndex = elementData.itemIndex;
element.addClass(activeClassName);
var eventCacheKey = elementData.eventCacheKey + ' ' + activeClassName;
var timings = getElementAnimationDetails(element, eventCacheKey);

var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration);
if(maxDuration == 0) {
element.removeClass(activeClassName);
animateClose(element, className);
activeAnimationComplete();
return;
}

var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay);
var stagger = elementData.stagger;
var itemIndex = elementData.itemIndex;
var maxDelayTime = maxDelay * ONE_SECOND;

var startTime = Date.now();
var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT;

var style = '', appliedStyles = [];
if(timings.transitionDuration > 0) {
var propertyStyle = timings.transitionPropertyStyle;
Expand Down Expand Up @@ -1367,8 +1345,10 @@ angular.module('ngAnimate', ['ng'])
node.setAttribute('style', oldStyle + ' ' + style);
}

var startTime = Date.now();
var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT;

element.on(css3AnimationEvents, onAnimationProgress);
element.addClass(activeClassName);
elementData.closeAnimationFn = function() {
onEnd();
activeAnimationComplete();
Expand Down Expand Up @@ -1460,8 +1440,6 @@ angular.module('ngAnimate', ['ng'])
//happen in the first place
var cancel = preReflowCancellation;
afterReflow(element, function() {
unblockTransitions(element, className);
unblockKeyframeAnimations(element);
//once the reflow is complete then we point cancel to
//the new cancellation function which will remove all of the
//animation properties from the active animation
Expand Down Expand Up @@ -1502,49 +1480,27 @@ angular.module('ngAnimate', ['ng'])
beforeSetClass : function(element, add, remove, animationCompleted) {
var className = suffixClasses(remove, '-remove') + ' ' +
suffixClasses(add, '-add');
var cancellationMethod = animateBefore('setClass', element, className, function(fn) {
/* when classes are removed from an element then the transition style
* that is applied is the transition defined on the element without the
* CSS class being there. This is how CSS3 functions outside of ngAnimate.
* http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */
var klass = element.attr('class');
element.removeClass(remove);
element.addClass(add);
var timings = fn();
element.attr('class', klass);
return timings;
});

var cancellationMethod = animateBefore('setClass', element, className);
if(cancellationMethod) {
afterReflow(element, function() {
unblockTransitions(element, className);
unblockKeyframeAnimations(element);
animationCompleted();
});
afterReflow(element, animationCompleted);
return cancellationMethod;
}
animationCompleted();
},

beforeAddClass : function(element, className, animationCompleted) {
var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'), function(fn) {

/* when a CSS class is added to an element then the transition style that
* is applied is the transition defined on the element when the CSS class
* is added at the time of the animation. This is how CSS3 functions
* outside of ngAnimate. */
element.addClass(className);
var timings = fn();
element.removeClass(className);
return timings;
});
var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'));
if(cancellationMethod) {
afterReflow(element, animationCompleted);
return cancellationMethod;
}
animationCompleted();
},

beforeRemoveClass : function(element, className, animationCompleted) {
var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'));
if(cancellationMethod) {
afterReflow(element, function() {
unblockTransitions(element, className);
unblockKeyframeAnimations(element);
animationCompleted();
});
afterReflow(element, animationCompleted);
return cancellationMethod;
}
animationCompleted();
Expand All @@ -1561,30 +1517,6 @@ angular.module('ngAnimate', ['ng'])
return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted);
},

beforeRemoveClass : function(element, className, animationCompleted) {
var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'), function(fn) {
/* when classes are removed from an element then the transition style
* that is applied is the transition defined on the element without the
* CSS class being there. This is how CSS3 functions outside of ngAnimate.
* http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */
var klass = element.attr('class');
element.removeClass(className);
var timings = fn();
element.attr('class', klass);
return timings;
});

if(cancellationMethod) {
afterReflow(element, function() {
unblockTransitions(element, className);
unblockKeyframeAnimations(element);
animationCompleted();
});
return cancellationMethod;
}
animationCompleted();
},

removeClass : function(element, className, animationCompleted) {
return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted);
}
Expand Down
Loading

0 comments on commit 037d538

Please sign in to comment.