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

fix(ngAnimate): use requestAnimationFrame to space out child animations #12669

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions angularFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ var angularFiles = {
'ngAnimate': [
'src/ngAnimate/shared.js',
'src/ngAnimate/body.js',
'src/ngAnimate/rafScheduler.js',
'src/ngAnimate/animateChildrenDirective.js',
'src/ngAnimate/animateCss.js',
'src/ngAnimate/animateCssDriver.js',
Expand Down
13 changes: 5 additions & 8 deletions src/ngAnimate/animateCss.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,10 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
var gcsLookup = createLocalCacheLookup();
var gcsStaggerLookup = createLocalCacheLookup();

this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', '$$forceReflow', '$sniffer', '$$rAF', '$animate',
function($window, $$jqLite, $$AnimateRunner, $timeout, $$forceReflow, $sniffer, $$rAF, $animate) {
this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout',
'$$forceReflow', '$sniffer', '$$rAFScheduler', '$animate',
function($window, $$jqLite, $$AnimateRunner, $timeout,
$$forceReflow, $sniffer, $$rAFScheduler, $animate) {

var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);

Expand Down Expand Up @@ -389,12 +391,8 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
var cancelLastRAFRequest;
var rafWaitQueue = [];
function waitUntilQuiet(callback) {
if (cancelLastRAFRequest) {
cancelLastRAFRequest(); //cancels the request
}
rafWaitQueue.push(callback);
cancelLastRAFRequest = $$rAF(function() {
cancelLastRAFRequest = null;
$$rAFScheduler.waitUntilQuiet(function() {
gcsLookup.flush();
gcsStaggerLookup.flush();

Expand Down Expand Up @@ -485,7 +483,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
// there actually is a detected transition or keyframe animation
if (options.applyClassesEarly && addRemoveClassName.length) {
applyAnimationClasses(element, options);
addRemoveClassName = '';
}

var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim();
Expand Down
22 changes: 5 additions & 17 deletions src/ngAnimate/animateCssDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro

var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);

return function initDriverFn(animationDetails, onBeforeClassesAppliedCb) {
return function initDriverFn(animationDetails) {
return animationDetails.from && animationDetails.to
? prepareFromToAnchorAnimation(animationDetails.from,
animationDetails.to,
animationDetails.classes,
animationDetails.anchors)
: prepareRegularAnimation(animationDetails, onBeforeClassesAppliedCb);
: prepareRegularAnimation(animationDetails);
};

function filterCssClasses(classes) {
Expand Down Expand Up @@ -224,21 +224,14 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
};
}

function prepareRegularAnimation(animationDetails, onBeforeClassesAppliedCb) {
function prepareRegularAnimation(animationDetails) {
var element = animationDetails.element;
var options = animationDetails.options || {};

// since the ng-EVENT, class-ADD and class-REMOVE classes are applied inside
// of the animateQueue pre and postDigest stages then there is no need to add
// then them here as well.
options.$$skipPreparationClasses = true;

// during the pre/post digest stages inside of animateQueue we also performed
// the blocking (transition:-9999s) so there is no point in doing that again.
options.skipBlocking = true;

if (animationDetails.structural) {
options.event = animationDetails.event;
options.structural = true;
options.applyClassesEarly = true;

// we special case the leave animation since we want to ensure that
// the element is removed as soon as the animation is over. Otherwise
Expand All @@ -248,11 +241,6 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
}
}

// we apply the classes right away since the pre-digest took care of the
// preparation classes.
onBeforeClassesAppliedCb(element);
applyAnimationClasses(element, options);

// We assign the preparationClasses as the actual animation event since
// the internals of $animateCss will just suffix the event token values
// with `-active` to trigger the animation.
Expand Down
8 changes: 1 addition & 7 deletions src/ngAnimate/animateQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,6 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
return runner;
}

applyGeneratedPreparationClasses(element, isStructural ? event : null, options);
blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);

// the counter keeps track of cancelled animations
var counter = (existingAnimation.counter || 0) + 1;
newAnimation.counter = counter;
Expand Down Expand Up @@ -442,10 +439,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
: animationDetails.event;

markElementAnimationState(element, RUNNING_STATE);
var realRunner = $$animation(element, event, animationDetails.options, function(e) {
$$forceReflow();
blockTransitions(getDomNode(e), false);
});
var realRunner = $$animation(element, event, animationDetails.options);

realRunner.done(function(status) {
close(!status);
Expand Down
32 changes: 15 additions & 17 deletions src/ngAnimate/animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
return element.data(RUNNER_STORAGE_KEY);
}

this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap',
function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap) {
this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap', '$$rAFScheduler',
function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap, $$rAFScheduler) {

var animationQueue = [];
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
Expand Down Expand Up @@ -88,26 +88,27 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
if (remainingLevelEntries <= 0) {
remainingLevelEntries = nextLevelEntries;
nextLevelEntries = 0;
result = result.concat(row);
result.push(row);
row = [];
}
row.push(entry.fn);
forEach(entry.children, function(childEntry) {
entry.children.forEach(function(childEntry) {
nextLevelEntries++;
queue.push(childEntry);
});
remainingLevelEntries--;
}

if (row.length) {
result = result.concat(row);
result.push(row);
}

return result;
}
}

// TODO(matsko): document the signature in a better way
return function(element, event, options, onBeforeClassesAppliedCb) {
return function(element, event, options) {
options = prepareAnimationOptions(options);
var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;

Expand Down Expand Up @@ -159,8 +160,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
// the element was destroyed early on which removed the runner
// form its storage. This means we can't animate this element
// at all and it already has been closed due to destruction.
var elm = entry.element;
if (getRunner(elm) && getDomNode(elm).parentNode) {
if (getRunner(entry.element)) {
animations.push(entry);
} else {
entry.close();
Expand Down Expand Up @@ -191,7 +191,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
: animationEntry.element;

if (getRunner(targetElement)) {
var operation = invokeFirstDriver(animationEntry, onBeforeClassesAppliedCb);
var operation = invokeFirstDriver(animationEntry);
if (operation) {
startAnimationFn = operation.start;
}
Expand All @@ -211,11 +211,9 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
});

// we need to sort each of the animations in order of parent to child
// relationships. This ensures that the parent to child classes are
// applied at the right time.
forEach(sortAnimations(toBeSortedAnimations), function(triggerAnimation) {
triggerAnimation();
});
// relationships. This ensures that the child classes are applied at the
// right time.
$$rAFScheduler(sortAnimations(toBeSortedAnimations));
});

return runner;
Expand Down Expand Up @@ -285,7 +283,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
var lookupKey = from.animationID.toString();
if (!anchorGroups[lookupKey]) {
var group = anchorGroups[lookupKey] = {
// TODO(matsko): double-check this code
structural: true,
beforeStart: function() {
fromAnimation.beforeStart();
toAnimation.beforeStart();
Expand Down Expand Up @@ -339,15 +337,15 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
return matches.join(' ');
}

function invokeFirstDriver(animationDetails, onBeforeClassesAppliedCb) {
function invokeFirstDriver(animationDetails) {
// we loop in reverse order since the more general drivers (like CSS and JS)
// may attempt more elements, but custom drivers are more particular
for (var i = drivers.length - 1; i >= 0; i--) {
var driverName = drivers[i];
if (!$injector.has(driverName)) continue; // TODO(matsko): remove this check

var factory = $injector.get(driverName);
var driver = factory(animationDetails, onBeforeClassesAppliedCb);
var driver = factory(animationDetails);
if (driver) {
return driver;
}
Expand Down
2 changes: 2 additions & 0 deletions src/ngAnimate/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

$$BodyProvider,
$$AnimateAsyncRunFactory,
$$rAFSchedulerFactory,
$$AnimateChildrenDirective,
$$AnimateRunnerFactory,
$$AnimateQueueProvider,
Expand Down Expand Up @@ -744,6 +745,7 @@ angular.module('ngAnimate', [])
.provider('$$body', $$BodyProvider)

.directive('ngAnimateChildren', $$AnimateChildrenDirective)
.factory('$$rAFScheduler', $$rAFSchedulerFactory)

.factory('$$AnimateRunner', $$AnimateRunnerFactory)
.factory('$$animateAsyncRun', $$AnimateAsyncRunFactory)
Expand Down
50 changes: 50 additions & 0 deletions src/ngAnimate/rafScheduler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';

var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) {
var queue, cancelFn;

function scheduler(tasks) {
// we make a copy since RAFScheduler mutates the state
// of the passed in array variable and this would be difficult
// to track down on the outside code
queue = queue.concat(tasks);
nextTick();
}

queue = scheduler.queue = [];

/* waitUntilQuiet does two things:
* 1. It will run the FINAL `fn` value only when an uncancelled RAF has passed through
* 2. It will delay the next wave of tasks from running until the quiet `fn` has run.
*
* The motivation here is that animation code can request more time from the scheduler
* before the next wave runs. This allows for certain DOM properties such as classes to
* be resolved in time for the next animation to run.
*/
scheduler.waitUntilQuiet = function(fn) {
if (cancelFn) cancelFn();

cancelFn = $$rAF(function() {
cancelFn = null;
fn();
nextTick();
});
};

return scheduler;

function nextTick() {
if (!queue.length) return;

var items = queue.shift();
for (var i = 0; i < items.length; i++) {
items[i]();
}

if (!cancelFn) {
$$rAF(function() {
if (!cancelFn) nextTick();
});
}
}
}];
2 changes: 1 addition & 1 deletion test/ngAnimate/animateCssDriverSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ describe("ngAnimate $$animateCssDriver", function() {
expect(capturedAnimation[1].applyClassesEarly).toBeFalsy();

driver({ element: element, structural: true });
expect(capturedAnimation[1].applyClassesEarly).toBeFalsy();
expect(capturedAnimation[1].applyClassesEarly).toBeTruthy();
}));

it("should only set the event value if the animation is structural", inject(function() {
Expand Down
68 changes: 0 additions & 68 deletions test/ngAnimate/animateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -492,74 +492,6 @@ describe("animations", function() {
expect(element).not.toHaveClass('green');
}));

they('should apply the $prop CSS class to the element before digest for the given event and remove when complete',
{'ng-enter': 'enter', 'ng-leave': 'leave', 'ng-move': 'move'}, function(event) {

inject(function($animate, $rootScope, $document, $rootElement) {
$animate.enabled(true);

var element = jqLite('<div></div>');
var parent = jqLite('<div></div>');

$rootElement.append(parent);
jqLite($document[0].body).append($rootElement);

var runner;
if (event === 'leave') {
parent.append(element);
runner = $animate[event](element);
} else {
runner = $animate[event](element, parent);
}

var expectedClassName = 'ng-' + event;

expect(element).toHaveClass(expectedClassName);

$rootScope.$digest();
expect(element).toHaveClass(expectedClassName);

runner.end();
expect(element).not.toHaveClass(expectedClassName);

dealoc(parent);
});
});

they('should add CSS classes with the $prop suffix when depending on the event and remove when complete',
{'-add': 'add', '-remove': 'remove'}, function(event) {

inject(function($animate, $rootScope, $document, $rootElement) {
$animate.enabled(true);

var element = jqLite('<div></div>');

$rootElement.append(element);
jqLite($document[0].body).append($rootElement);

var classes = 'one two';
var expectedClasses = ['one-',event,' ','two-', event].join('');

var runner;
if (event === 'add') {
runner = $animate.addClass(element, classes);
} else {
element.addClass(classes);
runner = $animate.removeClass(element, classes);
}

expect(element).toHaveClass(expectedClasses);

$rootScope.$digest();
expect(element).toHaveClass(expectedClasses);

runner.end();
expect(element).not.toHaveClass(expectedClasses);

dealoc(element);
});
});

they('$prop() should operate using a native DOM element',
['enter', 'move', 'leave', 'addClass', 'removeClass', 'setClass', 'animate'], function(event) {

Expand Down
Loading