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
This commit is contained in:
Matias Niemelä
2015-12-17 16:53:07 -08:00
committed by Martin Staffa
parent 512c081187
commit 2fc954d33a
5 changed files with 107 additions and 10 deletions
+8 -4
View File
@@ -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
+8 -6
View File
@@ -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);
+22
View File
@@ -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');
+22
View File
@@ -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) {
+47
View File
@@ -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('<div class="animate-me"></div>');
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('<section><div ng-if="true" class="animate-me" ng-class="{' +
'red: red,' +
'blue: blue' +
'}"></div></section>');
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() {