Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 2fc954d

Browse files
matskoNarretz
authored andcommitted
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
1 parent 512c081 commit 2fc954d

File tree

5 files changed

+107
-10
lines changed

5 files changed

+107
-10
lines changed

src/ng/animateCss.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ var $CoreAnimateCssProvider = function() {
1515
this.$get = ['$$rAF', '$q', '$$AnimateRunner', function($$rAF, $q, $$AnimateRunner) {
1616

1717
return function(element, initialOptions) {
18-
// we always make a copy of the options since
19-
// there should never be any side effects on
20-
// the input data when running `$animateCss`.
21-
var options = copy(initialOptions);
18+
// all of the animation functions should create
19+
// a copy of the options data, however, if a
20+
// parent service has already created a copy then
21+
// we should stick to using that
22+
var options = initialOptions || {};
23+
if (!options.$$prepared) {
24+
options = copy(options);
25+
}
2226

2327
// there is no point in applying the styles since
2428
// there is no animation that goes on at all in

src/ngAnimate/animateCss.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -447,10 +447,14 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
447447
}
448448

449449
return function init(element, initialOptions) {
450-
// we always make a copy of the options since
451-
// there should never be any side effects on
452-
// the input data when running `$animateCss`.
453-
var options = copy(initialOptions);
450+
// all of the animation functions should create
451+
// a copy of the options data, however, if a
452+
// parent service has already created a copy then
453+
// we should stick to using that
454+
var options = initialOptions || {};
455+
if (!options.$$prepared) {
456+
options = prepareAnimationOptions(copy(options));
457+
}
454458

455459
var restoreStyles = {};
456460
var node = getDomNode(element);
@@ -460,8 +464,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
460464
return closeAndReturnNoopAnimator();
461465
}
462466

463-
options = prepareAnimationOptions(options);
464-
465467
var temporaryStyles = [];
466468
var classes = element.attr('class');
467469
var styles = packageStyles(options);

test/ng/animateCssSpec.js

+22
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,28 @@ describe("$animateCss", function() {
3131
expect(copiedOptions).toEqual(initialOptions);
3232
}));
3333

34+
it("should not create a copy of the provided options if they have already been prepared earlier",
35+
inject(function($animateCss, $$rAF) {
36+
37+
var options = {
38+
from: { height: '50px' },
39+
to: { width: '50px' },
40+
addClass: 'one',
41+
removeClass: 'two'
42+
};
43+
44+
options.$$prepared = true;
45+
var runner = $animateCss(element, options).start();
46+
runner.end();
47+
48+
$$rAF.flush();
49+
50+
expect(options.addClass).toBeFalsy();
51+
expect(options.removeClass).toBeFalsy();
52+
expect(options.to).toBeFalsy();
53+
expect(options.from).toBeFalsy();
54+
}));
55+
3456
it("should apply the provided [from] CSS to the element", inject(function($animateCss) {
3557
$animateCss(element, { from: { height: '50px' }}).start();
3658
expect(element.css('height')).toBe('50px');

test/ngAnimate/animateCssSpec.js

+22
Original file line numberDiff line numberDiff line change
@@ -2036,6 +2036,28 @@ describe("ngAnimate $animateCss", function() {
20362036
expect(copiedOptions).toEqual(initialOptions);
20372037
}));
20382038

2039+
it("should not create a copy of the provided options if they have already been prepared earlier",
2040+
inject(function($animate, $animateCss) {
2041+
2042+
var options = {
2043+
from: { height: '50px' },
2044+
to: { width: '50px' },
2045+
addClass: 'one',
2046+
removeClass: 'two'
2047+
};
2048+
2049+
options.$$prepared = true;
2050+
var runner = $animateCss(element, options).start();
2051+
runner.end();
2052+
2053+
$animate.flush();
2054+
2055+
expect(options.addClass).toBeFalsy();
2056+
expect(options.removeClass).toBeFalsy();
2057+
expect(options.to).toBeFalsy();
2058+
expect(options.from).toBeFalsy();
2059+
}));
2060+
20392061
describe("[$$skipPreparationClasses]", function() {
20402062
it('should not apply and remove the preparation classes to the element when true',
20412063
inject(function($animateCss) {

test/ngAnimate/integrationSpec.js

+47
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,27 @@ describe('ngAnimate integration tests', function() {
2828
describe('CSS animations', function() {
2929
if (!browserSupportsCssAnimations()) return;
3030

31+
it("should only create a single copy of the provided animation options",
32+
inject(function($rootScope, $rootElement, $animate) {
33+
34+
ss.addRule('.animate-me', 'transition:2s linear all;');
35+
36+
var element = jqLite('<div class="animate-me"></div>');
37+
html(element);
38+
39+
var myOptions = {to: { 'color': 'red' }};
40+
41+
var spy = spyOn(window, 'copy');
42+
expect(spy).not.toHaveBeenCalled();
43+
44+
var animation = $animate.leave(element, myOptions);
45+
$rootScope.$digest();
46+
$animate.flush();
47+
48+
expect(spy).toHaveBeenCalledOnce();
49+
dealoc(element);
50+
}));
51+
3152
they('should render an $prop animation',
3253
['enter', 'leave', 'move', 'addClass', 'removeClass', 'setClass'], function(event) {
3354

@@ -390,6 +411,32 @@ describe('ngAnimate integration tests', function() {
390411
expect(element).not.toHaveClass('hide');
391412
}));
392413

414+
it('should handle ng-if & ng-class with a class that is removed before its add animation has concluded', function() {
415+
inject(function($animate, $rootScope, $compile, $timeout, $$rAF) {
416+
417+
ss.addRule('.animate-me', 'transition: all 0.5s;');
418+
419+
element = jqLite('<section><div ng-if="true" class="animate-me" ng-class="{' +
420+
'red: red,' +
421+
'blue: blue' +
422+
'}"></div></section>');
423+
424+
html(element);
425+
$rootScope.blue = true;
426+
$rootScope.red = true;
427+
$compile(element)($rootScope);
428+
$rootScope.$digest();
429+
430+
var child = element.find('div');
431+
432+
// Trigger class removal before the add animation has been concluded
433+
$rootScope.blue = false;
434+
$animate.closeAndFlush();
435+
436+
expect(child).toHaveClass('red');
437+
expect(child).not.toHaveClass('blue');
438+
});
439+
});
393440
});
394441

395442
describe('JS animations', function() {

0 commit comments

Comments
 (0)