Skip to content

Commit

Permalink
fix(ngAnimate): do not alter the provided options data
Browse files Browse the repository at this point in the history
Prior to this fix the provided options object would be
altered as the animation kicks off due to the underlying
mechanics of ngAnimate. This patch ensures that a
copy of the provided options is used instead. This patch
also works for when `$animateCss` is used by itself.

Fixes angular#13040
Closes angular#13175
  • Loading branch information
matsko authored and Narretz committed Dec 2, 2015
1 parent 6a0686d commit 193153c
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 7 deletions.
8 changes: 7 additions & 1 deletion src/ng/animateCss.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
*/
var $CoreAnimateCssProvider = function() {
this.$get = ['$$rAF', '$q', '$$AnimateRunner', function($$rAF, $q, $$AnimateRunner) {
return function(element, options) {

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);

// there is no point in applying the styles since
// there is no animation that goes on at all in
// this version of $animateCss.
Expand Down
1 change: 1 addition & 0 deletions src/ngAnimate/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"angular": false,
"noop": false,

"copy": false,
"forEach": false,
"extend": false,
"jqLite": false,
Expand Down
7 changes: 6 additions & 1 deletion src/ngAnimate/animateCss.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,12 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
return timings;
}

return function init(element, options) {
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);

var restoreStyles = {};
var node = getDomNode(element);
if (!node
Expand Down
7 changes: 6 additions & 1 deletion src/ngAnimate/animateQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,12 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
}
};

function queueAnimation(element, event, options) {
function queueAnimation(element, event, 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);

var node, parent;
element = stripCommentsFromElement(element);
if (element) {
Expand Down
1 change: 1 addition & 0 deletions src/ngAnimate/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

/* jshint ignore:start */
var noop = angular.noop;
var copy = angular.copy;
var extend = angular.extend;
var jqLite = angular.element;
var forEach = angular.forEach;
Expand Down
15 changes: 15 additions & 0 deletions test/ng/animateCssSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ describe("$animateCss", function() {

describe("without animation", function() {

it("should not alter the provided options input in any way", inject(function($animateCss) {
var initialOptions = {
from: { height: '50px' },
to: { width: '50px' },
addClass: 'one',
removeClass: 'two'
};

var copiedOptions = copy(initialOptions);

expect(copiedOptions).toEqual(initialOptions);
$animateCss(element, copiedOptions).start();
expect(copiedOptions).toEqual(initialOptions);
}));

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');
Expand Down
21 changes: 21 additions & 0 deletions test/ng/animateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,27 @@ describe("$animate", function() {
});
});

it("should not alter the provided options input in any way throughout the animation", inject(function($animate, $rootElement, $rootScope) {
var element = jqLite('<div></div>');
var parent = $rootElement;

var initialOptions = {
from: { height: '50px' },
to: { width: '50px' },
addClass: 'one',
removeClass: 'two'
};

var copiedOptions = copy(initialOptions);
expect(copiedOptions).toEqual(initialOptions);

var runner = $animate.enter(element, parent, null, copiedOptions);
expect(copiedOptions).toEqual(initialOptions);

$rootScope.$digest();
expect(copiedOptions).toEqual(initialOptions);
}));

describe('CSS class DOM manipulation', function() {
var element;
var addClass;
Expand Down
31 changes: 31 additions & 0 deletions test/ngAnimate/animateCssSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1865,6 +1865,37 @@ describe("ngAnimate $animateCss", function() {
};
}));

it("should not alter the provided options input in any way throughout the animation", inject(function($animateCss) {
var initialOptions = {
from: { height: '50px' },
to: { width: '50px' },
addClass: 'one',
removeClass: 'two',
duration: 10,
delay: 10,
structural: true,
keyframeStyle: '1s rotate',
transitionStyle: '1s linear',
stagger: 0.5,
staggerIndex: 3
};

var copiedOptions = copy(initialOptions);
expect(copiedOptions).toEqual(initialOptions);

var animator = $animateCss(element, copiedOptions);
expect(copiedOptions).toEqual(initialOptions);

var runner = animator.start();
expect(copiedOptions).toEqual(initialOptions);

triggerAnimationStartFrame();
expect(copiedOptions).toEqual(initialOptions);

runner.end();
expect(copiedOptions).toEqual(initialOptions);
}));

describe("[$$skipPreparationClasses]", function() {
it('should not apply and remove the preparation classes to the element when true',
inject(function($animateCss) {
Expand Down
34 changes: 30 additions & 4 deletions test/ngAnimate/animateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,24 @@ describe("animations", function() {
};
}));

it("should not alter the provided options input in any way throughout the animation", inject(function($animate, $rootScope) {
var initialOptions = {
from: { height: '50px' },
to: { width: '50px' },
addClass: 'one',
removeClass: 'two'
};

var copiedOptions = copy(initialOptions);
expect(copiedOptions).toEqual(initialOptions);

var runner = $animate.enter(element, parent, null, copiedOptions);
expect(copiedOptions).toEqual(initialOptions);

$rootScope.$digest();
expect(copiedOptions).toEqual(initialOptions);
}));

it('should animate only the specified CSS className matched within $animateProvider.classNameFilter', function() {
module(function($animateProvider) {
$animateProvider.classNameFilter(/only-allow-this-animation/);
Expand All @@ -149,32 +167,40 @@ describe("animations", function() {
});
});

they('should nullify both options.$prop when passed into an animation if it is not a string or an array', ['addClass', 'removeClass'], function(prop) {
they('should not apply the provided options.$prop value unless it\'s a string or string-based array', ['addClass', 'removeClass'], function(prop) {
inject(function($animate, $rootScope) {
var startingCssClasses = element.attr('class') || '';

var options1 = {};
options1[prop] = function() {};
$animate.enter(element, parent, null, options1);

expect(options1[prop]).toBeFalsy();
expect(element.attr('class')).toEqual(startingCssClasses);

$rootScope.$digest();

var options2 = {};
options2[prop] = true;
$animate.leave(element, options2);

expect(options2[prop]).toBeFalsy();
expect(element.attr('class')).toEqual(startingCssClasses);

$rootScope.$digest();

capturedAnimation = null;

var options3 = {};
if (prop === 'removeClass') {
element.addClass('fatias');
startingCssClasses = element.attr('class');
}

options3[prop] = ['fatias'];
$animate.enter(element, parent, null, options3);
expect(options3[prop]).toBe('fatias');

$rootScope.$digest();

expect(element.attr('class')).not.toEqual(startingCssClasses);
});
});

Expand Down
45 changes: 45 additions & 0 deletions test/ngAnimate/integrationSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -562,5 +562,50 @@ describe('ngAnimate integration tests', function() {
}
});
});

it("should not alter the provided options values in anyway throughout the animation", function() {
var animationSpy = jasmine.createSpy();
module(function($animateProvider) {
$animateProvider.register('.this-animation', function() {
return {
enter: function(element, done) {
animationSpy();
done();
}
};
});
});

inject(function($animate, $rootScope, $compile) {
element = jqLite('<div class="parent-man"></div>');
var child = jqLite('<div class="child-man one"></div>');

var initialOptions = {
from: { height: '50px' },
to: { width: '100px' },
addClass: 'one',
removeClass: 'two'
};

var copiedOptions = copy(initialOptions);
expect(copiedOptions).toEqual(initialOptions);

html(element);
$compile(element)($rootScope);

$animate.enter(child, element, null, copiedOptions);
$rootScope.$digest();
expect(copiedOptions).toEqual(initialOptions);

$animate.flush();
expect(copiedOptions).toEqual(initialOptions);

expect(child).toHaveClass('one');
expect(child).not.toHaveClass('two');

expect(child.attr('style')).toContain('100px');
expect(child.attr('style')).toContain('50px');
});
});
});
});

0 comments on commit 193153c

Please sign in to comment.