From cae3239c48c881762d3d9ae9f63e264b13c6ef66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Mon, 8 Sep 2014 00:19:22 -0400 Subject: [PATCH 1/3] 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. --- src/ng/animate.js | 56 ++++-- src/ngAnimate/animate.js | 202 +++++++++++++++------ test/ng/animateSpec.js | 50 ++++++ test/ngAnimate/animateSpec.js | 325 +++++++++++++++++++++++++++++++++- 4 files changed, 564 insertions(+), 69 deletions(-) diff --git a/src/ng/animate.js b/src/ng/animate.js index 569910151314..4cc571d78121 100644 --- a/src/ng/animate.js +++ b/src/ng/animate.js @@ -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,17 @@ var $AnimateProvider = ['$provide', function($provide) { return currentDefer.promise; } + function applyStyles(element, options) { + if (angular.isObject(options)) { + if (options.from || options.to) { + options = extend(options.from || {}, options.to || {}); + } else { + delete options.tempClasses; + } + element.css(options); + } + } + /** * * @ngdoc service @@ -176,9 +188,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 +206,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 +229,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 +248,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 +263,8 @@ var $AnimateProvider = ['$provide', function($provide) { forEach(element, function (element) { jqLiteAddClass(element, className); }); + applyStyles(element, options); + return asyncPromise(); }, /** @@ -258,13 +277,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 +292,7 @@ var $AnimateProvider = ['$provide', function($provide) { forEach(element, function (element) { jqLiteRemoveClass(element, className); }); + applyStyles(element, options); return asyncPromise(); }, @@ -286,9 +307,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 +319,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 +345,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 +357,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(); }, diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index a754408992ec..b0650dccdcec 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -83,7 +83,7 @@ * will automatically extend the wait time to enable animations once **all** of the outbound HTTP requests * are complete. * - *

CSS-defined Animations

+ * ## 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,47 @@ * 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 CSS-based 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', { + * 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 it's 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 `before`. The destination styles are then placed inside of object called `after`. + * + * ```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 +419,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 +514,15 @@ 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 (isString(options)) { + options = options.split(/\s+/); + } + if (isArray(options)) { + options = { + tempClasses : options + }; + } + if (isObject(options)) return options; } function resolveElementClasses(element, cache, runningAnimations) { @@ -550,7 +599,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 +607,13 @@ angular.module('ngAnimate', ['ng']) return; } + if (options && !options.from && !options.to) { + options = { + from : null, + to : options + }; + } + var classNameAdd; var classNameRemove; if (isArray(className)) { @@ -645,16 +701,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 +726,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 +852,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 +896,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 +943,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 +987,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 +1021,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 +1053,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 +1064,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 +1093,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 +1188,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 +1294,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 +1367,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 +1664,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 +1696,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 +1715,9 @@ angular.module('ngAnimate', ['ng']) if (blockTransition) { blockTransitions(node, true); + if (styles) { + element.css(styles); + } } if (blockAnimation) { @@ -1654,7 +1727,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 +1735,6 @@ angular.module('ngAnimate', ['ng']) return; } - if (elementData.blockTransition) { - blockTransitions(node, false); - } - var activeClassName = ''; var pendingClassName = ''; forEach(className.split(' '), function(klass, i) { @@ -1696,6 +1765,9 @@ angular.module('ngAnimate', ['ng']) if (!staggerTime) { element.addClass(activeClassName); + if (elementData.blockTransition) { + blockTransitions(node, false); + } } var eventCacheKey = elementData.cacheKey + ' ' + activeClassName; @@ -1708,6 +1780,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 +1812,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 +1890,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 +1928,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 +1950,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 +1978,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 +1989,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 +2000,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); } }; diff --git a/test/ng/animateSpec.js b/test/ng/animateSpec.js index ff1f15cbed8f..de827533556e 100644 --- a/test/ng/animateSpec.js +++ b/test/ng/animateSpec.js @@ -103,6 +103,56 @@ describe("$animate", function() { }); inject(); }); + + it("should apply and retain inline styles on the element that is animated", inject(function($animate, $rootScope) { + var element = jqLite('
'); + var parent = jqLite('
'); + var other = jqLite('
'); + parent.append(other); + $animate.enabled(true); + + $animate.enter(element, parent, null, { color : 'red' }); + assertColor('red'); + + $animate.move(element, null, other, { color : 'yellow' }); + assertColor('yellow'); + + $animate.addClass(element, 'on', { color : 'green' }); + $rootScope.$digest(); + assertColor('green'); + + $animate.setClass(element, 'off', 'on', { color : 'black' }); + $rootScope.$digest(); + assertColor('black'); + + $animate.removeClass(element, 'off', { color : 'blue' }); + $rootScope.$digest(); + assertColor('blue'); + + $animate.leave(element, 'off', { 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('
'); + + 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() { diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index a88f4b773ae9..737c2a128833 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -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('
'))($rootScope); + var otherParent = $compile('
')($rootScope); + var child = $compile('
')($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('
')($rootScope); + + $animate.enter(element, $rootElement, null, { + 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('
1
'))($rootScope); + + $animate.addClass(element, 'on', { + 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('
'))($rootScope); + + var elements = []; + for(var i = 0; i < 4; i++) { + var newScope = $rootScope.$new(); + var element = $compile('
')(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('
'))($rootScope); + + var elements = []; + for(var i = 0; i < 4; i++) { + var newScope = $rootScope.$new(); + var element = $compile('
')(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('
1
'))($rootScope); + + $animate.addClass(element, 'on', { + 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; }; } From 2f1649e86f2ac9bb3d0e29760da660fcceec923e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Mon, 13 Oct 2014 14:01:39 -0400 Subject: [PATCH 2/3] docs complete --- src/ng/animate.js | 8 ++---- src/ng/directive/ngShowHide.js | 8 ++++-- src/ngAnimate/animate.js | 41 +++++++++++++---------------- test/ng/animateSpec.js | 24 ++++++++++++----- test/ng/directive/ngShowHideSpec.js | 8 +++--- test/ngAnimate/animateSpec.js | 30 ++++++++++++++------- 6 files changed, 70 insertions(+), 49 deletions(-) diff --git a/src/ng/animate.js b/src/ng/animate.js index 4cc571d78121..2d3ad57d159a 100644 --- a/src/ng/animate.js +++ b/src/ng/animate.js @@ -147,12 +147,8 @@ var $AnimateProvider = ['$provide', function($provide) { function applyStyles(element, options) { if (angular.isObject(options)) { - if (options.from || options.to) { - options = extend(options.from || {}, options.to || {}); - } else { - delete options.tempClasses; - } - element.css(options); + var styles = extend(options.from || {}, options.to || {}); + element.css(styles); } } diff --git a/src/ng/directive/ngShowHide.js b/src/ng/directive/ngShowHide.js index ebcc05b14111..414b986a9ae6 100644 --- a/src/ng/directive/ngShowHide.js +++ b/src/ng/directive/ngShowHide.js @@ -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 + }); }); } }; diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index b0650dccdcec..46697907634e 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -323,24 +323,26 @@ * * ### 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 CSS-based 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`. + * 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', { - * left : e.client.x + 'px', - * top : e.client.y + 'px' + * 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 it's running animation. If we want + * 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 `before`. The destination styles are then placed inside of object called `after`. + * called `from` in the same object as the `to` animations. * * ```js * canvas.on('click', function(e) { @@ -514,15 +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 (isString(options)) { - options = options.split(/\s+/); - } - if (isArray(options)) { - options = { - tempClasses : options - }; + if (isObject(options)) { + if (options.tempClasses && isString(options.tempClasses)) { + options.tempClasses = options.tempClasses.split(/\s+/); + } + return options; } - if (isObject(options)) return options; } function resolveElementClasses(element, cache, runningAnimations) { @@ -607,11 +606,9 @@ angular.module('ngAnimate', ['ng']) return; } - if (options && !options.from && !options.to) { - options = { - from : null, - to : options - }; + if (options) { + options.to = options.to || {}; + options.from = options.from || {}; } var classNameAdd; @@ -728,7 +725,7 @@ angular.module('ngAnimate', ['ng']) isSetClassOperation : isSetClassOperation, applyStyles : function() { if (options) { - element.css(angular.extend(options.from || {}, options.to)); + element.css(angular.extend(options.from || {}, options.to || {})); } }, before : function(allCompleteFn) { diff --git a/test/ng/animateSpec.js b/test/ng/animateSpec.js index de827533556e..1a89c615087a 100644 --- a/test/ng/animateSpec.js +++ b/test/ng/animateSpec.js @@ -111,25 +111,37 @@ describe("$animate", function() { parent.append(other); $animate.enabled(true); - $animate.enter(element, parent, null, { color : 'red' }); + $animate.enter(element, parent, null, { + to: { color : 'red' } + }); assertColor('red'); - $animate.move(element, null, other, { color : 'yellow' }); + $animate.move(element, null, other, { + to: { color : 'yellow' } + }); assertColor('yellow'); - $animate.addClass(element, 'on', { color : 'green' }); + $animate.addClass(element, 'on', { + to: { color : 'green' } + }); $rootScope.$digest(); assertColor('green'); - $animate.setClass(element, 'off', 'on', { color : 'black' }); + $animate.setClass(element, 'off', 'on', { + to: { color : 'black' } + }); $rootScope.$digest(); assertColor('black'); - $animate.removeClass(element, 'off', { color : 'blue' }); + $animate.removeClass(element, 'off', { + to: { color : 'blue' } + }); $rootScope.$digest(); assertColor('blue'); - $animate.leave(element, 'off', { color : 'blue' }); + $animate.leave(element, 'off', { + to: { color : 'blue' } + }); assertColor('blue'); //nothing should happen the element is gone anyway function assertColor(color) { diff --git a/test/ng/directive/ngShowHideSpec.js b/test/ng/directive/ngShowHideSpec.js index 5140df4fbef1..260ba914e44b 100644 --- a/test/ng/directive/ngShowHideSpec.js +++ b/test/ng/directive/ngShowHideSpec.js @@ -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'); })); }); }); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 737c2a128833..43a90161408a 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -1143,7 +1143,7 @@ describe("ngAnimate", function() { element = $compile('
')($rootScope); $animate.enter(element, $rootElement, null, { - borderColor: 'red' + to : {borderColor: 'red'} }); $rootScope.$digest(); @@ -1343,7 +1343,7 @@ describe("ngAnimate", function() { element = $compile(html('
1
'))($rootScope); $animate.addClass(element, 'on', { - borderColor: 'blue' + to: {borderColor: 'blue'} }); $rootScope.$digest(); @@ -2346,7 +2346,7 @@ describe("ngAnimate", function() { element = $compile(html('
1
'))($rootScope); $animate.addClass(element, 'on', { - color: 'red' + to: {color: 'red'} }); $rootScope.$digest(); @@ -2808,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) { From 09cc1dcf219d7cc2d83e29ddbb0f6ef225fbfa9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Fri, 10 Oct 2014 21:46:42 -0400 Subject: [PATCH 3/3] feat($animate): introduce the $animate.animate() method --- src/ng/animate.js | 4 ++ src/ngAnimate/animate.js | 81 +++++++++++++++++++++++++++++++++-- src/ngMock/angular-mocks.js | 2 +- test/ng/animateSpec.js | 9 ++++ test/ngAnimate/animateSpec.js | 26 ++++++++++- 5 files changed, 116 insertions(+), 6 deletions(-) diff --git a/src/ng/animate.js b/src/ng/animate.js index 2d3ad57d159a..609ebf946333 100644 --- a/src/ng/animate.js +++ b/src/ng/animate.js @@ -170,6 +170,10 @@ var $AnimateProvider = ['$provide', function($provide) { * page}. */ return { + animate : function(element, from, to) { + applyStyles(element, { from: from, to: to }); + return asyncPromise(); + }, /** * diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 46697907634e..cecaf6fd1bca 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -628,9 +628,10 @@ angular.module('ngAnimate', ['ng']) } var isSetClassOperation = animationEvent == 'setClass'; - var isClassBased = isSetClassOperation || - animationEvent == 'addClass' || - animationEvent == 'removeClass'; + var isClassBased = isSetClassOperation + || animationEvent == 'addClass' + || animationEvent == 'removeClass' + || animationEvent == 'animate'; var currentClassName = element.attr('class'); var classes = currentClassName + ' ' + className; @@ -700,6 +701,9 @@ angular.module('ngAnimate', ['ng']) case 'setClass': cancellations.push(animation.fn(element, classNameAdd, classNameRemove, progress, options)); break; + case 'animate': + cancellations.push(animation.fn(element, className, options.from, options.to, progress)); + break; case 'addClass': cancellations.push(animation.fn(element, classNameAdd || className, progress, options)); break; @@ -819,6 +823,65 @@ angular.module('ngAnimate', ['ng']) * */ return { + /** + * @ngdoc method + * @name $animate#animate + * @kind function + * + * @description + * Performs an inline animation on the element which applies the provided `to` and `from` CSS styles to the element. + * If any detected CSS transition, keyframe or JavaScript matches the provided `className` value then the animation + * will take on the provided styles. For example, if a transition animation is set for the given className then the + * provided `from` and `to` styles will be applied alongside the given transition. If a JavaScript animation is + * detected then the provided styles will be given in as function paramters. + * + * ```js + * ngModule.animation('.my-inline-animation', function() { + * return { + * animate : function(element, className, from, to, done) { + * //styles + * } + * } + * }); + * ``` + * + * Below is a breakdown of each step that occurs during the `animate` animation: + * + * | Animation Step | What the element class attribute looks like | + * |-------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------| + * | 1. $animate.animate(...) is called | class="my-animation" | + * | 2. $animate waits for the next digest to start the animation | class="my-animation ng-animate" | + * | 3. $animate runs the JavaScript-defined animations detected on the element | class="my-animation ng-animate" | + * | 4. the className class value is added to the element | class="my-animation ng-animate className" | + * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate className" | + * | 6. $animate blocks all CSS transitions on the element to ensure the .className class styling is applied right away| class="my-animation ng-animate className" | + * | 7. $animate applies the provided collection of `from` CSS styles to the element | class="my-animation ng-animate className" | + * | 8. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate className" | + * | 9. $animate removes the CSS transition block placed on the element | class="my-animation ng-animate className" | + * | 10. the className-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-animate className className-active" | + * | 11. $animate applies the collection of `to` CSS styles to the element which are then handled by the transition | class="my-animation ng-animate className className-active" | + * | 12. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate className className-active" | + * | 13. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | + * | 14. The returned promise is resolved. | class="my-animation" | + * + * @param {DOMElement} element the element that will be the focus of the enter animation + * @param {object} from a collection of CSS styles that will be applied to the element at the start of the animation + * @param {object} to a collection of CSS styles that the element will animate towards + * @param {string=} className an optional CSS class that will be added to the element for the duration of the animation (the default class is `ng-inline-animate`) + * @param {object=} options an optional collection of options that will be picked up by the CSS transition/animation + * @return {Promise} the animation callback promise + */ + animate : function(element, from, to, className, options) { + className = className || 'ng-inline-animate'; + options = parseAnimateOptions(options) || {}; + options.from = to ? from : null; + options.to = to ? to : from; + + return runAnimationPostDigest(function(done) { + return performAnimation('animate', className, stripCommentsFromElement(element), null, null, noop, options, done); + }); + }, + /** * @ngdoc method * @name $animate#enter @@ -1256,7 +1319,10 @@ angular.module('ngAnimate', ['ng']) } } - if (runner.isClassBased && !runner.isSetClassOperation && !skipAnimation) { + if (runner.isClassBased + && !runner.isSetClassOperation + && animationEvent != 'animate' + && !skipAnimation) { skipAnimation = (animationEvent == 'addClass') == element.hasClass(className); //opposite of XOR } @@ -1947,6 +2013,13 @@ angular.module('ngAnimate', ['ng']) } return { + animate : function(element, className, from, to, animationCompleted, options) { + options = options || {}; + options.from = from; + options.to = to; + return animate('animate', element, className, animationCompleted, options); + }, + enter : function(element, animationCompleted, options) { options = options || {}; return animate('enter', element, 'ng-enter', animationCompleted, options); diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 9690b46391c0..773d20d1d442 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -803,7 +803,7 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) }; angular.forEach( - ['enter','leave','move','addClass','removeClass','setClass'], function(method) { + ['animate','enter','leave','move','addClass','removeClass','setClass'], function(method) { animate[method] = function() { animate.queue.push({ event : method, diff --git a/test/ng/animateSpec.js b/test/ng/animateSpec.js index 1a89c615087a..f462b6c86b79 100644 --- a/test/ng/animateSpec.js +++ b/test/ng/animateSpec.js @@ -50,6 +50,15 @@ describe("$animate", function() { expect(element.text()).toBe('21'); })); + it("should apply styles instantly to the element", + inject(function($animate, $compile, $rootScope) { + + $animate.animate(element, { color: 'rgb(0, 0, 0)' }); + expect(element.css('color')).toBe('rgb(0, 0, 0)'); + + $animate.animate(element, { color: 'rgb(255, 0, 0)' }, { color: 'rgb(0, 255, 0)' }); + expect(element.css('color')).toBe('rgb(0, 255, 0)'); + })); it("should still perform DOM operations even if animations are disabled (post-digest)", inject(function($animate, $rootScope) { $animate.enabled(false); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 43a90161408a..df5b48f1bf13 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -329,7 +329,7 @@ describe("ngAnimate", function() { return function($animate, $compile, $rootScope, $rootElement) { element = $compile('
')($rootScope); - forEach(['.ng-hide-add', '.ng-hide-remove', '.ng-enter', '.ng-leave', '.ng-move'], function(selector) { + forEach(['.ng-hide-add', '.ng-hide-remove', '.ng-enter', '.ng-leave', '.ng-move', '.my-inline-animation'], function(selector) { ss.addRule(selector, '-webkit-transition:1s linear all;' + 'transition:1s linear all;'); }); @@ -454,6 +454,20 @@ describe("ngAnimate", function() { expect(element.text()).toBe('21'); })); + it("should perform the animate event", + inject(function($animate, $compile, $rootScope, $timeout, $sniffer) { + + $rootScope.$digest(); + $animate.animate(element, { color: 'rgb(255, 0, 0)' }, { color: 'rgb(0, 0, 255)' }, 'animated'); + $rootScope.$digest(); + + if($sniffer.transitions) { + expect(element.css('color')).toBe('rgb(255, 0, 0)'); + $animate.triggerReflow(); + } + expect(element.css('color')).toBe('rgb(0, 0, 255)'); + })); + it("should animate the show animation event", inject(function($animate, $rootScope, $sniffer) { @@ -653,6 +667,16 @@ describe("ngAnimate", function() { expect(child.attr('class')).toContain('ng-hide-remove-active'); browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 }); + //animate + $animate.animate(child, null, null, 'my-inline-animation'); + $rootScope.$digest(); + $animate.triggerReflow(); + + expect(child.attr('class')).toContain('my-inline-animation'); + expect(child.attr('class')).toContain('my-inline-animation-active'); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 }); + $animate.triggerCallbackPromise(); + //leave $animate.leave(child); $rootScope.$digest();