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

Commit e08fb08

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 ee22b57 commit e08fb08

File tree

6 files changed

+143
-45
lines changed

6 files changed

+143
-45
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
@@ -2028,6 +2028,28 @@ describe("ngAnimate $animateCss", function() {
20282028
expect(copiedOptions).toEqual(initialOptions);
20292029
}));
20302030

2031+
it("should not create a copy of the provided options if they have already been prepared earlier",
2032+
inject(function($animate, $animateCss) {
2033+
2034+
var options = {
2035+
from: { height: '50px' },
2036+
to: { width: '50px' },
2037+
addClass: 'one',
2038+
removeClass: 'two'
2039+
};
2040+
2041+
options.$$prepared = true;
2042+
var runner = $animateCss(element, options).start();
2043+
runner.end();
2044+
2045+
$animate.flush();
2046+
2047+
expect(options.addClass).toBeFalsy();
2048+
expect(options.removeClass).toBeFalsy();
2049+
expect(options.to).toBeFalsy();
2050+
expect(options.from).toBeFalsy();
2051+
}));
2052+
20312053
describe("[$$skipPreparationClasses]", function() {
20322054
it('should not apply and remove the preparation classes to the element when true',
20332055
inject(function($animateCss) {

test/ngAnimate/integrationSpec.js

+82-34
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

@@ -637,50 +658,77 @@ describe('ngAnimate integration tests', function() {
637658
}
638659
});
639660
});
661+
});
640662

641-
it("should not alter the provided options values in anyway throughout the animation", function() {
642-
var animationSpy = jasmine.createSpy();
643-
module(function($animateProvider) {
644-
$animateProvider.register('.this-animation', function() {
645-
return {
646-
enter: function(element, done) {
647-
animationSpy();
648-
done();
649-
}
650-
};
651-
});
663+
it("should not alter the provided options values in anyway throughout the animation", function() {
664+
var animationSpy = jasmine.createSpy();
665+
module(function($animateProvider) {
666+
$animateProvider.register('.this-animation', function() {
667+
return {
668+
enter: function(element, done) {
669+
animationSpy();
670+
done();
671+
}
672+
};
652673
});
674+
});
653675

654-
inject(function($animate, $rootScope, $compile) {
655-
element = jqLite('<div class="parent-man"></div>');
656-
var child = jqLite('<div class="child-man one"></div>');
676+
inject(function($animate, $rootScope, $compile) {
677+
element = jqLite('<div class="parent-man"></div>');
678+
var child = jqLite('<div class="child-man one"></div>');
657679

658-
var initialOptions = {
659-
from: { height: '50px' },
660-
to: { width: '100px' },
661-
addClass: 'one',
662-
removeClass: 'two'
663-
};
680+
var initialOptions = {
681+
from: { height: '50px' },
682+
to: { width: '100px' },
683+
addClass: 'one',
684+
removeClass: 'two'
685+
};
664686

665-
var copiedOptions = copy(initialOptions);
666-
expect(copiedOptions).toEqual(initialOptions);
687+
var copiedOptions = copy(initialOptions);
688+
expect(copiedOptions).toEqual(initialOptions);
667689

668-
html(element);
669-
$compile(element)($rootScope);
690+
html(element);
691+
$compile(element)($rootScope);
670692

671-
$animate.enter(child, element, null, copiedOptions);
672-
$rootScope.$digest();
673-
expect(copiedOptions).toEqual(initialOptions);
693+
$animate.enter(child, element, null, copiedOptions);
694+
$rootScope.$digest();
695+
expect(copiedOptions).toEqual(initialOptions);
674696

675-
$animate.flush();
676-
expect(copiedOptions).toEqual(initialOptions);
697+
$animate.flush();
698+
expect(copiedOptions).toEqual(initialOptions);
677699

678-
expect(child).toHaveClass('one');
679-
expect(child).not.toHaveClass('two');
700+
expect(child).toHaveClass('one');
701+
expect(child).not.toHaveClass('two');
680702

681-
expect(child.attr('style')).toContain('100px');
682-
expect(child.attr('style')).toContain('50px');
683-
});
703+
expect(child.attr('style')).toContain('100px');
704+
expect(child.attr('style')).toContain('50px');
705+
});
706+
});
707+
708+
it('should handle ng-if & ng-class with a class that is removed before its add animation has concluded', function() {
709+
inject(function($animate, $rootScope, $compile, $timeout, $$rAF) {
710+
711+
ss.addRule('.animate-me', 'transition: all 0.5s;');
712+
713+
element = jqLite('<section><div ng-if="true" class="animate-me" ng-class="{' +
714+
'red: red,' +
715+
'blue: blue' +
716+
'}"></div></section>');
717+
718+
html(element);
719+
$rootScope.blue = true;
720+
$rootScope.red = true;
721+
$compile(element)($rootScope);
722+
$rootScope.$digest();
723+
724+
var child = element.find('div');
725+
726+
// Trigger class removal before the add animation has been concluded
727+
$rootScope.blue = false;
728+
$animate.closeAndFlush();
729+
730+
expect(child).toHaveClass('red');
731+
expect(child).not.toHaveClass('blue');
684732
});
685733
});
686734
});

test/ngMock/angular-mocksSpec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2159,7 +2159,7 @@ describe('ngMockE2E', function() {
21592159
from: { background: 'red' },
21602160
to: { background: 'blue' },
21612161
duration: 1,
2162-
transitionStyle: '1s linear all'
2162+
transitionStyle: 'all 1s'
21632163
});
21642164

21652165
expect(function() {

0 commit comments

Comments
 (0)