diff options
| author | Matias Niemelä | 2013-10-07 14:35:24 -0400 | 
|---|---|---|
| committer | Misko Hevery | 2013-10-10 17:35:36 -0700 | 
| commit | 23c698821f41e7c7e46a5898e29ac0515041bedc (patch) | |
| tree | 9d1610f065277e79dab237facdd2af2646fa94fc | |
| parent | 3f31a7c7691993893f0724076816f6558643bd91 (diff) | |
| download | angular.js-23c698821f41e7c7e46a5898e29ac0515041bedc.tar.bz2 | |
refactor($animate): queue all successive animations to use only one reflow
| -rw-r--r-- | src/ngAnimate/animate.js | 29 | ||||
| -rw-r--r-- | test/ngAnimate/animateSpec.js | 89 | 
2 files changed, 98 insertions, 20 deletions
| diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 9b09cad2..fc865a65 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -569,7 +569,7 @@ angular.module('ngAnimate', ['ng'])        }      }]); -    $animateProvider.register('', ['$window', '$sniffer', function($window, $sniffer) { +    $animateProvider.register('', ['$window', '$sniffer', '$timeout', function($window, $sniffer, $timeout) {        var forEach = angular.forEach;        // Detect proper transitionend/animationend event names. @@ -605,6 +605,19 @@ angular.module('ngAnimate', ['ng'])            animationIterationCountKey = 'IterationCount',            ELEMENT_NODE = 1; +      var animationReflowQueue = [], animationTimer, timeOut = false; +      function afterReflow(callback) { +        animationReflowQueue.push(callback); +        $timeout.cancel(animationTimer); +        animationTimer = $timeout(function() { +          angular.forEach(animationReflowQueue, function(fn) { +            fn(); +          }); +          animationReflowQueue = []; +          animationTimer = null; +        }, 10, false);  +      } +        function animate(element, className, done) {          if(['ng-enter','ng-leave','ng-move'].indexOf(className) == -1) {            var existingDuration = 0; @@ -670,13 +683,15 @@ angular.module('ngAnimate', ['ng'])            });            // This triggers a reflow which allows for the transition animation to kick in. -          element.prop('clientWidth'); -          if(transitionDuration > 0) { -            node.style[transitionProp + propertyKey] = ''; -          } -          element.addClass(activeClassName); -            var css3AnimationEvents = animationendEvent + ' ' + transitionendEvent; + +          afterReflow(function() { +            if(transitionDuration > 0) { +              node.style[transitionProp + propertyKey] = ''; +            } +            element.addClass(activeClassName); +          }); +            element.on(css3AnimationEvents, onAnimationProgress);            // This will automatically be called by $animate so diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 5e53c585..b51a3584 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -127,7 +127,7 @@ describe("ngAnimate", function() {        })        it("should animate the enter animation event", -        inject(function($animate, $rootScope, $sniffer) { +        inject(function($animate, $rootScope, $sniffer, $timeout) {          element[0].removeChild(child[0]);          expect(element.contents().length).toBe(0); @@ -135,6 +135,7 @@ describe("ngAnimate", function() {          $rootScope.$digest();          if($sniffer.transitions) { +          $timeout.flush();            expect(child.hasClass('ng-enter')).toBe(true);            expect(child.hasClass('ng-enter-active')).toBe(true);            browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 }); @@ -144,13 +145,14 @@ describe("ngAnimate", function() {        }));        it("should animate the leave animation event", -        inject(function($animate, $rootScope, $sniffer) { +        inject(function($animate, $rootScope, $sniffer, $timeout) {          expect(element.contents().length).toBe(1);          $animate.leave(child);          $rootScope.$digest();          if($sniffer.transitions) { +          $timeout.flush();            expect(child.hasClass('ng-leave')).toBe(true);            expect(child.hasClass('ng-leave-active')).toBe(true);            browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 }); @@ -160,7 +162,7 @@ describe("ngAnimate", function() {        }));        it("should animate the move animation event", -        inject(function($animate, $compile, $rootScope) { +        inject(function($animate, $compile, $rootScope, $timeout, $sniffer) {          $rootScope.$digest();          element.html(''); @@ -172,17 +174,21 @@ describe("ngAnimate", function() {          expect(element.text()).toBe('12');          $animate.move(child1, element, child2);          $rootScope.$digest(); +        if($sniffer.transitions) { +          $timeout.flush(); +        }          expect(element.text()).toBe('21');        }));        it("should animate the show animation event", -        inject(function($animate, $rootScope, $sniffer) { +        inject(function($animate, $rootScope, $sniffer, $timeout) {          $rootScope.$digest();          child.addClass('ng-hide');          expect(child).toBeHidden();          $animate.removeClass(child, 'ng-hide');          if($sniffer.transitions) { +          $timeout.flush();            expect(child.hasClass('ng-hide-remove')).toBe(true);            expect(child.hasClass('ng-hide-remove-active')).toBe(true);            browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 }); @@ -193,12 +199,13 @@ describe("ngAnimate", function() {        }));        it("should animate the hide animation event", -        inject(function($animate, $rootScope, $sniffer) { +        inject(function($animate, $rootScope, $sniffer, $timeout) {          $rootScope.$digest();          expect(child).toBeShown();          $animate.addClass(child, 'ng-hide');          if($sniffer.transitions) { +          $timeout.flush();            expect(child.hasClass('ng-hide-add')).toBe(true);            expect(child.hasClass('ng-hide-add-active')).toBe(true);            browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 }); @@ -207,7 +214,7 @@ describe("ngAnimate", function() {        }));        it("should assign the ng-event className to all animation events when transitions/keyframes are used", -        inject(function($animate, $sniffer, $rootScope) { +        inject(function($animate, $sniffer, $rootScope, $timeout) {          if (!$sniffer.transitions) return; @@ -217,6 +224,7 @@ describe("ngAnimate", function() {          //enter          $animate.enter(child, element);          $rootScope.$digest(); +        $timeout.flush();          expect(child.attr('class')).toContain('ng-enter');          expect(child.attr('class')).toContain('ng-enter-active'); @@ -226,6 +234,7 @@ describe("ngAnimate", function() {          element.append(after);          $animate.move(child, element, after);          $rootScope.$digest(); +        $timeout.flush();          expect(child.attr('class')).toContain('ng-move');          expect(child.attr('class')).toContain('ng-move-active'); @@ -233,12 +242,14 @@ describe("ngAnimate", function() {          //hide          $animate.addClass(child, 'ng-hide'); +        $timeout.flush();          expect(child.attr('class')).toContain('ng-hide-add');          expect(child.attr('class')).toContain('ng-hide-add-active');          browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 });          //show          $animate.removeClass(child, 'ng-hide'); +        $timeout.flush();          expect(child.attr('class')).toContain('ng-hide-remove');          expect(child.attr('class')).toContain('ng-hide-remove-active');          browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 }); @@ -246,6 +257,7 @@ describe("ngAnimate", function() {          //leave          $animate.leave(child);          $rootScope.$digest(); +        $timeout.flush();          expect(child.attr('class')).toContain('ng-leave');          expect(child.attr('class')).toContain('ng-leave-active');          browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 }); @@ -283,6 +295,7 @@ describe("ngAnimate", function() {          $animate.leave(child);          $rootScope.$digest(); +        $timeout.flush();          expect(child).toBeHidden(); //hides instantly          //lets change this to prove that done doesn't fire anymore for the previous hide() operation @@ -507,7 +520,7 @@ describe("ngAnimate", function() {          }));          it("should finish the previous animation when a new animation is started", -          inject(function($animate, $rootScope, $compile, $sniffer) { +          inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {              var style = '-webkit-animation: some_animation 2s linear 0s 1 alternate;' +                                  'animation: some_animation 2s linear 0s 1 alternate;'; @@ -518,7 +531,9 @@ describe("ngAnimate", function() {              element.addClass('custom');              $animate.removeClass(element, 'ng-hide'); +              if($sniffer.animations) { +              $timeout.flush();                expect(element.hasClass('ng-hide-remove')).toBe(true);                expect(element.hasClass('ng-hide-remove-active')).toBe(true);              } @@ -528,6 +543,7 @@ describe("ngAnimate", function() {              if($sniffer.animations) { //cleanup some pending animations +              $timeout.flush();                expect(element.hasClass('ng-hide-add')).toBe(true);                expect(element.hasClass('ng-hide-add-active')).toBe(true);                browserTrigger(element,'animationend', { timeStamp: Date.now() + 2000, elapsedTime: 2000 }); @@ -648,7 +664,7 @@ describe("ngAnimate", function() {          }));          it("should finish the previous transition when a new animation is started", -          inject(function($animate, $rootScope, $compile, $sniffer) { +          inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {              var style = '-webkit-transition: 1s linear all;' +                                  'transition: 1s linear all;'; @@ -659,7 +675,9 @@ describe("ngAnimate", function() {              element.addClass('ng-hide');              $animate.removeClass(element, 'ng-hide'); +              if($sniffer.transitions) { +              $timeout.flush();                expect(element.hasClass('ng-hide-remove')).toBe(true);                expect(element.hasClass('ng-hide-remove-active')).toBe(true);                browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 }); @@ -669,7 +687,9 @@ describe("ngAnimate", function() {              expect(element).toBeShown();              $animate.addClass(element, 'ng-hide'); +              if($sniffer.transitions) { +              $timeout.flush();                expect(element.hasClass('ng-hide-add')).toBe(true);                expect(element.hasClass('ng-hide-add-active')).toBe(true);              } @@ -696,6 +716,7 @@ describe("ngAnimate", function() {          $rootScope.$digest();          if ($sniffer.transitions) { +          $timeout.flush();            expect(element.hasClass('abc')).toBe(true);            expect(element.hasClass('ng-enter')).toBe(true);            expect(element.hasClass('ng-enter-active')).toBe(true); @@ -708,6 +729,7 @@ describe("ngAnimate", function() {          $rootScope.$digest();          if ($sniffer.transitions) { +          $timeout.flush();            expect(element.hasClass('xyz')).toBe(true);            expect(element.hasClass('ng-enter')).toBe(true);            expect(element.hasClass('ng-enter-active')).toBe(true); @@ -717,7 +739,7 @@ describe("ngAnimate", function() {        }));        it('should only append active to the newly append CSS className values', -        inject(function($animate, $rootScope, $sniffer, $rootElement) { +        inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout) {          ss.addRule('.ng-enter', '-webkit-transition:9s linear all;' +                                          'transition:9s linear all;'); @@ -733,7 +755,9 @@ describe("ngAnimate", function() {          $animate.enter(element, parent);          $rootScope.$digest(); +          if($sniffer.transitions) { +          $timeout.flush();            expect(element.hasClass('one')).toBe(true);            expect(element.hasClass('two')).toBe(true);            expect(element.hasClass('ng-enter')).toBe(true); @@ -1013,7 +1037,9 @@ describe("ngAnimate", function() {          $animate.addClass(element,'klass', function() {            signature += '1';          }); +          if($sniffer.transitions) { +          $timeout.flush();            expect(element.hasClass('klass')).toBe(false);            expect(element.hasClass('klass-add')).toBe(true);            expect(element.hasClass('klass-add-active')).toBe(true); @@ -1026,6 +1052,7 @@ describe("ngAnimate", function() {          });          if($sniffer.transitions) { +          $timeout.flush();            expect(element.hasClass('klass')).toBe(true);            expect(element.hasClass('klass-add')).toBe(false);            expect(element.hasClass('klass-add-active')).toBe(false); @@ -1086,7 +1113,9 @@ describe("ngAnimate", function() {          $animate.addClass(element,'klass', function() {            signature += 'd';          }); +          if($sniffer.transitions) { +          $timeout.flush();            expect(element.hasClass('klass-add')).toBe(true);            expect(element.hasClass('klass-add-active')).toBe(true);            browserTrigger(element,'transitionend', { timeStamp: Date.now() + 11000, elapsedTime: 11000 }); @@ -1100,7 +1129,9 @@ describe("ngAnimate", function() {          $animate.removeClass(element,'klass', function() {            signature += 'b';          }); +          if($sniffer.transitions) { +          $timeout.flush();            expect(element.hasClass('klass-remove')).toBe(true);            expect(element.hasClass('klass-remove-active')).toBe(true);            browserTrigger(element,'transitionend', { timeStamp: Date.now() + 11000, elapsedTime: 11000 }); @@ -1133,6 +1164,7 @@ describe("ngAnimate", function() {          });          if($sniffer.transitions) { +          $timeout.flush();            expect(element.hasClass('one-add')).toBe(true);            expect(element.hasClass('two-add')).toBe(true); @@ -1177,6 +1209,7 @@ describe("ngAnimate", function() {          });          if($sniffer.transitions) { +          $timeout.flush();            expect(element.hasClass('one-remove')).toBe(true);            expect(element.hasClass('two-remove')).toBe(true); @@ -1217,7 +1250,7 @@ describe("ngAnimate", function() {    }    it("should properly animate and parse CSS3 transitions", -    inject(function($compile, $rootScope, $animate, $sniffer) { +    inject(function($compile, $rootScope, $animate, $sniffer, $timeout) {      ss.addRule('.ng-enter', '-webkit-transition:1s linear all;' +                                      'transition:1s linear all;'); @@ -1229,6 +1262,7 @@ describe("ngAnimate", function() {      $rootScope.$digest();      if($sniffer.transitions) { +      $timeout.flush();        expect(child.hasClass('ng-enter')).toBe(true);        expect(child.hasClass('ng-enter-active')).toBe(true);        browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 }); @@ -1239,7 +1273,7 @@ describe("ngAnimate", function() {    }));    it("should properly animate and parse CSS3 animations", -    inject(function($compile, $rootScope, $animate, $sniffer) { +    inject(function($compile, $rootScope, $animate, $sniffer, $timeout) {      ss.addRule('.ng-enter', '-webkit-animation: some_animation 4s linear 1s 2 alternate;' +                                      'animation: some_animation 4s linear 1s 2 alternate;'); @@ -1251,6 +1285,7 @@ describe("ngAnimate", function() {      $rootScope.$digest();      if($sniffer.transitions) { +      $timeout.flush();        expect(child.hasClass('ng-enter')).toBe(true);        expect(child.hasClass('ng-enter-active')).toBe(true);        browserTrigger(child,'transitionend', { timeStamp: Date.now() + 9000, elapsedTime: 9000 }); @@ -1260,7 +1295,7 @@ describe("ngAnimate", function() {    }));    it("should not set the transition property flag if only CSS animations are used", -    inject(function($compile, $rootScope, $animate, $sniffer) { +    inject(function($compile, $rootScope, $animate, $sniffer, $timeout) {      if (!$sniffer.animations) return; @@ -1278,6 +1313,7 @@ describe("ngAnimate", function() {      $animate.enter(child, element);      $rootScope.$digest(); +    $timeout.flush();      browserTrigger(child,'transitionend', { timeStamp: Date.now() + 2000, elapsedTime: 2000 }); @@ -1369,6 +1405,7 @@ describe("ngAnimate", function() {        //this is added/removed right away otherwise        if($sniffer.transitions) { +        $timeout.flush();          expect(child.hasClass('ng-enter')).toBe(true);          expect(child.hasClass('ng-enter-active')).toBe(true);        } @@ -1377,6 +1414,7 @@ describe("ngAnimate", function() {        child.addClass('usurper');        $animate.leave(child);        $rootScope.$digest(); +      $timeout.flush();        expect(child.hasClass('ng-enter')).toBe(false);        expect(child.hasClass('ng-enter-active')).toBe(false); @@ -1561,6 +1599,7 @@ describe("ngAnimate", function() {      $animate.enter(child, element);      $rootScope.$digest(); +    $timeout.flush();      expect(child.hasClass('ng-enter')).toBe(true);      expect(child.hasClass('ng-enter-active')).toBe(true); @@ -1619,6 +1658,7 @@ describe("ngAnimate", function() {        expect(animationState).toBe('enter');        if($sniffer.transitions) {          expect(child.hasClass('ng-enter')).toBe(true); +        $timeout.flush();          expect(child.hasClass('ng-enter-active')).toBe(true);        } @@ -1630,12 +1670,12 @@ describe("ngAnimate", function() {        expect(animationState).toBe('enter-cancel');        $rootScope.$digest(); -      $timeout.flush();        $animate.addClass(child, 'something');        expect(animationState).toBe('addClass');        if($sniffer.transitions) {          expect(child.hasClass('something-add')).toBe(true); +        $timeout.flush();          expect(child.hasClass('something-add-active')).toBe(true);        } @@ -1648,4 +1688,27 @@ describe("ngAnimate", function() {      });    }); +  it("should wait until a queue of animations are complete before performing a reflow", +    inject(function($rootScope, $compile, $timeout,$sniffer) { + +    if(!$sniffer.transitions) return; + +    $rootScope.items = [1,2,3,4,5]; +    var element = html($compile('<div><div class="animated" ng-repeat="item in items"></div></div>')($rootScope)); + +    ss.addRule('.animated.ng-enter',  '-webkit-transition: width 1s, background 1s 1s;' + +                                              'transition: width 1s, background 1s 1s;'); + +    $rootScope.$digest(); +    expect(element[0].querySelectorAll('.ng-enter-active').length).toBe(0); +    $timeout.flush(); +    expect(element[0].querySelectorAll('.ng-enter-active').length).toBe(5); + +    angular.forEach(element.children(), function(kid) { +      browserTrigger(kid, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1000 }); +    }); + +    expect(element[0].querySelectorAll('.ng-enter-active').length).toBe(0); +  })); +  }); | 
