The problem
Achieving what you want can be a bit tricky.
A common attempt is to use ng-style
to calculate the element′s position based on its index in the list:
<div ng-repeat="c in countries | orderBy:q" ng-style="{ 'top': $index * 20 + 'px' }">
Demo: http://plnkr.co/edit/anv4fIrMxVDWuov6K3sw?p=preview
The problem is that only some elements are animated, and only towards the bottom.
Why is that?
Consider the following list sorted by name (similar to the one from the demo above):
- 2 - Denmark
- 3 - Norway
- 1 - Sweden
When you sort this list by id instead only one element will move - Sweden from bottom to top. What actually happens is that the Sweden element is removed from the DOM and inserted again at its new position. However, when an element is inserted into the DOM the associated CSS transtions will normally not occur (I say normally as it ultimately depends on how the browser in question is implemented).
The other two elements remain in the DOM, get new top
positions and their transitions are animated.
So with this strategy the transitions are only animated for the elements that didn't actually move in the DOM.
Another strategy is to include the ngAnimate module and use that CSS class ng-move
. Almost all examples of animated ng-repeats use this.
However, this will not work because of two reasons:
The ng-move
class would only be applied to the elements that move (so only to the Sweden element in the example above)
The ng-move
class is applied to the element after it has been inserted into its new position in the DOM. You can have CSS that says "animate from opacity 0 to 1", but you can't have "animate from old position to new" since the old position is not known and each element would have to move a different distance.
A solution
A solution I've used myself in the past is to use ng-repeat
to render the list but never actually resorting the underlying data. This way all the DOM elements will remain in the DOM and can be animated. To render the elements correctly use ng-style
and a custom property, for example:
ng-style="{ 'top': country.position * 20 + 'px' }"
To update the position
property do the following:
Create a copy of the underlying data
You could use angular.copy
to copy the entire array, but with large arrays this wouldn't be good for performance. It would also be unnecessary since each object in the copied array would only need a property that is unique and the property to sort by:
var tempArray = countries.map(function(country) {
var obj = {
id: country.id
};
obj[property] = country[property];
return obj;
});
In the example above id
is the unique property and property
is a variable containing the name of the property to sort by, for example name
.
Sort the copy
To sort the array use Array.prototype.sort()
with a compare function:
tempArray.sort(function(a, b) {
if (a[property] > b[property])
return 1;
if (a[property] < b[property])
return -1;
return 0;
});
Set position to the element's index in the sorted copy
countries.forEach(function(country) {
country.position = getNewPosition(country.id);
});
function getNewPosition(countryId) {
for (var i = 0, length = tempArray.length; i < length; i++) {
if (tempArray[i].id === countryId) return i;
}
}
There is room for improvement, but that is the basics of it.
Demo: http://plnkr.co/edit/2Ramkg3sMW9pds9ZF1oc?p=preview
I implemented a version that used staggering, but it looked a bit weird since elements would overlap each other momentarily.