fix(ngRepeat): trigger move animation #15072
base: master
Are you sure you want to change the base?
Conversation
ngRepeat does not trigger a move animation for items that come after inserted or deleted items. The documentation says that a move animation occurs "when an adjacent item is filtered out causing a reorder or when the item contents are reordered". With this fix, items that are moved can use an animation, such as changing the border or background color, to show a change. This will fix issue angular#15068.
…mparison operators with type.
src/ng/directive/ngRepeat.js
Outdated
}; | ||
// var getBlockStart = function(block) { | ||
// return block.clone[0]; | ||
// }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please remove this commented-out code?
The previous Travis test failed because of a network issue with saucelabs.com.
src/ng/directive/ngRepeat.js
Outdated
for (var itemKey in collection) { | ||
if (hasOwnProperty.call(collection, itemKey) && itemKey.charAt(0) !== '$') { | ||
collectionKeys.push(itemKey); | ||
for (collectionKey in collection) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this change? Key of the current item makes more sense to me than key of collection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are probably right. I went back and forth on several names and don't remember why I settled on collectionKey. Possibly for consistency in how I named other keys. Let me know if you want me to change it.
Hi @pondermatic, thanks for this PR. This is a much requested feature, and I'd be happy if we can get this in. Since ngRepeat is a performance critical part of the code, we'll have to review it thoroughly, though (and run some benchmarks). |
The diff is pretty ugly because I changed several variable names. I'll work on a version that preserves as many names as possible. |
I've made some tests with the orderBy benchmark (after some fixes), rendering 500 ngRepeated spans from scratch, and found that the new version is about 15% slower than the current master. I'm not sure how much truth is in these numbers, but the difference was consistent (still in the millisecond area, though) The unit tests won't say much about performance as they usually have repeats with small numbers. |
I've made a version that minimizes the changes, but the diff is still hard to follow. It'll probably be better to explain the key changes. I'm going to do some profiling and see if I can speed things up. |
src/ng/directive/ngRepeat.js
Outdated
keyIdentifier, nextBlockMap[nextKey].key, nextLength); | ||
|
||
delete nextBlockMap[nextKey].key; | ||
delete nextBlockMap[nextKey].value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is new, right? Is this necessary? The tests do not fail without this.
If we are doing this for every item, this might actually impact performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous version did not store key and value in nextBlockMap. I am deleting them to keep them from needlessly using memory when nextBlockMap is assigned to lastBlockMap.
The orderby-bp benchmark ran about the same with or without deleting the properties.
I've been running the orderby-bp benchmark, updated by @Narretz today 0784977.
c54f7a9 before pondermatic 263be6b after pondermatic My version is 5.29% slower. :( When I removed the lines that delete the collection item's key and value from nextBlockMap, pass 1 was faster but pass 2 was much slower. Either way, the last collection is retained in memory which is not good. Then I put the key/value in its own array so that deleting is unnecessary. I also tried an array of just the keys, which should use less memory than 263be6b, especially if the values are large objects. The mean of those benchmark passes was 1111.31. I plan on committing revised code with this memory tweak and another tweak. |
In the ngRepeatAction function: * The collection keys and values are accessed directly instead of copying them to nextBlockMap temporarily. * Checking the collectionIsLikeArray boolean value is faster than comparing value and type of collectionKeys and collection.
…angular.js into ngRepeat-animations
var getBlockStart = function(block) { | ||
return block.clone[0]; | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getBlockStart function is no longer used.
Commit 7a98172 ran the orderby-bp benchmark in 1037.09 ms for pass 1 and 1079.71 ms for pass 2. ngRepeat is now as fast as or a little bit faster than it was before. |
Hi @Narretz. Thank you for catching that bug! Your fix and unit test work perfectly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, except a few nitpicks. I want someone else from the team to look at this before merging as ngRepeat is an important directive.
expect(item.event).toBe('leave'); | ||
expect(item.element.text()).toBe('2'); | ||
}) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please put a newline between the two it
s
test/ng/directive/ngRepeatSpec.js
Outdated
$rootScope.items = [items[0], items[1], items[3]]; | ||
$rootScope.$digest(); | ||
|
||
// The leaving item should maintain it's position until it is removed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's -> its (I know, I added this test ;))
Here's a plunker with ngRepeat and the fixed version to play around with: http://plnkr.co/edit/fu2SDoNfxCrUZPA0v0zm?p=preview |
test/ng/directive/ngRepeatSpec.js
Outdated
if (item.event === 'leave') { | ||
break; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A comment to explain why this is needed would be helpful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am going to revert this code. The ngRepeat tests assume the animation queue is in a certain order. During refactoring of ngRepeat, the order was different in this test so I altered the test. My latest version of ngRepeat, c969ae2, passes the original version of this test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should tests assume that animation events are queued in a certain order? If not, then perhaps the queue should be tested like this:
// Test each queue item's event type
while ($animate.queue.length) {
item = $animate.queue.shift();
switch (item.element.text()) {
case '2':
expect(item.event).toBe('leave');
break;
case '3':
expect(item.event).toBe('move');
break;
default:
expect(item).toBeUndefined();
}
}
Maybe we should add a Jasmine matcher in the "ngRepeat Animations" block that compares item.event and item.text() values between two arrays? Should this be new GitHub issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apparently, the animation queue order IS different than it used to be.
Options:
- use a while loop to search for the leave event, as in commit 2331c42
- shift the queue array a second time, assuming that the leave event is the second item in the queue
- add a Jasmine matcher that searches the queue for a item with a specific element text and event
As far as I can tell, tests should not assume the order of events in the queue. So I propose the following matcher be added to the 'ngRepeat animations' group of tests:
beforeEach(function() {
jasmine.addMatchers({
toContainQueueItem: function() {
return {
compare: generateCompare(false),
negativeCompare: generateCompare(true)
};
function generateCompare(isNot) {
/**
* Matcher that checks that the expected element text is in the actual Array of
* animation queue items and that the event matches.
* @param {array} actual
* @param {string} expectedElementText
* @param {string} expectedEvent optional if isNot = true
* @returns {{pass: boolean, message: message}}
*/
return function(actual, expectedElementText, expectedEvent) {
if (expectedEvent === undefined) {
expectedEvent = '';
}
var actualLength = actual.length;
var i;
var item;
var message = valueFn(
'Expected animation queue to ' + (isNot ? 'not ' : '') + 'contain an item where the element\'s text is "'
+ expectedElementText + '"' + (isNot ? '' : ' and the event is "' + expectedEvent + '"')
);
var pass = isNot;
for (i = 0; i < actualLength; i++) {
item = actual[i];
if (item.element.text() === expectedElementText) {
pass = item.event === expectedEvent;
break;
}
}
return {'pass': pass, 'message': message};
};
}
}
});
});
The tests should use the matcher like this:
expect($animate.queue).not.toContainQueueItem('1');
expect($animate.queue).toContainQueueItem('2', 'leave');
expect($animate.queue).toContainQueueItem('3', 'move');
$animate.queue = [];
What does everyone think?
test/ng/directive/ngRepeatSpec.js
Outdated
}) | ||
); | ||
|
||
it('should fire off the move animation for filtered items', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This description could be more clear. Perhaps:
"should fire the move animation for items that shifted due to removal of previous items"
Or something like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about this description?
should fire off the move animation for items that change position when other items are filtered out
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should the tests be checking the previousNode
value that is passed to the move animation?
Generally looks like a good refactoring. Thanks @pondermatic |
In the 'should fire off the leave animation' test, revert change in how the animation queue is shifted to find item '2'. At one time during the refactoring of ngRepeat, the order of events in the queue changed. Improve title of the spec that checks for 'move' animation events when filtering items changes other items positions.
Improve title of the spec that checks for 'move' animation events when filtering items changes other items positions. Remove unused item variable in two tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM - except for the failing tests :-)
The 'ngRepeat animations' test group should not assume the order of items in the animation queue. This matcher searches the animation queue for an item with an expected element text and event.
I can't believe I pushed that change without running tests. :) |
@@ -1424,15 +1424,58 @@ describe('ngRepeat animations', function() { | |||
}; | |||
})); | |||
|
|||
beforeEach(function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests should not rely on the order of items in the animation queue. This Jasmine matcher looks for a queued item with a given element text and matches against that item's event.
I decided to do some more tests, and it looks like we created a performance problem with the new behavior when ngAnimate is used. See this updated plnkr: http://plnkr.co/edit/fu2SDoNfxCrUZPA0v0zm?p=preview The second repeat renders 10000 elements (I know that you generally should avoid these huge lists)
@matsko had done some interesting changes with regard to caching animation styles here: #14166 With ngAnimate enabled and no actual animation styles, the whole process is aborted earlier with this PR, but there is no noticable performance gain. So the question is, are we okay with the perf dropoff with large repeats? There are many ways to disable animations on perf critical parts of the page, but this might still bite people whose apps will suddenly perform worse. I suggest we should at least look into making ngAnimate faster when no actual css animations are present. Consistenly slow is checking when animations are allowed, because we have to traverse the node parent again again and again: angular.js/src/ngAnimate/animateQueue.js Line 621 in f403925
angular.js/src/ngAnimate/animation.js Line 51 in f403925
|
What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
bug fix
What is the current behavior? (You can also link to an open issue here)
#15068