4

I have a list of items, each has a unique id

$scope.arr = [{val:0,id:'a'},{val:1,id:'b'},{val:2,id:'c'}];

Each item is absolute positioned according to their $index

<div class="item" ng-repeat="item in arr track by item.id" 
ng-style="getAbsPos($index)" >{{item.id}}</div>

All I wanted is swapping arr[0] and arr[2] in the array, and display a moving animation of this action. It turns out to be very difficult.

I assume this css would work since the list is tracked by id

.item{
    transition:all 3000ms;
}

but somehow angular decides only moving one of items' dom and re-create the other one (why?!). As result, only one item is animated.

= Question =

Is there a solution to fix this problem, so both items will be animated when they swap? Thanks.

(Have to actually swap the item's position in the array, so it can be easily accessed by correct index all the time)

= See Plunker demo =

http://plnkr.co/edit/5AVhz81x3ZjzQFJKM0Iw?p=preview

Mark Ni
  • 2,383
  • 1
  • 27
  • 33
  • possible duplicate of [List reorder animation with angularjs](http://stackoverflow.com/questions/18523531/list-reorder-animation-with-angularjs) – Blackhole Jun 01 '14 at 22:18
  • @Blackhole Hi, thanks. My situation requires actually reordering the array, the solution provided seems faking reordering by add extra position information to each item. – Mark Ni Jun 01 '14 at 22:28
  • Oh, in fact, there is no good solution in the other question as well ;) . Actually, it's look like a bug ([see this comment](https://github.com/angular/angular.js/issues/5160#issuecomment-31024859), which perfectly describes your use case). – Blackhole Jun 01 '14 at 23:21
  • While playing with the Plunker, I've got this: http://plnkr.co/edit/npxFVh9FII0akVj6lIwp?p=preview - if you have the DOM elements inspector, and click the button several times before animation completes, you'll see extra DOM nodes being added... strange behaviour. This does not help, but shows related unexpected behaviour. – J. Bruni Jun 01 '14 at 23:55
  • @J.Bruni that seems to be the feature of ngAnimate module, which auto add delay to dom changes according to css transition value. – Mark Ni Jun 02 '14 at 00:19

2 Answers2

4

After playing around, I did find a very hacky solution which does change the item order in array:

=Idea=

  1. As Zack and many other suggested, we keep a record of display position(item.x) in each item, use it to determine dom position

    <div class="item" ng-repeat="item in arr track by item.id" 
    ng-style="getAbsPos(item.x)" >{{item.id}}</div>
    
  2. when swap, reordering the array first, because dom position is determined by item.x, not $index, no animation will be triggered;

     var a= arr[0];
     var c = arr[2];
     arr[0] = c;
     arr[2] = a; 
    
  3. swap the item.x value of the two items in async manner (using $timeout), so angular treats step 2 and 3 as two separated dom changes, and only step 3 will trigger animation.

     $timeout(function(){
         var tempX = a.x;
     a.x = c.x;
     c.x = tempX;           
     },10)   
    

This may create some problems when batch swap operations are performed. But for user triggered simple two items swap (my use case), it seems works just ok.

Let me know if there is a better solution, thanks.

=Plunker demo=

http://plnkr.co/edit/Vjj9qCcoqMCyuOhNYKKY?p=preview

Mark Ni
  • 2,383
  • 1
  • 27
  • 33
0

One idea would be to use your own left marker, rather than $index. Here is an example using a directive that watches your objects .left attribute. In this scenario you could use the .left to reorder the actual array at some point if you need to post it to a server or something. Here is an accompanying JSFIDDLE.

HTML

<div class="item" ng-repeat="item in list" move-to="item.left">{{item.id}} / {{$index}}</div>

module.controller('myCtrl', function($scope) { 
$scope.list = [
        {val:0, id:'a', left: 0},
        {val:1, id:'b', left: 100},
        {val:2, id:'c', left: 200}
    ];

    $scope.swap = function() {
        var a_left = $scope.list[0].left
        $scope.list[0].left = $scope.list[2].left;
        $scope.list[2].left = a_left;
    }
}) 

.directive('moveTo', function() {
    return {
        restrict: 'A',
        link: function(scope, elem, attrs) {
            scope.$watch(attrs.moveTo, function(newVal) {
                elem.css('left', newVal + "px"); 
            });
        }
    }
});
Zack Argyle
  • 8,057
  • 4
  • 29
  • 37
  • Hi, thanks for the answer. I do aware of this method, however my application requires heavy access of elements by index in real time (with tons of items). `getElementByLeftValue()` would be very expensive in this case. Maybe some additional data structures would help? – Mark Ni Jun 02 '14 at 00:26
  • One idea would be to keep a second array ordered by left, and have it map to an index in the primary array. Again, that is a lot of data, but if you are handling large amounts of data, angularjs might not be the way to go anyway. – Zack Argyle Jun 02 '14 at 00:33