diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 0417f18e0d30..4f2432200143 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -317,6 +317,10 @@ angular.module('ngAnimate', ['ng']) return classNameFilter.test(className); }; + function async(fn) { + return $timeout(fn, 0, false); + } + function lookup(name) { if (name) { var matches = [], @@ -608,6 +612,8 @@ angular.module('ngAnimate', ['ng']) //best to catch this early on to prevent any animation operations from occurring if(!node || !isAnimatableClassName(classes)) { fireDOMOperation(); + fireBeforeCallbackAsync(); + fireAfterCallbackAsync(); closeAnimation(); return; } @@ -627,6 +633,8 @@ angular.module('ngAnimate', ['ng']) //NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case a NO animation is not found. if (animationsDisabled(element, parentElement) || matches.length === 0) { fireDOMOperation(); + fireBeforeCallbackAsync(); + fireAfterCallbackAsync(); closeAnimation(); return; } @@ -665,6 +673,8 @@ angular.module('ngAnimate', ['ng']) //animation do it's thing and close this one early if(animations.length === 0) { fireDOMOperation(); + fireBeforeCallbackAsync(); + fireAfterCallbackAsync(); fireDoneCallbackAsync(); return; } @@ -718,6 +728,8 @@ angular.module('ngAnimate', ['ng']) if((animationEvent == 'addClass' && futureClassName.indexOf(classNameToken) >= 0) || (animationEvent == 'removeClass' && futureClassName.indexOf(classNameToken) == -1)) { fireDOMOperation(); + fireBeforeCallbackAsync(); + fireAfterCallbackAsync(); fireDoneCallbackAsync(); return; } @@ -758,6 +770,10 @@ angular.module('ngAnimate', ['ng']) } function invokeRegisteredAnimationFns(animations, phase, allAnimationFnsComplete) { + phase == 'after' ? + fireAfterCallbackAsync() : + fireBeforeCallbackAsync(); + var endFnName = phase + 'End'; forEach(animations, function(animation, index) { var animationPhaseCompleted = function() { @@ -794,8 +810,27 @@ angular.module('ngAnimate', ['ng']) } } + function fireDOMCallback(animationPhase) { + element.triggerHandler('$animate:' + animationPhase, { + event : animationEvent, + className : className + }); + } + + function fireBeforeCallbackAsync() { + async(function() { + fireDOMCallback('before'); + }); + } + + function fireAfterCallbackAsync() { + async(function() { + fireDOMCallback('after'); + }); + } + function fireDoneCallbackAsync() { - doneCallback && $timeout(doneCallback, 0, false); + doneCallback && async(doneCallback); } //it is less complicated to use a flag than managing and cancelling @@ -819,9 +854,9 @@ angular.module('ngAnimate', ['ng']) if(isClassBased) { cleanup(element); } else { - data.closeAnimationTimeout = $timeout(function() { + data.closeAnimationTimeout = async(function() { cleanup(element); - }, 0, false); + }); element.data(NG_ANIMATE_STATE, data); } } diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 1477bca03873..6d9367bdbab0 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -1496,6 +1496,68 @@ describe("ngAnimate", function() { expect(signature).toBe('AB'); })); + it('should fire DOM callbacks on the element being animated', + inject(function($animate, $rootScope, $compile, $sniffer, $rootElement, $timeout) { + + if(!$sniffer.transitions) return; + + $animate.enabled(true); + + ss.addRule('.klass-add', '-webkit-transition:1s linear all;' + + 'transition:1s linear all;'); + + var element = jqLite('
'); + $rootElement.append(element); + body.append($rootElement); + + var steps = []; + element.on('$animate:before', function(e, data) { + steps.push(['before', data.className, data.event]); + }); + + element.on('$animate:after', function(e, data) { + steps.push(['after', data.className, data.event]); + }); + + $animate.addClass(element, 'klass'); + + $timeout.flush(1); + + expect(steps.pop()).toEqual(['before', 'klass', 'addClass']); + + $animate.triggerReflow(); + $timeout.flush(1); + + expect(steps.pop()).toEqual(['after', 'klass', 'addClass']); + })); + + it('should fire the DOM callbacks even if no animation is rendered', + inject(function($animate, $rootScope, $compile, $sniffer, $rootElement, $timeout) { + + $animate.enabled(true); + + var parent = jqLite(''); + var element = jqLite(''); + $rootElement.append(parent); + body.append($rootElement); + + var steps = []; + element.on('$animate:before', function(e, data) { + steps.push(['before', data.className, data.event]); + }); + + element.on('$animate:after', function(e, data) { + steps.push(['after', data.className, data.event]); + }); + + $animate.enter(element, parent); + $rootScope.$digest(); + + $timeout.flush(1); + + expect(steps.shift()).toEqual(['before', 'ng-enter', 'enter']); + expect(steps.shift()).toEqual(['after', 'ng-enter', 'enter']); + })); it("should fire a done callback when provided with no animation", inject(function($animate, $rootScope, $compile, $sniffer, $rootElement, $timeout) {