From 4368ea38f03a0f544cce21da29347529dd890469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Thu, 17 Dec 2015 16:53:07 -0800 Subject: [PATCH] fix(ngAnimate): only copy over the animation options once A bug in material has exposed that ngAnimate makes a copy of the provided animation options twice. By making two copies, the same DOM operations are performed during and at the end of the animation. If the CSS classes being added/ removed contain existing transition code, then this will lead to rendering issues. Closes #13722 Closes #13578 --- src/ng/animateCss.js | 12 +++++--- src/ngAnimate/animateCss.js | 14 +++++---- test/ng/animateCssSpec.js | 22 +++++++++++++++ test/ngAnimate/animateCssSpec.js | 22 +++++++++++++++ test/ngAnimate/integrationSpec.js | 47 +++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 10 deletions(-) diff --git a/src/ng/animateCss.js b/src/ng/animateCss.js index b3e3c3786550..0ee4e18097bf 100644 --- a/src/ng/animateCss.js +++ b/src/ng/animateCss.js @@ -15,10 +15,14 @@ var $CoreAnimateCssProvider = function() { this.$get = ['$$rAF', '$q', '$$AnimateRunner', function($$rAF, $q, $$AnimateRunner) { return function(element, initialOptions) { - // we always make a copy of the options since - // there should never be any side effects on - // the input data when running `$animateCss`. - var options = copy(initialOptions); + // all of the animation functions should create + // a copy of the options data, however, if a + // parent service has already created a copy then + // we should stick to using that + var options = initialOptions || {}; + if (!options.$$prepared) { + options = copy(options); + } // there is no point in applying the styles since // there is no animation that goes on at all in diff --git a/src/ngAnimate/animateCss.js b/src/ngAnimate/animateCss.js index c2df5b16e3a6..5025751e1dfc 100644 --- a/src/ngAnimate/animateCss.js +++ b/src/ngAnimate/animateCss.js @@ -447,10 +447,14 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { } return function init(element, initialOptions) { - // we always make a copy of the options since - // there should never be any side effects on - // the input data when running `$animateCss`. - var options = copy(initialOptions); + // all of the animation functions should create + // a copy of the options data, however, if a + // parent service has already created a copy then + // we should stick to using that + var options = initialOptions || {}; + if (!options.$$prepared) { + options = prepareAnimationOptions(copy(options)); + } var restoreStyles = {}; var node = getDomNode(element); @@ -460,8 +464,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { return closeAndReturnNoopAnimator(); } - options = prepareAnimationOptions(options); - var temporaryStyles = []; var classes = element.attr('class'); var styles = packageStyles(options); diff --git a/test/ng/animateCssSpec.js b/test/ng/animateCssSpec.js index f0a027805cfa..dcc37b7fc9c9 100644 --- a/test/ng/animateCssSpec.js +++ b/test/ng/animateCssSpec.js @@ -31,6 +31,28 @@ describe("$animateCss", function() { expect(copiedOptions).toEqual(initialOptions); })); + it("should not create a copy of the provided options if they have already been prepared earlier", + inject(function($animateCss, $$rAF) { + + var options = { + from: { height: '50px' }, + to: { width: '50px' }, + addClass: 'one', + removeClass: 'two' + }; + + options.$$prepared = true; + var runner = $animateCss(element, options).start(); + runner.end(); + + $$rAF.flush(); + + expect(options.addClass).toBeFalsy(); + expect(options.removeClass).toBeFalsy(); + expect(options.to).toBeFalsy(); + expect(options.from).toBeFalsy(); + })); + it("should apply the provided [from] CSS to the element", inject(function($animateCss) { $animateCss(element, { from: { height: '50px' }}).start(); expect(element.css('height')).toBe('50px'); diff --git a/test/ngAnimate/animateCssSpec.js b/test/ngAnimate/animateCssSpec.js index c8939d60252f..7de34ee58615 100644 --- a/test/ngAnimate/animateCssSpec.js +++ b/test/ngAnimate/animateCssSpec.js @@ -2036,6 +2036,28 @@ describe("ngAnimate $animateCss", function() { expect(copiedOptions).toEqual(initialOptions); })); + it("should not create a copy of the provided options if they have already been prepared earlier", + inject(function($animate, $animateCss) { + + var options = { + from: { height: '50px' }, + to: { width: '50px' }, + addClass: 'one', + removeClass: 'two' + }; + + options.$$prepared = true; + var runner = $animateCss(element, options).start(); + runner.end(); + + $animate.flush(); + + expect(options.addClass).toBeFalsy(); + expect(options.removeClass).toBeFalsy(); + expect(options.to).toBeFalsy(); + expect(options.from).toBeFalsy(); + })); + describe("[$$skipPreparationClasses]", function() { it('should not apply and remove the preparation classes to the element when true', inject(function($animateCss) { diff --git a/test/ngAnimate/integrationSpec.js b/test/ngAnimate/integrationSpec.js index f506e53aee74..a1f4c5cf7a18 100644 --- a/test/ngAnimate/integrationSpec.js +++ b/test/ngAnimate/integrationSpec.js @@ -28,6 +28,27 @@ describe('ngAnimate integration tests', function() { describe('CSS animations', function() { if (!browserSupportsCssAnimations()) return; + it("should only create a single copy of the provided animation options", + inject(function($rootScope, $rootElement, $animate) { + + ss.addRule('.animate-me', 'transition:2s linear all;'); + + var element = jqLite('
'); + html(element); + + var myOptions = {to: { 'color': 'red' }}; + + var spy = spyOn(window, 'copy'); + expect(spy).not.toHaveBeenCalled(); + + var animation = $animate.leave(element, myOptions); + $rootScope.$digest(); + $animate.flush(); + + expect(spy).toHaveBeenCalledOnce(); + dealoc(element); + })); + they('should render an $prop animation', ['enter', 'leave', 'move', 'addClass', 'removeClass', 'setClass'], function(event) { @@ -390,6 +411,32 @@ describe('ngAnimate integration tests', function() { expect(element).not.toHaveClass('hide'); })); + it('should handle ng-if & ng-class with a class that is removed before its add animation has concluded', function() { + inject(function($animate, $rootScope, $compile, $timeout, $$rAF) { + + ss.addRule('.animate-me', 'transition: all 0.5s;'); + + element = jqLite('
'); + + html(element); + $rootScope.blue = true; + $rootScope.red = true; + $compile(element)($rootScope); + $rootScope.$digest(); + + var child = element.find('div'); + + // Trigger class removal before the add animation has been concluded + $rootScope.blue = false; + $animate.closeAndFlush(); + + expect(child).toHaveClass('red'); + expect(child).not.toHaveClass('blue'); + }); + }); }); describe('JS animations', function() {