feat($animate): allow $animate to pass custom styles into animations

$animate now supports an optional parameter which provides CSS styling
which will be provided into the CSS-based animations as well as any
custom animation functions. Once the animation is complete then the
styles will be applied directly to the element. If no animation is
detected or the `ngAnimate` module is not active then the styles
will be applied immediately.

BREAKING CHANGE: staggering animations that use transitions will now
always block the transition from starting (via `transition: 0s none`)
up until the stagger step kicks in. The former behaviour was that the
block was removed as soon as the pending class was added. This fix
allows for styles to be applied in the pending class without causing
an animation to trigger prematurely.
This commit is contained in:
Matias Niemelä
2014-09-08 00:19:22 -04:00
committed by Igor Minar
parent 63ef085b9a
commit e5f4d7b10a
6 changed files with 597 additions and 81 deletions

View File

@@ -103,6 +103,68 @@ describe("$animate", function() {
});
inject();
});
it("should apply and retain inline styles on the element that is animated", inject(function($animate, $rootScope) {
var element = jqLite('<div></div>');
var parent = jqLite('<div></div>');
var other = jqLite('<div></div>');
parent.append(other);
$animate.enabled(true);
$animate.enter(element, parent, null, {
to: { color : 'red' }
});
assertColor('red');
$animate.move(element, null, other, {
to: { color : 'yellow' }
});
assertColor('yellow');
$animate.addClass(element, 'on', {
to: { color : 'green' }
});
$rootScope.$digest();
assertColor('green');
$animate.setClass(element, 'off', 'on', {
to: { color : 'black' }
});
$rootScope.$digest();
assertColor('black');
$animate.removeClass(element, 'off', {
to: { color : 'blue' }
});
$rootScope.$digest();
assertColor('blue');
$animate.leave(element, 'off', {
to: { color : 'blue' }
});
assertColor('blue'); //nothing should happen the element is gone anyway
function assertColor(color) {
expect(element[0].style.color).toBe(color);
}
}));
it("should merge the from and to styles that are provided",
inject(function($animate, $rootScope) {
var element = jqLite('<div></div>');
element.css('color', 'red');
$animate.addClass(element, 'on', {
from : { color : 'green' },
to : { borderColor : 'purple' }
});
$rootScope.$digest();
var style = element[0].style;
expect(style.color).toBe('green');
expect(style.borderColor).toBe('purple');
}));
});
describe('CSS class DOM manipulation', function() {

View File

@@ -170,13 +170,13 @@ describe('ngShow / ngHide animations', function() {
item = $animate.queue.shift();
expect(item.event).toEqual('addClass');
expect(item.options).toEqual('ng-hide-animate');
expect(item.options.tempClasses).toEqual('ng-hide-animate');
$scope.on = true;
$scope.$digest();
item = $animate.queue.shift();
expect(item.event).toEqual('removeClass');
expect(item.options).toEqual('ng-hide-animate');
expect(item.options.tempClasses).toEqual('ng-hide-animate');
}));
});
@@ -217,13 +217,13 @@ describe('ngShow / ngHide animations', function() {
item = $animate.queue.shift();
expect(item.event).toEqual('removeClass');
expect(item.options).toEqual('ng-hide-animate');
expect(item.options.tempClasses).toEqual('ng-hide-animate');
$scope.on = true;
$scope.$digest();
item = $animate.queue.shift();
expect(item.event).toEqual('addClass');
expect(item.options).toEqual('ng-hide-animate');
expect(item.options.tempClasses).toEqual('ng-hide-animate');
}));
});
});

View File

@@ -292,7 +292,7 @@ describe("ngAnimate", function() {
});
$animateProvider.register('.custom-delay', function($timeout) {
function animate(element, done) {
done = arguments.length == 3 ? arguments[2] : done;
done = arguments.length == 4 ? arguments[2] : done;
$timeout(done, 2000, false);
return function() {
element.addClass('animation-cancelled');
@@ -306,7 +306,7 @@ describe("ngAnimate", function() {
});
$animateProvider.register('.custom-long-delay', function($timeout) {
function animate(element, done) {
done = arguments.length == 3 ? arguments[2] : done;
done = arguments.length == 4 ? arguments[2] : done;
$timeout(done, 20000, false);
return function(cancelled) {
element.addClass(cancelled ? 'animation-cancelled' : 'animation-ended');
@@ -1037,8 +1037,124 @@ describe("ngAnimate", function() {
expect(element.hasClass('custom-long-delay-add')).toBe(false);
expect(element.hasClass('custom-long-delay-add-active')).toBe(false);
}));
it('should apply directive styles and provide the style collection to the animation function', function() {
var animationDone;
var animationStyles;
var proxyAnimation = function() {
var limit = arguments.length-1;
animationStyles = arguments[limit];
animationDone = arguments[limit-1];
};
module(function($animateProvider) {
$animateProvider.register('.capture', function() {
return {
enter : proxyAnimation,
leave : proxyAnimation,
move : proxyAnimation,
addClass : proxyAnimation,
removeClass : proxyAnimation,
setClass : proxyAnimation
};
});
});
inject(function($animate, $rootScope, $compile, $sniffer, $timeout, _$rootElement_) {
$rootElement = _$rootElement_;
$animate.enabled(true);
element = $compile(html('<div></div>'))($rootScope);
var otherParent = $compile('<div></div>')($rootScope);
var child = $compile('<div class="capture" style="transition: 0s!important; -webkit-transition: 0s!important;"></div>')($rootScope);
$rootElement.append(otherParent);
$rootScope.$digest();
var styles = {
from: { backgroundColor: 'blue' },
to: { backgroundColor: 'red' }
};
//enter
$animate.enter(child, element, null, styles);
$rootScope.$digest();
$animate.triggerReflow();
expect(animationStyles).toEqual(styles);
animationDone();
animationDone = animationStyles = null;
$animate.triggerCallbacks();
//move
$animate.move(child, null, otherParent, styles);
$rootScope.$digest();
$animate.triggerReflow();
expect(animationStyles).toEqual(styles);
animationDone();
animationDone = animationStyles = null;
$animate.triggerCallbacks();
//addClass
$animate.addClass(child, 'on', styles);
$rootScope.$digest();
$animate.triggerReflow();
expect(animationStyles).toEqual(styles);
animationDone();
animationDone = animationStyles = null;
$animate.triggerCallbacks();
//setClass
$animate.setClass(child, 'off', 'on', styles);
$rootScope.$digest();
$animate.triggerReflow();
expect(animationStyles).toEqual(styles);
animationDone();
animationDone = animationStyles = null;
$animate.triggerCallbacks();
//removeClass
$animate.removeClass(child, 'off', styles);
$rootScope.$digest();
$animate.triggerReflow();
expect(animationStyles).toEqual(styles);
animationDone();
animationDone = animationStyles = null;
$animate.triggerCallbacks();
//leave
$animate.leave(child, styles);
$rootScope.$digest();
$animate.triggerReflow();
expect(animationStyles).toEqual(styles);
animationDone();
animationDone = animationStyles = null;
$animate.triggerCallbacks();
dealoc(otherParent);
});
});
});
it("should apply animated styles even if there are no detected animations",
inject(function($compile, $animate, $rootScope, $sniffer, $rootElement, $document) {
$animate.enabled(true);
jqLite($document[0].body).append($rootElement);
element = $compile('<div class="fake-animation"></div>')($rootScope);
$animate.enter(element, $rootElement, null, {
to : {borderColor: 'red'}
});
$rootScope.$digest();
expect(element).toHaveClass('ng-animate');
$animate.triggerReflow();
$animate.triggerCallbacks();
expect(element).not.toHaveClass('ng-animate');
expect(element.attr('style')).toMatch(/border-color: red/);
}));
describe("with CSS3", function() {
@@ -1218,6 +1334,28 @@ describe("ngAnimate", function() {
})
);
it("should piggy-back-transition the styles with the max keyframe duration if provided by the directive",
inject(function($compile, $animate, $rootScope, $sniffer) {
$animate.enabled(true);
ss.addRule('.on', '-webkit-animation: 1s keyframeanimation; animation: 1s keyframeanimation;');
element = $compile(html('<div>1</div>'))($rootScope);
$animate.addClass(element, 'on', {
to: {borderColor: 'blue'}
});
$rootScope.$digest();
if ($sniffer.transitions) {
$animate.triggerReflow();
expect(element.attr('style')).toContain('border-color: blue');
expect(element.attr('style')).toMatch(/transition:.*1s/);
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
}
expect(element.attr('style')).toContain('border-color: blue');
}));
it("should pause the playstate when performing a stagger animation",
inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
@@ -1372,6 +1510,86 @@ describe("ngAnimate", function() {
}
}));
it("should stagger items and apply the transition + directive styles the right time when piggy-back styles are used",
inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $window) {
if(!$sniffer.transitions) return;
$animate.enabled(true);
ss.addRule('.stagger-animation.ng-enter, .stagger-animation.ng-leave',
'-webkit-animation:my_animation 1s 1s, your_animation 1s 2s;' +
'animation:my_animation 1s 1s, your_animation 1s 2s;');
ss.addRule('.stagger-animation.ng-enter-stagger, .stagger-animation.ng-leave-stagger',
'-webkit-animation-delay:0.1s;' +
'animation-delay:0.1s;');
var styles = {
from : { left : '50px' },
to : { left : '100px' }
};
var container = $compile(html('<div></div>'))($rootScope);
var elements = [];
for(var i = 0; i < 4; i++) {
var newScope = $rootScope.$new();
var element = $compile('<div class="stagger-animation"></div>')(newScope);
$animate.enter(element, container, null, styles);
elements.push(element);
}
$rootScope.$digest();
for(i = 0; i < 4; i++) {
expect(elements[i]).toHaveClass('ng-enter');
assertTransitionDuration(elements[i], '2', true);
assertLeftStyle(elements[i], '50');
}
$animate.triggerReflow();
expect(elements[0]).toHaveClass('ng-enter-active');
assertLeftStyle(elements[0], '100');
assertTransitionDuration(elements[0], '1');
for(i = 1; i < 4; i++) {
expect(elements[i]).not.toHaveClass('ng-enter-active');
assertTransitionDuration(elements[i], '1', true);
assertLeftStyle(elements[i], '100', true);
}
$timeout.flush(300);
for(i = 1; i < 4; i++) {
expect(elements[i]).toHaveClass('ng-enter-active');
assertTransitionDuration(elements[i], '1');
assertLeftStyle(elements[i], '100');
}
$timeout.flush();
for(i = 0; i < 4; i++) {
expect(elements[i]).not.toHaveClass('ng-enter');
expect(elements[i]).not.toHaveClass('ng-enter-active');
assertTransitionDuration(elements[i], '1', true);
assertLeftStyle(elements[i], '100');
}
function assertLeftStyle(element, val, not) {
var regex = new RegExp('left: ' + val + 'px');
var style = element.attr('style');
not ? expect(style).not.toMatch(regex)
: expect(style).toMatch(regex);
}
function assertTransitionDuration(element, val, not) {
var regex = new RegExp('transition:.*' + val + 's');
var style = element.attr('style');
not ? expect(style).not.toMatch(regex)
: expect(style).toMatch(regex);
}
}));
});
@@ -1741,6 +1959,82 @@ describe("ngAnimate", function() {
}
}));
it("should stagger items, apply directive styles but not apply a transition style when the stagger step kicks in",
inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $window) {
if(!$sniffer.transitions) return;
$animate.enabled(true);
ss.addRule('.stagger-animation.ng-enter, .ani.ng-leave',
'-webkit-transition:1s linear color 2s, 3s linear font-size 4s;' +
'transition:1s linear color 2s, 3s linear font-size 4s;');
ss.addRule('.stagger-animation.ng-enter-stagger, .ani.ng-leave-stagger',
'-webkit-transition-delay:0.1s;' +
'transition-delay:0.1s;');
var styles = {
from : { left : '155px' },
to : { left : '255px' }
};
var container = $compile(html('<div></div>'))($rootScope);
var elements = [];
for(var i = 0; i < 4; i++) {
var newScope = $rootScope.$new();
var element = $compile('<div class="stagger-animation"></div>')(newScope);
$animate.enter(element, container, null, styles);
elements.push(element);
}
$rootScope.$digest();
for(i = 0; i < 4; i++) {
expect(elements[i]).toHaveClass('ng-enter');
assertLeftStyle(elements[i], '155');
}
$animate.triggerReflow();
expect(elements[0]).toHaveClass('ng-enter-active');
assertLeftStyle(elements[0], '255');
assertNoTransitionDuration(elements[0]);
for(i = 1; i < 4; i++) {
expect(elements[i]).not.toHaveClass('ng-enter-active');
assertLeftStyle(elements[i], '255', true);
}
$timeout.flush(300);
for(i = 1; i < 4; i++) {
expect(elements[i]).toHaveClass('ng-enter-active');
assertNoTransitionDuration(elements[i]);
assertLeftStyle(elements[i], '255');
}
$timeout.flush();
for(i = 0; i < 4; i++) {
expect(elements[i]).not.toHaveClass('ng-enter');
expect(elements[i]).not.toHaveClass('ng-enter-active');
assertNoTransitionDuration(elements[i]);
assertLeftStyle(elements[i], '255');
}
function assertLeftStyle(element, val, not) {
var regex = new RegExp('left: ' + val + 'px');
var style = element.attr('style');
not ? expect(style).not.toMatch(regex)
: expect(style).toMatch(regex);
}
function assertNoTransitionDuration(element) {
var style = element.attr('style');
expect(style).not.toMatch(/transition/);
}
}));
it("should apply a closing timeout to close all pending transitions",
inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
@@ -2042,6 +2336,29 @@ describe("ngAnimate", function() {
expect(elements[i].attr('style')).toBeFalsy();
}
}));
it("should create a piggy-back-transition which has a duration the same as the max keyframe duration if any directive styles are provided",
inject(function($compile, $animate, $rootScope, $sniffer) {
$animate.enabled(true);
ss.addRule('.on', '-webkit-transition: 1s linear all; transition: 1s linear all;');
element = $compile(html('<div>1</div>'))($rootScope);
$animate.addClass(element, 'on', {
to: {color: 'red'}
});
$rootScope.$digest();
if ($sniffer.transitions) {
$animate.triggerReflow();
expect(element.attr('style')).toContain('color: red');
expect(element.attr('style')).not.toContain('transition');
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
}
expect(element.attr('style')).toContain('color: red');
}));
});
@@ -2472,9 +2789,9 @@ describe("ngAnimate", function() {
};
function capture(event) {
return function(element, add, remove, done) {
return function(element, add, remove, styles, done) {
//some animations only have one extra param
done = done || remove || add;
done = arguments[arguments.length-2]; //the last one is the styles array
captures[event]=done;
};
}
@@ -2491,28 +2808,40 @@ describe("ngAnimate", function() {
$compile(element)($rootScope);
assertTempClass('enter', 'temp-enter', function() {
$animate.enter(element, container, null, 'temp-enter');
$animate.enter(element, container, null, {
tempClasses: 'temp-enter'
});
});
assertTempClass('move', 'temp-move', function() {
$animate.move(element, null, container2, 'temp-move');
$animate.move(element, null, container2, {
tempClasses: 'temp-move'
});
});
assertTempClass('addClass', 'temp-add', function() {
$animate.addClass(element, 'add', 'temp-add');
$animate.addClass(element, 'add', {
tempClasses: 'temp-add'
});
});
assertTempClass('removeClass', 'temp-remove', function() {
$animate.removeClass(element, 'add', 'temp-remove');
$animate.removeClass(element, 'add', {
tempClasses: 'temp-remove'
});
});
element.addClass('remove');
assertTempClass('setClass', 'temp-set', function() {
$animate.setClass(element, 'add', 'remove', 'temp-set');
$animate.setClass(element, 'add', 'remove', {
tempClasses: 'temp-set'
});
});
assertTempClass('leave', 'temp-leave', function() {
$animate.leave(element, 'temp-leave');
$animate.leave(element, {
tempClasses: 'temp-leave'
});
});
function assertTempClass(event, className, animationOperation) {