feat($animate): allow $animate to pass custom styles into animations

$animate now supports an optional parameter which provides CSS styling
which will be provided into the CSS-based animations as well as any
custom animation functions. Once the animation is complete then the
styles will be applied directly to the element. If no animation is
detected or the `ngAnimate` module is not active then the styles
will be applied immediately.

BREAKING CHANGE: staggering animations that use transitions will now
always block the transition from starting (via `transition: 0s none`)
up until the stagger step kicks in. The former behaviour was that the
block was removed as soon as the pending class was added. This fix
allows for styles to be applied in the pending class without causing
an animation to trigger prematurely.
This commit is contained in:
Matias Niemelä
2014-09-08 00:19:22 -04:00
committed by Igor Minar
parent 63ef085b9a
commit e5f4d7b10a
6 changed files with 597 additions and 81 deletions

View File

@@ -122,7 +122,8 @@ var $AnimateProvider = ['$provide', function($provide) {
}
});
return (toAdd.length + toRemove.length) > 0 && [toAdd.length && toAdd, toRemove.length && toRemove];
return (toAdd.length + toRemove.length) > 0 &&
[toAdd.length ? toAdd : null, toRemove.length ? toRemove : null];
}
function cachedClassManipulation(cache, classes, op) {
@@ -144,6 +145,13 @@ var $AnimateProvider = ['$provide', function($provide) {
return currentDefer.promise;
}
function applyStyles(element, options) {
if (angular.isObject(options)) {
var styles = extend(options.from || {}, options.to || {});
element.css(styles);
}
}
/**
*
* @ngdoc service
@@ -176,9 +184,11 @@ var $AnimateProvider = ['$provide', function($provide) {
* a child (if the after element is not present)
* @param {DOMElement} after the sibling element which will append the element
* after itself
* @param {object=} options an optional collection of styles that will be applied to the element.
* @return {Promise} the animation callback promise
*/
enter : function(element, parent, after) {
enter : function(element, parent, after, options) {
applyStyles(element, options);
after ? after.after(element)
: parent.prepend(element);
return asyncPromise();
@@ -192,9 +202,10 @@ var $AnimateProvider = ['$provide', function($provide) {
* @description Removes the element from the DOM. When the function is called a promise
* is returned that will be resolved at a later time.
* @param {DOMElement} element the element which will be removed from the DOM
* @param {object=} options an optional collection of options that will be applied to the element.
* @return {Promise} the animation callback promise
*/
leave : function(element) {
leave : function(element, options) {
element.remove();
return asyncPromise();
},
@@ -214,12 +225,13 @@ var $AnimateProvider = ['$provide', function($provide) {
* inserted into (if the after element is not present)
* @param {DOMElement} after the sibling element where the element will be
* positioned next to
* @param {object=} options an optional collection of options that will be applied to the element.
* @return {Promise} the animation callback promise
*/
move : function(element, parent, after) {
move : function(element, parent, after, options) {
// Do not remove element before insert. Removing will cause data associated with the
// element to be dropped. Insert will implicitly do the remove.
return this.enter(element, parent, after);
return this.enter(element, parent, after, options);
},
/**
@@ -232,13 +244,14 @@ var $AnimateProvider = ['$provide', function($provide) {
* @param {DOMElement} element the element which will have the className value
* added to it
* @param {string} className the CSS class which will be added to the element
* @param {object=} options an optional collection of options that will be applied to the element.
* @return {Promise} the animation callback promise
*/
addClass : function(element, className) {
return this.setClass(element, className, []);
addClass : function(element, className, options) {
return this.setClass(element, className, [], options);
},
$$addClassImmediately : function(element, className) {
$$addClassImmediately : function(element, className, options) {
element = jqLite(element);
className = !isString(className)
? (isArray(className) ? className.join(' ') : '')
@@ -246,6 +259,8 @@ var $AnimateProvider = ['$provide', function($provide) {
forEach(element, function (element) {
jqLiteAddClass(element, className);
});
applyStyles(element, options);
return asyncPromise();
},
/**
@@ -258,13 +273,14 @@ var $AnimateProvider = ['$provide', function($provide) {
* @param {DOMElement} element the element which will have the className value
* removed from it
* @param {string} className the CSS class which will be removed from the element
* @param {object=} options an optional collection of options that will be applied to the element.
* @return {Promise} the animation callback promise
*/
removeClass : function(element, className) {
return this.setClass(element, [], className);
removeClass : function(element, className, options) {
return this.setClass(element, [], className, options);
},
$$removeClassImmediately : function(element, className) {
$$removeClassImmediately : function(element, className, options) {
element = jqLite(element);
className = !isString(className)
? (isArray(className) ? className.join(' ') : '')
@@ -272,6 +288,7 @@ var $AnimateProvider = ['$provide', function($provide) {
forEach(element, function (element) {
jqLiteRemoveClass(element, className);
});
applyStyles(element, options);
return asyncPromise();
},
@@ -286,9 +303,10 @@ var $AnimateProvider = ['$provide', function($provide) {
* removed from it
* @param {string} add the CSS classes which will be added to the element
* @param {string} remove the CSS class which will be removed from the element
* @param {object=} options an optional collection of options that will be applied to the element.
* @return {Promise} the animation callback promise
*/
setClass : function(element, add, remove) {
setClass : function(element, add, remove, options) {
var self = this;
var STORAGE_KEY = '$$animateClasses';
var createdCache = false;
@@ -297,9 +315,12 @@ var $AnimateProvider = ['$provide', function($provide) {
var cache = element.data(STORAGE_KEY);
if (!cache) {
cache = {
classes: {}
classes: {},
options : options
};
createdCache = true;
} else if (options && cache.options) {
cache.options = angular.extend(cache.options || {}, options);
}
var classes = cache.classes;
@@ -320,7 +341,7 @@ var $AnimateProvider = ['$provide', function($provide) {
if (cache) {
var classes = resolveElementClasses(element, cache.classes);
if (classes) {
self.$$setClassImmediately(element, classes[0], classes[1]);
self.$$setClassImmediately(element, classes[0], classes[1], cache.options);
}
}
@@ -332,9 +353,10 @@ var $AnimateProvider = ['$provide', function($provide) {
return cache.promise;
},
$$setClassImmediately : function(element, add, remove) {
$$setClassImmediately : function(element, add, remove, options) {
add && this.$$addClassImmediately(element, add);
remove && this.$$removeClassImmediately(element, remove);
applyStyles(element, options);
return asyncPromise();
},

View File

@@ -167,7 +167,9 @@ var ngShowDirective = ['$animate', function($animate) {
// we can control when the element is actually displayed on screen without having
// to have a global/greedy CSS selector that breaks when other animations are run.
// Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845
$animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, NG_HIDE_IN_PROGRESS_CLASS);
$animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, {
tempClasses : NG_HIDE_IN_PROGRESS_CLASS
});
});
}
};
@@ -324,7 +326,9 @@ var ngHideDirective = ['$animate', function($animate) {
scope.$watch(attr.ngHide, function ngHideWatchAction(value){
// The comment inside of the ngShowDirective explains why we add and
// remove a temporary class for the show/hide animation
$animate[value ? 'addClass' : 'removeClass'](element,NG_HIDE_CLASS, NG_HIDE_IN_PROGRESS_CLASS);
$animate[value ? 'addClass' : 'removeClass'](element,NG_HIDE_CLASS, {
tempClasses : NG_HIDE_IN_PROGRESS_CLASS
});
});
}
};

View File

@@ -83,7 +83,7 @@
* will automatically extend the wait time to enable animations once **all** of the outbound HTTP requests
* are complete.
*
* <h2>CSS-defined Animations</h2>
* ## CSS-defined Animations
* The animate service will automatically apply two CSS classes to the animated element and these two CSS classes
* are designed to contain the start and end CSS styling. Both CSS transitions and keyframe animations are supported
* and can be used to play along with this naming structure.
@@ -320,6 +320,49 @@
* and the JavaScript animation is found, then the enter callback will handle that animation (in addition to the CSS keyframe animation
* or transition code that is defined via a stylesheet).
*
*
* ### Applying Directive-specific Styles to an Animation
* In some cases a directive or service may want to provide `$animate` with extra details that the animation will
* include into its animation. Let's say for example we wanted to render an animation that animates an element
* towards the mouse coordinates as to where the user clicked last. By collecting the X/Y coordinates of the click
* (via the event parameter) we can set the `top` and `left` styles into an object and pass that into our function
* call to `$animate.addClass`.
*
* ```js
* canvas.on('click', function(e) {
* $animate.addClass(element, 'on', {
* to: {
* left : e.client.x + 'px',
* top : e.client.y + 'px'
* }
* }):
* });
* ```
*
* Now when the animation runs, and a transition or keyframe animation is picked up, then the animation itself will
* also include and transition the styling of the `left` and `top` properties into its running animation. If we want
* to provide some starting animation values then we can do so by placing the starting animations styles into an object
* called `from` in the same object as the `to` animations.
*
* ```js
* canvas.on('click', function(e) {
* $animate.addClass(element, 'on', {
* from: {
* position: 'absolute',
* left: '0px',
* top: '0px'
* },
* to: {
* left : e.client.x + 'px',
* top : e.client.y + 'px'
* }
* }):
* });
* ```
*
* Once the animation is complete or cancelled then the union of both the before and after styles are applied to the
* element. If `ngAnimate` is not present then the styles will be applied immediately.
*
*/
angular.module('ngAnimate', ['ng'])
@@ -378,6 +421,7 @@ angular.module('ngAnimate', ['ng'])
var selectors = $animateProvider.$$selectors;
var isArray = angular.isArray;
var isString = angular.isString;
var isObject = angular.isObject;
var ELEMENT_NODE = 1;
var NG_ANIMATE_STATE = '$$ngAnimateState';
@@ -472,8 +516,12 @@ angular.module('ngAnimate', ['ng'])
// some plugin code may still be passing in the callback
// function as the last param for the $animate methods so
// it's best to only allow string or array values for now
if (isArray(options)) return options;
if (isString(options)) return [options];
if (isObject(options)) {
if (options.tempClasses && isString(options.tempClasses)) {
options.tempClasses = options.tempClasses.split(/\s+/);
}
return options;
}
}
function resolveElementClasses(element, cache, runningAnimations) {
@@ -550,7 +598,7 @@ angular.module('ngAnimate', ['ng'])
}
}
function animationRunner(element, animationEvent, className) {
function animationRunner(element, animationEvent, className, options) {
//transcluded directives may sometimes fire an animation using only comment nodes
//best to catch this early on to prevent any animation operations from occurring
var node = element[0];
@@ -558,6 +606,11 @@ angular.module('ngAnimate', ['ng'])
return;
}
if (options) {
options.to = options.to || {};
options.from = options.from || {};
}
var classNameAdd;
var classNameRemove;
if (isArray(className)) {
@@ -645,16 +698,16 @@ angular.module('ngAnimate', ['ng'])
};
switch(animation.event) {
case 'setClass':
cancellations.push(animation.fn(element, classNameAdd, classNameRemove, progress));
cancellations.push(animation.fn(element, classNameAdd, classNameRemove, progress, options));
break;
case 'addClass':
cancellations.push(animation.fn(element, classNameAdd || className, progress));
cancellations.push(animation.fn(element, classNameAdd || className, progress, options));
break;
case 'removeClass':
cancellations.push(animation.fn(element, classNameRemove || className, progress));
cancellations.push(animation.fn(element, classNameRemove || className, progress, options));
break;
default:
cancellations.push(animation.fn(element, progress));
cancellations.push(animation.fn(element, progress, options));
break;
}
});
@@ -670,6 +723,11 @@ angular.module('ngAnimate', ['ng'])
className : className,
isClassBased : isClassBased,
isSetClassOperation : isSetClassOperation,
applyStyles : function() {
if (options) {
element.css(angular.extend(options.from || {}, options.to || {}));
}
},
before : function(allCompleteFn) {
beforeComplete = allCompleteFn;
run(before, beforeCancel, function() {
@@ -791,6 +849,7 @@ angular.module('ngAnimate', ['ng'])
* @param {DOMElement} element the element that will be the focus of the enter animation
* @param {DOMElement} parentElement the parent element of the element that will be the focus of the enter animation
* @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the enter animation
* @param {object=} options an optional collection of options that will be picked up by the CSS transition/animation
* @return {Promise} the animation callback promise
*/
enter : function(element, parentElement, afterElement, options) {
@@ -834,6 +893,7 @@ angular.module('ngAnimate', ['ng'])
* | 13. The returned promise is resolved. | ... |
*
* @param {DOMElement} element the element that will be the focus of the leave animation
* @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
* @return {Promise} the animation callback promise
*/
leave : function(element, options) {
@@ -880,6 +940,7 @@ angular.module('ngAnimate', ['ng'])
* @param {DOMElement} element the element that will be the focus of the move animation
* @param {DOMElement} parentElement the parentElement element of the element that will be the focus of the move animation
* @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the move animation
* @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
* @return {Promise} the animation callback promise
*/
move : function(element, parentElement, afterElement, options) {
@@ -923,6 +984,7 @@ angular.module('ngAnimate', ['ng'])
*
* @param {DOMElement} element the element that will be animated
* @param {string} className the CSS class that will be added to the element and then animated
* @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
* @return {Promise} the animation callback promise
*/
addClass : function(element, className, options) {
@@ -956,6 +1018,7 @@ angular.module('ngAnimate', ['ng'])
*
* @param {DOMElement} element the element that will be animated
* @param {string} className the CSS class that will be animated and then removed from the element
* @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
* @return {Promise} the animation callback promise
*/
removeClass : function(element, className, options) {
@@ -987,6 +1050,7 @@ angular.module('ngAnimate', ['ng'])
* @param {string} add the CSS classes which will be added to the element
* @param {string} remove the CSS class which will be removed from the element
* CSS classes have been set on the element
* @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
* @return {Promise} the animation callback promise
*/
setClass : function(element, add, remove, options) {
@@ -997,7 +1061,7 @@ angular.module('ngAnimate', ['ng'])
element = stripCommentsFromElement(element);
if (classBasedAnimationsBlocked(element)) {
return $delegate.$$setClassImmediately(element, add, remove);
return $delegate.$$setClassImmediately(element, add, remove, options);
}
// we're using a combined array for both the add and remove
@@ -1026,7 +1090,7 @@ angular.module('ngAnimate', ['ng'])
if (hasCache) {
if (options && cache.options) {
cache.options = cache.options.concat(options);
cache.options = angular.extend(cache.options || {}, options);
}
//the digest cycle will combine all the animations into one function
@@ -1121,9 +1185,8 @@ angular.module('ngAnimate', ['ng'])
and the onComplete callback will be fired once the animation is fully complete.
*/
function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, options, doneCallback) {
var noopCancel = noop;
var runner = animationRunner(element, animationEvent, className);
var runner = animationRunner(element, animationEvent, className, options);
if (!runner) {
fireDOMOperation();
fireBeforeCallbackAsync();
@@ -1228,8 +1291,8 @@ angular.module('ngAnimate', ['ng'])
//the ng-animate class does nothing, but it's here to allow for
//parent animations to find and cancel child animations when needed
element.addClass(NG_ANIMATE_CLASS_NAME);
if (isArray(options)) {
forEach(options, function(className) {
if (options && options.tempClasses) {
forEach(options.tempClasses, function(className) {
element.addClass(className);
});
}
@@ -1301,9 +1364,13 @@ angular.module('ngAnimate', ['ng'])
function closeAnimation() {
if (!closeAnimation.hasBeenRun) {
if (runner) { //the runner doesn't exist if it fails to instantiate
runner.applyStyles();
}
closeAnimation.hasBeenRun = true;
if (isArray(options)) {
forEach(options, function(className) {
if (options && options.tempClasses) {
forEach(options.tempClasses, function(className) {
element.removeClass(className);
});
}
@@ -1594,7 +1661,7 @@ angular.module('ngAnimate', ['ng'])
return parentID + '-' + extractElementNode(element).getAttribute('class');
}
function animateSetup(animationEvent, element, className) {
function animateSetup(animationEvent, element, className, styles) {
var structural = ['ng-enter','ng-leave','ng-move'].indexOf(className) >= 0;
var cacheKey = getCacheKey(element);
@@ -1626,7 +1693,7 @@ angular.module('ngAnimate', ['ng'])
return false;
}
var blockTransition = structural && transitionDuration > 0;
var blockTransition = styles || (structural && transitionDuration > 0);
var blockAnimation = animationDuration > 0 &&
stagger.animationDelay > 0 &&
stagger.animationDuration === 0;
@@ -1645,6 +1712,9 @@ angular.module('ngAnimate', ['ng'])
if (blockTransition) {
blockTransitions(node, true);
if (styles) {
element.css(styles);
}
}
if (blockAnimation) {
@@ -1654,7 +1724,7 @@ angular.module('ngAnimate', ['ng'])
return true;
}
function animateRun(animationEvent, element, className, activeAnimationComplete) {
function animateRun(animationEvent, element, className, activeAnimationComplete, styles) {
var node = extractElementNode(element);
var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY);
if (node.getAttribute('class').indexOf(className) == -1 || !elementData) {
@@ -1662,10 +1732,6 @@ angular.module('ngAnimate', ['ng'])
return;
}
if (elementData.blockTransition) {
blockTransitions(node, false);
}
var activeClassName = '';
var pendingClassName = '';
forEach(className.split(' '), function(klass, i) {
@@ -1696,6 +1762,9 @@ angular.module('ngAnimate', ['ng'])
if (!staggerTime) {
element.addClass(activeClassName);
if (elementData.blockTransition) {
blockTransitions(node, false);
}
}
var eventCacheKey = elementData.cacheKey + ' ' + activeClassName;
@@ -1708,6 +1777,14 @@ angular.module('ngAnimate', ['ng'])
return;
}
if (!staggerTime && styles) {
if (!timings.transitionDuration) {
element.css('transition', timings.animationDuration + 's linear all');
appliedStyles.push('transition');
}
element.css(styles);
}
var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay);
var maxDelayTime = maxDelay * ONE_SECOND;
@@ -1732,11 +1809,24 @@ angular.module('ngAnimate', ['ng'])
element.addClass(pendingClassName);
staggerTimeout = $timeout(function() {
staggerTimeout = null;
element.addClass(activeClassName);
element.removeClass(pendingClassName);
if (timings.transitionDuration > 0) {
blockTransitions(node, false);
}
if (timings.animationDuration > 0) {
blockAnimations(node, false);
}
element.addClass(activeClassName);
element.removeClass(pendingClassName);
if (styles) {
if (timings.transitionDuration === 0) {
element.css('transition', timings.animationDuration + 's linear all');
}
element.css(styles);
appliedStyles.push('transition');
}
}, staggerTime * ONE_SECOND, false);
}
@@ -1797,28 +1887,28 @@ angular.module('ngAnimate', ['ng'])
node.style[ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY] = bool ? 'paused' : '';
}
function animateBefore(animationEvent, element, className, calculationDecorator) {
if (animateSetup(animationEvent, element, className, calculationDecorator)) {
function animateBefore(animationEvent, element, className, styles) {
if (animateSetup(animationEvent, element, className, styles)) {
return function(cancelled) {
cancelled && animateClose(element, className);
};
}
}
function animateAfter(animationEvent, element, className, afterAnimationComplete) {
function animateAfter(animationEvent, element, className, afterAnimationComplete, styles) {
if (element.data(NG_ANIMATE_CSS_DATA_KEY)) {
return animateRun(animationEvent, element, className, afterAnimationComplete);
return animateRun(animationEvent, element, className, afterAnimationComplete, styles);
} else {
animateClose(element, className);
afterAnimationComplete();
}
}
function animate(animationEvent, element, className, animationComplete) {
function animate(animationEvent, element, className, animationComplete, options) {
//If the animateSetup function doesn't bother returning a
//cancellation function then it means that there is no animation
//to perform at all
var preReflowCancellation = animateBefore(animationEvent, element, className);
var preReflowCancellation = animateBefore(animationEvent, element, className, options.from);
if (!preReflowCancellation) {
clearCacheAfterReflow();
animationComplete();
@@ -1835,7 +1925,7 @@ angular.module('ngAnimate', ['ng'])
//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
cancel = animateAfter(animationEvent, element, className, animationComplete);
cancel = animateAfter(animationEvent, element, className, animationComplete, options.to);
});
return function(cancelled) {
@@ -1857,22 +1947,26 @@ angular.module('ngAnimate', ['ng'])
}
return {
enter : function(element, animationCompleted) {
return animate('enter', element, 'ng-enter', animationCompleted);
enter : function(element, animationCompleted, options) {
options = options || {};
return animate('enter', element, 'ng-enter', animationCompleted, options);
},
leave : function(element, animationCompleted) {
return animate('leave', element, 'ng-leave', animationCompleted);
leave : function(element, animationCompleted, options) {
options = options || {};
return animate('leave', element, 'ng-leave', animationCompleted, options);
},
move : function(element, animationCompleted) {
return animate('move', element, 'ng-move', animationCompleted);
move : function(element, animationCompleted, options) {
options = options || {};
return animate('move', element, 'ng-move', animationCompleted, options);
},
beforeSetClass : function(element, add, remove, animationCompleted) {
beforeSetClass : function(element, add, remove, animationCompleted, options) {
options = options || {};
var className = suffixClasses(remove, '-remove') + ' ' +
suffixClasses(add, '-add');
var cancellationMethod = animateBefore('setClass', element, className);
var cancellationMethod = animateBefore('setClass', element, className, options.from);
if (cancellationMethod) {
afterReflow(element, animationCompleted);
return cancellationMethod;
@@ -1881,8 +1975,9 @@ angular.module('ngAnimate', ['ng'])
animationCompleted();
},
beforeAddClass : function(element, className, animationCompleted) {
var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'));
beforeAddClass : function(element, className, animationCompleted, options) {
options = options || {};
var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'), options.from);
if (cancellationMethod) {
afterReflow(element, animationCompleted);
return cancellationMethod;
@@ -1891,8 +1986,9 @@ angular.module('ngAnimate', ['ng'])
animationCompleted();
},
beforeRemoveClass : function(element, className, animationCompleted) {
var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'));
beforeRemoveClass : function(element, className, animationCompleted, options) {
options = options || {};
var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'), options.from);
if (cancellationMethod) {
afterReflow(element, animationCompleted);
return cancellationMethod;
@@ -1901,19 +1997,22 @@ angular.module('ngAnimate', ['ng'])
animationCompleted();
},
setClass : function(element, add, remove, animationCompleted) {
setClass : function(element, add, remove, animationCompleted, options) {
options = options || {};
remove = suffixClasses(remove, '-remove');
add = suffixClasses(add, '-add');
var className = remove + ' ' + add;
return animateAfter('setClass', element, className, animationCompleted);
return animateAfter('setClass', element, className, animationCompleted, options.to);
},
addClass : function(element, className, animationCompleted) {
return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted);
addClass : function(element, className, animationCompleted, options) {
options = options || {};
return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted, options.to);
},
removeClass : function(element, className, animationCompleted) {
return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted);
removeClass : function(element, className, animationCompleted, options) {
options = options || {};
return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted, options.to);
}
};

View File

@@ -103,6 +103,68 @@ describe("$animate", function() {
});
inject();
});
it("should apply and retain inline styles on the element that is animated", inject(function($animate, $rootScope) {
var element = jqLite('<div></div>');
var parent = jqLite('<div></div>');
var other = jqLite('<div></div>');
parent.append(other);
$animate.enabled(true);
$animate.enter(element, parent, null, {
to: { color : 'red' }
});
assertColor('red');
$animate.move(element, null, other, {
to: { color : 'yellow' }
});
assertColor('yellow');
$animate.addClass(element, 'on', {
to: { color : 'green' }
});
$rootScope.$digest();
assertColor('green');
$animate.setClass(element, 'off', 'on', {
to: { color : 'black' }
});
$rootScope.$digest();
assertColor('black');
$animate.removeClass(element, 'off', {
to: { color : 'blue' }
});
$rootScope.$digest();
assertColor('blue');
$animate.leave(element, 'off', {
to: { color : 'blue' }
});
assertColor('blue'); //nothing should happen the element is gone anyway
function assertColor(color) {
expect(element[0].style.color).toBe(color);
}
}));
it("should merge the from and to styles that are provided",
inject(function($animate, $rootScope) {
var element = jqLite('<div></div>');
element.css('color', 'red');
$animate.addClass(element, 'on', {
from : { color : 'green' },
to : { borderColor : 'purple' }
});
$rootScope.$digest();
var style = element[0].style;
expect(style.color).toBe('green');
expect(style.borderColor).toBe('purple');
}));
});
describe('CSS class DOM manipulation', function() {

View File

@@ -170,13 +170,13 @@ describe('ngShow / ngHide animations', function() {
item = $animate.queue.shift();
expect(item.event).toEqual('addClass');
expect(item.options).toEqual('ng-hide-animate');
expect(item.options.tempClasses).toEqual('ng-hide-animate');
$scope.on = true;
$scope.$digest();
item = $animate.queue.shift();
expect(item.event).toEqual('removeClass');
expect(item.options).toEqual('ng-hide-animate');
expect(item.options.tempClasses).toEqual('ng-hide-animate');
}));
});
@@ -217,13 +217,13 @@ describe('ngShow / ngHide animations', function() {
item = $animate.queue.shift();
expect(item.event).toEqual('removeClass');
expect(item.options).toEqual('ng-hide-animate');
expect(item.options.tempClasses).toEqual('ng-hide-animate');
$scope.on = true;
$scope.$digest();
item = $animate.queue.shift();
expect(item.event).toEqual('addClass');
expect(item.options).toEqual('ng-hide-animate');
expect(item.options.tempClasses).toEqual('ng-hide-animate');
}));
});
});

View File

@@ -292,7 +292,7 @@ describe("ngAnimate", function() {
});
$animateProvider.register('.custom-delay', function($timeout) {
function animate(element, done) {
done = arguments.length == 3 ? arguments[2] : done;
done = arguments.length == 4 ? arguments[2] : done;
$timeout(done, 2000, false);
return function() {
element.addClass('animation-cancelled');
@@ -306,7 +306,7 @@ describe("ngAnimate", function() {
});
$animateProvider.register('.custom-long-delay', function($timeout) {
function animate(element, done) {
done = arguments.length == 3 ? arguments[2] : done;
done = arguments.length == 4 ? arguments[2] : done;
$timeout(done, 20000, false);
return function(cancelled) {
element.addClass(cancelled ? 'animation-cancelled' : 'animation-ended');
@@ -1037,8 +1037,124 @@ describe("ngAnimate", function() {
expect(element.hasClass('custom-long-delay-add')).toBe(false);
expect(element.hasClass('custom-long-delay-add-active')).toBe(false);
}));
it('should apply directive styles and provide the style collection to the animation function', function() {
var animationDone;
var animationStyles;
var proxyAnimation = function() {
var limit = arguments.length-1;
animationStyles = arguments[limit];
animationDone = arguments[limit-1];
};
module(function($animateProvider) {
$animateProvider.register('.capture', function() {
return {
enter : proxyAnimation,
leave : proxyAnimation,
move : proxyAnimation,
addClass : proxyAnimation,
removeClass : proxyAnimation,
setClass : proxyAnimation
};
});
});
inject(function($animate, $rootScope, $compile, $sniffer, $timeout, _$rootElement_) {
$rootElement = _$rootElement_;
$animate.enabled(true);
element = $compile(html('<div></div>'))($rootScope);
var otherParent = $compile('<div></div>')($rootScope);
var child = $compile('<div class="capture" style="transition: 0s!important; -webkit-transition: 0s!important;"></div>')($rootScope);
$rootElement.append(otherParent);
$rootScope.$digest();
var styles = {
from: { backgroundColor: 'blue' },
to: { backgroundColor: 'red' }
};
//enter
$animate.enter(child, element, null, styles);
$rootScope.$digest();
$animate.triggerReflow();
expect(animationStyles).toEqual(styles);
animationDone();
animationDone = animationStyles = null;
$animate.triggerCallbacks();
//move
$animate.move(child, null, otherParent, styles);
$rootScope.$digest();
$animate.triggerReflow();
expect(animationStyles).toEqual(styles);
animationDone();
animationDone = animationStyles = null;
$animate.triggerCallbacks();
//addClass
$animate.addClass(child, 'on', styles);
$rootScope.$digest();
$animate.triggerReflow();
expect(animationStyles).toEqual(styles);
animationDone();
animationDone = animationStyles = null;
$animate.triggerCallbacks();
//setClass
$animate.setClass(child, 'off', 'on', styles);
$rootScope.$digest();
$animate.triggerReflow();
expect(animationStyles).toEqual(styles);
animationDone();
animationDone = animationStyles = null;
$animate.triggerCallbacks();
//removeClass
$animate.removeClass(child, 'off', styles);
$rootScope.$digest();
$animate.triggerReflow();
expect(animationStyles).toEqual(styles);
animationDone();
animationDone = animationStyles = null;
$animate.triggerCallbacks();
//leave
$animate.leave(child, styles);
$rootScope.$digest();
$animate.triggerReflow();
expect(animationStyles).toEqual(styles);
animationDone();
animationDone = animationStyles = null;
$animate.triggerCallbacks();
dealoc(otherParent);
});
});
});
it("should apply animated styles even if there are no detected animations",
inject(function($compile, $animate, $rootScope, $sniffer, $rootElement, $document) {
$animate.enabled(true);
jqLite($document[0].body).append($rootElement);
element = $compile('<div class="fake-animation"></div>')($rootScope);
$animate.enter(element, $rootElement, null, {
to : {borderColor: 'red'}
});
$rootScope.$digest();
expect(element).toHaveClass('ng-animate');
$animate.triggerReflow();
$animate.triggerCallbacks();
expect(element).not.toHaveClass('ng-animate');
expect(element.attr('style')).toMatch(/border-color: red/);
}));
describe("with CSS3", function() {
@@ -1218,6 +1334,28 @@ describe("ngAnimate", function() {
})
);
it("should piggy-back-transition the styles with the max keyframe duration if provided by the directive",
inject(function($compile, $animate, $rootScope, $sniffer) {
$animate.enabled(true);
ss.addRule('.on', '-webkit-animation: 1s keyframeanimation; animation: 1s keyframeanimation;');
element = $compile(html('<div>1</div>'))($rootScope);
$animate.addClass(element, 'on', {
to: {borderColor: 'blue'}
});
$rootScope.$digest();
if ($sniffer.transitions) {
$animate.triggerReflow();
expect(element.attr('style')).toContain('border-color: blue');
expect(element.attr('style')).toMatch(/transition:.*1s/);
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
}
expect(element.attr('style')).toContain('border-color: blue');
}));
it("should pause the playstate when performing a stagger animation",
inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
@@ -1372,6 +1510,86 @@ describe("ngAnimate", function() {
}
}));
it("should stagger items and apply the transition + directive styles the right time when piggy-back styles are used",
inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $window) {
if(!$sniffer.transitions) return;
$animate.enabled(true);
ss.addRule('.stagger-animation.ng-enter, .stagger-animation.ng-leave',
'-webkit-animation:my_animation 1s 1s, your_animation 1s 2s;' +
'animation:my_animation 1s 1s, your_animation 1s 2s;');
ss.addRule('.stagger-animation.ng-enter-stagger, .stagger-animation.ng-leave-stagger',
'-webkit-animation-delay:0.1s;' +
'animation-delay:0.1s;');
var styles = {
from : { left : '50px' },
to : { left : '100px' }
};
var container = $compile(html('<div></div>'))($rootScope);
var elements = [];
for(var i = 0; i < 4; i++) {
var newScope = $rootScope.$new();
var element = $compile('<div class="stagger-animation"></div>')(newScope);
$animate.enter(element, container, null, styles);
elements.push(element);
}
$rootScope.$digest();
for(i = 0; i < 4; i++) {
expect(elements[i]).toHaveClass('ng-enter');
assertTransitionDuration(elements[i], '2', true);
assertLeftStyle(elements[i], '50');
}
$animate.triggerReflow();
expect(elements[0]).toHaveClass('ng-enter-active');
assertLeftStyle(elements[0], '100');
assertTransitionDuration(elements[0], '1');
for(i = 1; i < 4; i++) {
expect(elements[i]).not.toHaveClass('ng-enter-active');
assertTransitionDuration(elements[i], '1', true);
assertLeftStyle(elements[i], '100', true);
}
$timeout.flush(300);
for(i = 1; i < 4; i++) {
expect(elements[i]).toHaveClass('ng-enter-active');
assertTransitionDuration(elements[i], '1');
assertLeftStyle(elements[i], '100');
}
$timeout.flush();
for(i = 0; i < 4; i++) {
expect(elements[i]).not.toHaveClass('ng-enter');
expect(elements[i]).not.toHaveClass('ng-enter-active');
assertTransitionDuration(elements[i], '1', true);
assertLeftStyle(elements[i], '100');
}
function assertLeftStyle(element, val, not) {
var regex = new RegExp('left: ' + val + 'px');
var style = element.attr('style');
not ? expect(style).not.toMatch(regex)
: expect(style).toMatch(regex);
}
function assertTransitionDuration(element, val, not) {
var regex = new RegExp('transition:.*' + val + 's');
var style = element.attr('style');
not ? expect(style).not.toMatch(regex)
: expect(style).toMatch(regex);
}
}));
});
@@ -1741,6 +1959,82 @@ describe("ngAnimate", function() {
}
}));
it("should stagger items, apply directive styles but not apply a transition style when the stagger step kicks in",
inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $window) {
if(!$sniffer.transitions) return;
$animate.enabled(true);
ss.addRule('.stagger-animation.ng-enter, .ani.ng-leave',
'-webkit-transition:1s linear color 2s, 3s linear font-size 4s;' +
'transition:1s linear color 2s, 3s linear font-size 4s;');
ss.addRule('.stagger-animation.ng-enter-stagger, .ani.ng-leave-stagger',
'-webkit-transition-delay:0.1s;' +
'transition-delay:0.1s;');
var styles = {
from : { left : '155px' },
to : { left : '255px' }
};
var container = $compile(html('<div></div>'))($rootScope);
var elements = [];
for(var i = 0; i < 4; i++) {
var newScope = $rootScope.$new();
var element = $compile('<div class="stagger-animation"></div>')(newScope);
$animate.enter(element, container, null, styles);
elements.push(element);
}
$rootScope.$digest();
for(i = 0; i < 4; i++) {
expect(elements[i]).toHaveClass('ng-enter');
assertLeftStyle(elements[i], '155');
}
$animate.triggerReflow();
expect(elements[0]).toHaveClass('ng-enter-active');
assertLeftStyle(elements[0], '255');
assertNoTransitionDuration(elements[0]);
for(i = 1; i < 4; i++) {
expect(elements[i]).not.toHaveClass('ng-enter-active');
assertLeftStyle(elements[i], '255', true);
}
$timeout.flush(300);
for(i = 1; i < 4; i++) {
expect(elements[i]).toHaveClass('ng-enter-active');
assertNoTransitionDuration(elements[i]);
assertLeftStyle(elements[i], '255');
}
$timeout.flush();
for(i = 0; i < 4; i++) {
expect(elements[i]).not.toHaveClass('ng-enter');
expect(elements[i]).not.toHaveClass('ng-enter-active');
assertNoTransitionDuration(elements[i]);
assertLeftStyle(elements[i], '255');
}
function assertLeftStyle(element, val, not) {
var regex = new RegExp('left: ' + val + 'px');
var style = element.attr('style');
not ? expect(style).not.toMatch(regex)
: expect(style).toMatch(regex);
}
function assertNoTransitionDuration(element) {
var style = element.attr('style');
expect(style).not.toMatch(/transition/);
}
}));
it("should apply a closing timeout to close all pending transitions",
inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
@@ -2042,6 +2336,29 @@ describe("ngAnimate", function() {
expect(elements[i].attr('style')).toBeFalsy();
}
}));
it("should create a piggy-back-transition which has a duration the same as the max keyframe duration if any directive styles are provided",
inject(function($compile, $animate, $rootScope, $sniffer) {
$animate.enabled(true);
ss.addRule('.on', '-webkit-transition: 1s linear all; transition: 1s linear all;');
element = $compile(html('<div>1</div>'))($rootScope);
$animate.addClass(element, 'on', {
to: {color: 'red'}
});
$rootScope.$digest();
if ($sniffer.transitions) {
$animate.triggerReflow();
expect(element.attr('style')).toContain('color: red');
expect(element.attr('style')).not.toContain('transition');
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
}
expect(element.attr('style')).toContain('color: red');
}));
});
@@ -2472,9 +2789,9 @@ describe("ngAnimate", function() {
};
function capture(event) {
return function(element, add, remove, done) {
return function(element, add, remove, styles, done) {
//some animations only have one extra param
done = done || remove || add;
done = arguments[arguments.length-2]; //the last one is the styles array
captures[event]=done;
};
}
@@ -2491,28 +2808,40 @@ describe("ngAnimate", function() {
$compile(element)($rootScope);
assertTempClass('enter', 'temp-enter', function() {
$animate.enter(element, container, null, 'temp-enter');
$animate.enter(element, container, null, {
tempClasses: 'temp-enter'
});
});
assertTempClass('move', 'temp-move', function() {
$animate.move(element, null, container2, 'temp-move');
$animate.move(element, null, container2, {
tempClasses: 'temp-move'
});
});
assertTempClass('addClass', 'temp-add', function() {
$animate.addClass(element, 'add', 'temp-add');
$animate.addClass(element, 'add', {
tempClasses: 'temp-add'
});
});
assertTempClass('removeClass', 'temp-remove', function() {
$animate.removeClass(element, 'add', 'temp-remove');
$animate.removeClass(element, 'add', {
tempClasses: 'temp-remove'
});
});
element.addClass('remove');
assertTempClass('setClass', 'temp-set', function() {
$animate.setClass(element, 'add', 'remove', 'temp-set');
$animate.setClass(element, 'add', 'remove', {
tempClasses: 'temp-set'
});
});
assertTempClass('leave', 'temp-leave', function() {
$animate.leave(element, 'temp-leave');
$animate.leave(element, {
tempClasses: 'temp-leave'
});
});
function assertTempClass(event, className, animationOperation) {