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

Commit 7484830

Browse files
committed
feat(ngAnimate): provide support for staggering animations with CSS
1 parent 29e40c1 commit 7484830

File tree

2 files changed

+217
-6
lines changed

2 files changed

+217
-6
lines changed

src/ngAnimate/animate.js

+95-5
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,57 @@
144144
* immediately resulting in a DOM element that is at its final state. This final state is when the DOM element
145145
* has no CSS transition/animation classes applied to it.
146146
*
147+
* <h3>CSS Staggering Animations</h3>
148+
* A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a
149+
* curtain-like effect. The ngAnimate module, as of 1.2.0, supports staggering animations and the stagger effect can be
150+
* performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for
151+
* the animation. The style property expected within the stagger class can either be a **transition-delay** or an
152+
* **animation-delay** property (or both if your animation contains both transitions and keyframe animations).
153+
*
154+
* <pre>
155+
* .my-animation.ng-enter {
156+
* /&#42; standard transition code &#42;/
157+
* }
158+
* .my-animation.ng-enter-stagger {
159+
* /&#42; this will have a 100ms delay between each successive leave animation &#42;/
160+
* -webkit-transition-delay: 0.1s;
161+
* transition-delay: 0.1s;
162+
*
163+
* /&#42; in case the stagger doesn't work then these two values
164+
* must be set to 0 to avoid an accidental CSS inheritance &#42;/
165+
* -webkit-transition-duration: 0s;
166+
* transition-duration: 0s;
167+
* }
168+
* .my-animation.ng-enter.ng-enter-active {
169+
* /&#42; standard transition styles &#42;/
170+
* }
171+
* </pre>
172+
*
173+
* Staggering animations work by default in ngRepeat (so long as the CSS class is defiend). Outside of ngRepeat, to use staggering animations
174+
* on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this
175+
* are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation
176+
* will also be reset if more than 10ms has passed after the last animation has been fired.
177+
*
178+
* The following code will issue the **ng-leave-stagger** event on the element provided:
179+
*
180+
* <pre>
181+
* var kids = parent.children();
182+
*
183+
* $animate.leave(kids[0]); //stagger index=0
184+
* $animate.leave(kids[1]); //stagger index=1
185+
* $animate.leave(kids[2]); //stagger index=2
186+
* $animate.leave(kids[3]); //stagger index=3
187+
* $animate.leave(kids[4]); //stagger index=4
188+
*
189+
* $timeout(function() {
190+
* //stagger has reset itself
191+
* $animate.leave(kids[5]); //stagger index=0
192+
* $animate.leave(kids[6]); //stagger index=1
193+
* }, 100, false);
194+
* </pre>
195+
*
196+
* Stagger animations are currently only supported within CSS-defined animations.
197+
*
147198
* <h2>JavaScript-defined Animations</h2>
148199
* In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations on browsers that do not
149200
* yet support CSS transitions/animations, then you can make use of JavaScript animations defined inside of your AngularJS module.
@@ -672,7 +723,7 @@ angular.module('ngAnimate', ['ng'])
672723
var forEach = angular.forEach;
673724

674725
// Detect proper transitionend/animationend event names.
675-
var transitionProp, transitionendEvent, animationProp, animationendEvent;
726+
var prefix = '', transitionProp, transitionendEvent, animationProp, animationendEvent;
676727

677728
// If unprefixed events are not supported but webkit-prefixed are, use the latter.
678729
// Otherwise, just use W3C names, browsers not supporting them at all will just ignore them.
@@ -683,6 +734,7 @@ angular.module('ngAnimate', ['ng'])
683734
// Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit
684735
// therefore there is no reason to test anymore for other vendor prefixes: http://caniuse.com/#search=transition
685736
if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) {
737+
prefix = '-webkit-';
686738
transitionProp = 'WebkitTransition';
687739
transitionendEvent = 'webkitTransitionEnd transitionend';
688740
} else {
@@ -691,6 +743,7 @@ angular.module('ngAnimate', ['ng'])
691743
}
692744

693745
if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) {
746+
prefix = '-webkit-';
694747
animationProp = 'WebkitAnimation';
695748
animationendEvent = 'webkitAnimationEnd animationend';
696749
} else {
@@ -722,6 +775,13 @@ angular.module('ngAnimate', ['ng'])
722775
}, 10, false);
723776
}
724777

778+
function applyStyle(node, style) {
779+
var oldStyle = node.getAttribute('style') || '';
780+
var newStyle = (oldStyle.length > 0 ? '; ' : '') + style;
781+
node.setAttribute('style', newStyle);
782+
return oldStyle;
783+
}
784+
725785
function getElementAnimationDetails(element, cacheKey, onlyCheckTransition) {
726786
var data = cacheKey ? lookupCache[cacheKey] : null;
727787
if(!data) {
@@ -751,6 +811,7 @@ angular.module('ngAnimate', ['ng'])
751811
}
752812
});
753813
data = {
814+
total : 0,
754815
transitionDelay : transitionDelay,
755816
animationDelay : animationDelay,
756817
transitionDuration : transitionDuration,
@@ -782,17 +843,32 @@ angular.module('ngAnimate', ['ng'])
782843
}
783844

784845
function animate(element, className, done) {
785-
786846
var cacheKey = getCacheKey(element);
787847
if(getElementAnimationDetails(element, cacheKey, true).transitionDuration > 0) {
788848

789849
done();
790850
return;
791851
}
792852

853+
var eventCacheKey = cacheKey + ' ' + className;
854+
var ii = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0;
855+
856+
var stagger = {};
857+
if(ii > 0) {
858+
var staggerClassName = className + '-stagger';
859+
var staggerCacheKey = cacheKey + ' ' + staggerClassName;
860+
var applyClasses = !lookupCache[staggerCacheKey];
861+
862+
applyClasses && element.addClass(staggerClassName);
863+
864+
stagger = getElementAnimationDetails(element, staggerCacheKey);
865+
866+
applyClasses && element.removeClass(staggerClassName);
867+
}
868+
793869
element.addClass(className);
794870

795-
var timings = getElementAnimationDetails(element, cacheKey + ' ' + className);
871+
var timings = getElementAnimationDetails(element, eventCacheKey);
796872

797873
/* there is no point in performing a reflow if the animation
798874
timeout is empty (this would cause a flicker bug normally
@@ -815,12 +891,21 @@ angular.module('ngAnimate', ['ng'])
815891
activeClassName += (i > 0 ? ' ' : '') + klass + '-active';
816892
});
817893

818-
// This triggers a reflow which allows for the transition animation to kick in.
819-
var css3AnimationEvents = animationendEvent + ' ' + transitionendEvent;
894+
var formerStyle, css3AnimationEvents = animationendEvent + ' ' + transitionendEvent;
820895

896+
// This triggers a reflow which allows for the transition animation to kick in.
821897
afterReflow(function() {
822898
if(timings.transitionDuration > 0) {
823899
node.style[transitionProp + propertyKey] = '';
900+
if(ii > 0 && stagger.transitionDelay > 0 && stagger.transitionDuration === 0) {
901+
formerStyle = applyStyle(node, prefix + 'transition-delay: ' +
902+
(ii * stagger.transitionDelay + timings.transitionDelay) + 's');
903+
}
904+
}
905+
906+
if(ii > 0 && stagger.animationDelay > 0 && stagger.animationDuration === 0) {
907+
formerStyle = applyStyle(node, prefix + 'animation-delay: ' +
908+
(ii * stagger.animationDelay + timings.animationDelay) + 's');
824909
}
825910
element.addClass(activeClassName);
826911
});
@@ -836,6 +921,11 @@ angular.module('ngAnimate', ['ng'])
836921
element.removeClass(className);
837922
element.removeClass(activeClassName);
838923
element.removeData(NG_ANIMATE_CLASS_KEY);
924+
if(formerStyle != null) {
925+
formerStyle.length > 0 ?
926+
node.setAttribute('style', formerStyle) :
927+
node.removeAttribute('style');
928+
}
839929

840930
// Only when the animation is cancelled is the done()
841931
// function not called for this animation therefore

test/ngAnimate/animateSpec.js

+122-1
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,66 @@ describe("ngAnimate", function() {
642642

643643
expect(element.hasClass('ng-hide-remove-active')).toBe(false);
644644
}));
645+
646+
it("should stagger the items when the correct CSS class is provided",
647+
inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) {
648+
649+
if(!$sniffer.animations) return;
650+
651+
$animate.enabled(true);
652+
653+
ss.addRule('.ani.ng-enter, .ani.ng-leave, .ani-fake.ng-enter, .ani-fake.ng-leave',
654+
'-webkit-animation:1s my_animation;' +
655+
'transition:1s my_animation;');
656+
657+
ss.addRule('.ani.ng-enter-stagger, .ani.ng-leave-stagger',
658+
'-webkit-animation-delay:0.1s;' +
659+
'-webkit-animation-duration:0s;' +
660+
'animation-delay:0.1s;' +
661+
'animation-duration:0s;');
662+
663+
ss.addRule('.ani-fake.ng-enter-stagger, .ani-fake.ng-leave-stagger',
664+
'-webkit-animation-delay:0.1s;' +
665+
'-webkit-animation-duration:1s;' +
666+
'animation-delay:0.1s;' +
667+
'animation-duration:1s;');
668+
669+
var container = $compile(html('<div></div>'))($rootScope);
670+
671+
var elements = [];
672+
for(var i = 0; i < 5; i++) {
673+
var newScope = $rootScope.$new();
674+
var element = $compile('<div class="ani"></div>')(newScope);
675+
$animate.enter(element, container);
676+
elements.push(element);
677+
};
678+
679+
$rootScope.$digest();
680+
$timeout.flush();
681+
682+
expect(elements[0].attr('style')).toBeFalsy();
683+
expect(elements[1].attr('style')).toMatch(/animation-delay: 0\.1\d*s/);
684+
expect(elements[2].attr('style')).toMatch(/animation-delay: 0\.2\d*s/);
685+
expect(elements[3].attr('style')).toMatch(/animation-delay: 0\.3\d*s/);
686+
expect(elements[4].attr('style')).toMatch(/animation-delay: 0\.4\d*s/);
687+
688+
for(var i = 0; i < 5; i++) {
689+
dealoc(elements[i]);
690+
var newScope = $rootScope.$new();
691+
var element = $compile('<div class="ani-fake"></div>')(newScope);
692+
$animate.enter(element, container);
693+
elements[i] = element;
694+
};
695+
696+
$rootScope.$digest();
697+
$timeout.flush();
698+
699+
expect(elements[0].attr('style')).toBeFalsy();
700+
expect(elements[1].attr('style')).not.toMatch(/animation-delay: 0\.1\d*s/);
701+
expect(elements[2].attr('style')).not.toMatch(/animation-delay: 0\.2\d*s/);
702+
expect(elements[3].attr('style')).not.toMatch(/animation-delay: 0\.3\d*s/);
703+
expect(elements[4].attr('style')).not.toMatch(/animation-delay: 0\.4\d*s/);
704+
}));
645705
});
646706

647707
describe("Transitions", function() {
@@ -785,6 +845,66 @@ describe("ngAnimate", function() {
785845
expect(element.hasClass('ng-hide-add-active')).toBe(true);
786846
}
787847
}));
848+
849+
it("should stagger the items when the correct CSS class is provided",
850+
inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement) {
851+
852+
if(!$sniffer.transitions) return;
853+
854+
$animate.enabled(true);
855+
856+
ss.addRule('.ani.ng-enter, .ani.ng-leave, .ani-fake.ng-enter, .ani-fake.ng-leave',
857+
'-webkit-transition:1s linear all;' +
858+
'transition:1s linear all;');
859+
860+
ss.addRule('.ani.ng-enter-stagger, .ani.ng-leave-stagger',
861+
'-webkit-transition-delay:0.1s;' +
862+
'-webkit-transition-duration:0s;' +
863+
'transition-delay:0.1s;' +
864+
'transition-duration:0s;');
865+
866+
ss.addRule('.ani-fake.ng-enter-stagger, .ani-fake.ng-leave-stagger',
867+
'-webkit-transition-delay:0.1s;' +
868+
'-webkit-transition-duration:1s;' +
869+
'transition-delay:0.1s;' +
870+
'transition-duration:1s;');
871+
872+
var container = $compile(html('<div></div>'))($rootScope);
873+
874+
var elements = [];
875+
for(var i = 0; i < 5; i++) {
876+
var newScope = $rootScope.$new();
877+
var element = $compile('<div class="ani"></div>')(newScope);
878+
$animate.enter(element, container);
879+
elements.push(element);
880+
};
881+
882+
$rootScope.$digest();
883+
$timeout.flush();
884+
885+
expect(elements[0].attr('style')).toBeFalsy();
886+
expect(elements[1].attr('style')).toMatch(/transition-delay: 0\.1\d*s/);
887+
expect(elements[2].attr('style')).toMatch(/transition-delay: 0\.2\d*s/);
888+
expect(elements[3].attr('style')).toMatch(/transition-delay: 0\.3\d*s/);
889+
expect(elements[4].attr('style')).toMatch(/transition-delay: 0\.4\d*s/);
890+
891+
for(var i = 0; i < 5; i++) {
892+
dealoc(elements[i]);
893+
var newScope = $rootScope.$new();
894+
var element = $compile('<div class="ani-fake"></div>')(newScope);
895+
$animate.enter(element, container);
896+
elements[i] = element;
897+
};
898+
899+
$rootScope.$digest();
900+
$timeout.flush();
901+
902+
expect(elements[0].attr('style')).toBeFalsy();
903+
expect(elements[1].attr('style')).not.toMatch(/transition-delay: 0\.1\d*s/);
904+
expect(elements[2].attr('style')).not.toMatch(/transition-delay: 0\.2\d*s/);
905+
expect(elements[3].attr('style')).not.toMatch(/transition-delay: 0\.3\d*s/);
906+
expect(elements[4].attr('style')).not.toMatch(/transition-delay: 0\.4\d*s/);
907+
}));
788908
});
789909
});
790910

@@ -2008,7 +2128,8 @@ describe("ngAnimate", function() {
20082128
$rootScope.$digest();
20092129
$timeout.flush();
20102130

2011-
expect(count).toBe(2);
2131+
//called three times since the classname is the same
2132+
expect(count).toBe(3);
20122133

20132134
dealoc(element);
20142135
count = 0;

0 commit comments

Comments
 (0)