How can I animate sorting a list with orderBy using ng-repeat with ng-animate?
Asked Answered
C

2

30

I'm rendering a list of objects using ng-repeat with an orderBy filter like this:

<li class="list-item" ng-repeat="item in items | orderBy:predicate:reverse">

My attempts to ng-animate a change in sorting of the list have proven frustrating and aren't worth sharing. I have seen the Yearofmoo example app here.

Unfortunately this demonstration is not quite what I'm trying to achieve. I need to animate the X position of a given list item when it is placed in a new order after the orderBy definition changes. I have tried to accomplish this with css transitions and absolute positioning, but ng-repeat seems to recreate the list items on orderBy making animation a real challenge.

  1. Is this possible with ng-repeat | orderBy (with or without ng-animate)?
  2. Can you suggest an approach or provide an example?
Crassus answered 8/5, 2013 at 1:18 Comment(3)
I have come across this issue but haven't had to build a solution, or I would provide a proper answer with examples - however the solution I planned was to filter a copy of the list (i.e. in a controller), find the new index of the item, then animate moving the item to that index. You're right - the filter+ng-repeat just rebuilds the list. What you actually want is separate determining the position from processing the animation.Decoction
I was afraid that might be the solution -- thanks!Crassus
@AlexOsborn You should submit that as an answer. It helped me!Lynn
W
42

So, even if @Alex Osborn has shown a way to do what you want in the comments, here is my attempt:

angular.module('StackApp', []).controller('MainCtrl', function($scope) {
  'use strict';

  $scope.reverse = 'false';

  $scope.myList = [{
    id: 0,
    text: 'HTML5 Boilerplate'
  }, {
    id: 1,
    text: 'AngularJS'
  }, {
    id: 2,
    text: 'Karma'
  }, {
    id: 3,
    text: 'Hello'
  }, {
    id: 4,
    text: 'World'
  }, {
    id: 5,
    text: 'How'
  }, {
    id: 6,
    text: 'Are'
  }, {
    id: 7,
    text: 'You'
  }, {
    id: 8,
    text: '?'
  }, {
    id: 9,
    text: 'I'
  }, {
    id: 10,
    text: 'write'
  }, {
    id: 11,
    text: 'more'
  }, {
    id: 12,
    text: 'to'
  }, {
    id: 13,
    text: 'make'
  }, {
    id: 14,
    text: 'the'
  }, {
    id: 15,
    text: 'list'
  }, {
    id: 16,
    text: 'longer'
  }];

  $scope.$watch('reverse', function() {
    $scope.setOrder();
  });

  $scope.setOrder = function() {

    if ($scope.reverse === 'random') {

      var t = [];

      for (var i = 0; i < $scope.myList.length; i++) {
        var r = Math.floor(Math.random() * $scope.myList.length);
        while (inArray(t, r)) {
          r = Math.floor(Math.random() * $scope.myList.length);
        }
        t.push(r);
        $scope.myList[i].order = r;
      }

    } else {

      for (var i = 0; i < $scope.myList.length; i++) {
        if ($scope.reverse === 'false') {
          $scope.myList[i].order = i;
        } else {
          $scope.myList[i].order = ($scope.myList.length - 1 - i);
        }
      }
    }
  };

  function inArray(a, value) {
    for (var i = 0; i < a.length; i++) {
      if (a[i] === value) {
        return true;
      }
    }
    return false;
  }

});
#list {
  /* Needed, otherwise items would be at top of the page (see below) */
  position: absolute;
  /* full width, or it would look strange */
  width: 100%;
}
#list li {
  position: absolute;
  /* Top: 0; this will be changed for every single list item by AngularJS */
  top: 0;
  /* Item height; hold this in sync with template file */
  height: 40px;
  /*  Simple transition */
  -webkit-transition: top 0.5s ease-in-out;
  -moz-transition: top 0.5s ease-in-out;
  transition: top 0.5s ease-in-out;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>
<div ng-app="StackApp">
  <div ng-controller="MainCtrl">
    <h1>Animate Order</h1>
    <form action="">
      <label for="reverse">reverse = true</label>
      <br>
      <input type="radio" value="true" name="reverse" ng-model="reverse">
      <br>
      <br>
      <label for="reverse">reverse = false</label>
      <br>
      <input type="radio" value="false" name="reverse" ng-model="reverse">
      <br>
      <br>
      <label for="reverse">reverse = random (click button below to shuffle again)</label>
      <br>
      <input type="radio" value="random" name="reverse" ng-model="reverse">
    </form>
    <br>
    <br>
    <input type="button" ng-click="reverse = 'random';setOrder()" value="setOrder()">
    <br>
    <br>
    <ul id="list" ng-style="{height: ((myList.length * 40) + 'px')}">
      <li ng-repeat="item in myList" ng-style="{top: ((item.order * 40) + 'px')}">{{$index}} - {{item.order}}. {{item.text}}</li>
    </ul>
  </div>
</div>

So, AngularJS doesn't order the items, but it changes the CSS attribute top (ng-style="{top: ...}"). AngularJS doesn't recreate the list and we get a nice animation. :)

Wernerwernerite answered 21/5, 2013 at 12:24 Comment(3)
Thanks for putting this together!Crassus
Note that this solution does only change the elements' top style attributes, not their order in the DOM (which is probably why it works - the elements are not removed and readded by Angular). While this doesn't matter visually, it is bad for accessibility: blind users with screen readers will always get the original order of the first rendering. See the WCAG guideline on this topic: w3.org/TR/WCAG20-TECHS/C27.htmlUrethrectomy
Rather Then writing So Much of Code you can directly use reverse() function of javascript. var list = CompanyList[]; reverselist = list.reverse(); ng-repeat="item in reverselist"Trainor
A
11

I've expanded upon AndreM96's answer to allow displaying the list as a grid.

angular.module('StackApp', []).config(function($routeProvider) {

  'use strict';

  $routeProvider
    .when('/', {
      template: '<h1>Animate Order</h1>' +
        '<form action="">' +
        '<input type="radio" value="true" name="order" ng-model="order">' +
        '<label for="order">reverse</label><br><br>' +

        '<input type="radio" value="false" name="order" ng-model="order">' +
        '<label for="order">normal</label><br><br>' +

        '<input type="radio" value="random" name="order" ng-model="order">' +
        '<label for="order">random (click button below to shuffle again)</label><br>' +

        '</form>' +
        '<input type="button" ng-click="order = \'random\';setOrder()" value="randomize">' +
        '<br><br>' +
        '<ul id="list" ng-style="{height: ((myList.length * 90) + \'px\')}">' +
        '<li ng-repeat="item in myList" ng-style="{top: ((item.row * 90) + \'px\'), left: ((item.column * 90) + \'px\')}">{{$index}} - {{item.order}}. {{item.text}}</li>' +
        '</ul>',
      controller: 'MainCtrl'
    })
    .otherwise({
      redirectTo: '/'
    });

});

angular.module('StackApp').controller('MainCtrl', function($scope) {
  'use strict';

  $scope.order = 'false';

  $scope.myList = [{
      id: 0,
      text: 'HTML5 Boilerplate'
    },
    {
      id: 1,
      text: 'AngularJS'
    },
    {
      id: 2,
      text: 'Karma'
    },
    {
      id: 3,
      text: 'Hello'
    },
    {
      id: 4,
      text: 'World'
    },
    {
      id: 5,
      text: 'How'
    },
    {
      id: 6,
      text: 'Are'
    },
    {
      id: 7,
      text: 'You'
    },
    {
      id: 8,
      text: '?'
    },
    {
      id: 9,
      text: 'I'
    },
    {
      id: 10,
      text: 'write'
    },
    {
      id: 11,
      text: 'more'
    },
    {
      id: 12,
      text: 'to'
    },
    {
      id: 13,
      text: 'make'
    },
    {
      id: 14,
      text: 'the'
    },
    {
      id: 15,
      text: 'list'
    },
    {
      id: 16,
      text: 'longer'
    }
  ];

  $scope.$watch('order', function() {
    $scope.setOrder();
  });

  $scope.setOrder = function() {

    var i;

    if ($scope.order === 'random') {
      var t = [];
      for (i = 0; i < $scope.myList.length; i++) {
        var r = Math.floor(Math.random() * $scope.myList.length);
        while (inArray(t, r)) {
          r = Math.floor(Math.random() * $scope.myList.length);
        }
        t.push(r);
        $scope.myList[i].order = r;
      }
    } else if ($scope.order === 'false') {
      for (i = 0; i < $scope.myList.length; i++) {
        $scope.myList[i].order = i;
      }
    } else {
      for (i = 0; i < $scope.myList.length; i++) {
        $scope.myList[i].order = ($scope.myList.length - 1 - i);
      }
    }

    calcGridPosition();
  };

  function inArray(a, value) {
    for (var i = 0; i < a.length; i++) {
      if (a[i] === value) {
        return true;
      }
    }
    return false;
  }

  function calcGridPosition() {
    for (var i = 0; i < $scope.myList.length; i++) {
      var item = $scope.myList[i];

      // columns, left-to-right, top-to-bottom
      var columns = 5;
      item.column = item.order % columns;
      item.row = Math.floor(item.order / columns);

      // rows, top-to-bottom, left-to-right
      // var rows = 3;
      // item.column = Math.floor(item.order/rows);
      // item.row = item.order%rows;
    }
  }

});
#list {
  position: absolute;
  width: 100%;
  list-style-type: none;
  padding-left: 0;
}

#list li {
  position: absolute;
  height: 70px;
  width: 70px;
  background: #ddd;
  -webkit-transition: all 2.5s ease-in-out;
  -moz-transition: all 2.5s ease-in-out;
  transition: all 2.5s ease-in-out;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.min.js"></script>
<main ng-app="StackApp">
  <div class="container" ng-view></div>
</main>

JSBin Demo

enter image description here

Autarky answered 3/2, 2014 at 21:6 Comment(2)
Click the 'view demo' link above and you'll find an 'edit in js bin' button in the upper right corner of your browser. PS: Your question sounds kinda rude. I'd appreciate if you'd ask more nicely next time. Thanks.Autarky
hey thanks for the reply and feedback to my comment. I'll take more care next time.Disaccord

© 2022 - 2024 — McMap. All rights reserved.