From 02be700bda191b454de393f2805916f374a1d764 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] 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 2d3ad57d..609ebf94 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 c1ed9044..99adde67 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 2ef2f32d..9b532cb7 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 1a89c615..f462b6c8 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 43a90161..df5b48f1 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();