Skip to content

Commit

Permalink
fix(ngAnimate): allow removing classes that are added by a running an…
Browse files Browse the repository at this point in the history
…imation

This allows follow-up animations to remove a class that is currently
being added.

Fixes angular#13339
Fixes angular#13380
Closes angular#13414
  • Loading branch information
Narretz committed Jan 5, 2016
1 parent 0e03644 commit ff19137
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 4 deletions.
44 changes: 40 additions & 4 deletions src/ngAnimate/animateQueue.js
Expand Up @@ -5,13 +5,36 @@ var NG_ANIMATE_PIN_DATA = '$ngAnimatePin';
var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
var PRE_DIGEST_STATE = 1;
var RUNNING_STATE = 2;
var ONE_SPACE = ' ';

var rules = this.rules = {
skip: [],
cancel: [],
join: []
};

function makeTruthyCssClassMap(classString) {
if (!classString) {
return null;
}

var keys = classString.split(ONE_SPACE);
var map = Object.create(null);

forEach(keys, function(key) {
map[key] = true;
});
return map;
}

function hasMatchingClasses(classString, classMap) {
if (classString && classMap) {
return classString.split(ONE_SPACE).some(function(className) {
return classMap[className];
});
}
}

function isAllowed(ruleType, element, currentAnimation, previousAnimation) {
return rules[ruleType].some(function(fn) {
return fn(element, currentAnimation, previousAnimation);
Expand Down Expand Up @@ -59,11 +82,24 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
});

rules.cancel.push(function(element, newAnimation, currentAnimation) {
var nO = newAnimation.options;
var cO = currentAnimation.options;

// if the exact same CSS class is added/removed then it's safe to cancel it
return (nO.addClass && nO.addClass === cO.removeClass) || (nO.removeClass && nO.removeClass === cO.addClass);

var nA = newAnimation.options.addClass;
var nR = newAnimation.options.removeClass;
var cA = currentAnimation.options.addClass;
var cR = currentAnimation.options.removeClass;

// early detection to save the global CPU shortage :)
if ((!isDefined(nA) && !isDefined(nR)) || (!isDefined(cA) && !isDefined(cR))) {
return false;
}

var cancelSomething = false;

cA = makeTruthyCssClassMap(cA);
cR = makeTruthyCssClassMap(cR);

return (hasMatchingClasses(nA, cR)) || (hasMatchingClasses(nR, cA));
});

this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap',
Expand Down
39 changes: 39 additions & 0 deletions test/ngAnimate/integrationSpec.js
Expand Up @@ -387,6 +387,45 @@ describe('ngAnimate integration tests', function() {

dealoc(element);
}));


it("should remove a class when the same class is currently being added by a joined class-based animation",
inject(function($animate, $animateCss, $rootScope, $document, $rootElement, $$rAF) {

ss.addRule('.hide', 'opacity: 0');
ss.addRule('.hide-add, .hide-remove', 'transition: 1s linear all');

jqLite($document[0].body).append($rootElement);
element = jqLite('<div></div>');
$rootElement.append(element);

// These animations will be joined together
$animate.addClass(element, 'red');
$animate.addClass(element, 'hide');
$rootScope.$digest();

expect(element).toHaveClass('red-add');
expect(element).toHaveClass('hide-add');

// When a digest has passed, but no $rAF has been issued yet, .hide hasn't been added to
// the element yet
$animate.removeClass(element, 'hide');
$rootScope.$digest();
$$rAF.flush();

expect(element).not.toHaveClass('hide-add hide-add-active');
expect(element).toHaveClass('hide-remove hide-remove-active');

//End the animation process
browserTrigger(element, 'transitionend',
{ timeStamp: Date.now() + 1000, elapsedTime: 2 });
$animate.flush();

expect(element).not.toHaveClass('hide-add-active red-add-active');
expect(element).toHaveClass('red');
expect(element).not.toHaveClass('hide');
}));

});

describe('JS animations', function() {
Expand Down

0 comments on commit ff19137

Please sign in to comment.