Example of ngAnimate for smoothly sorting an ng-re

2019-01-12 07:20发布

问题:

I would like to see a functional example of using angular-animate (1.2x) to sort a list. (I have only come across broken fiddles etc on the interwebs):

An ng-repeat given an array [A,B,C] and later [C, B, A] should:

  • Move A to the bottom
  • Move C to the top
  • Keep B's position

(Using CSS absolute top positioning or similar.)

An example using staggering (transition-delay) is a bonus.

回答1:

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:

  1. The ng-move class would only be applied to the elements that move (so only to the Sweden element in the example above)

  2. 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:

  1. 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.

  2. 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;
    });
    
  3. 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.



回答2:

have a look on this, its very easy: CSS:

  .animate-enter, 
    .animate-leave
    { 
        -webkit-transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
        -moz-transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
        -ms-transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
        -o-transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
        transition: 400ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all;
        position: relative;
        display: block;
    } 

    .animate-enter.animate-enter-active, 
    .animate-leave {
        opacity: 1;
        top: 0;
        height: 30px;
    }

    .animate-leave.animate-leave-active,
    .animate-enter {
        opacity: 0;
        top: -50px;
        height: 0px;
    }

HTML:

    <!doctype html>
<html ng-app>
<head>
  <meta charset="utf-8">
  <title>Top Animation</title>
  <script>document.write('<base href="' + document.location + '" />');</script>
  <link rel="stylesheet" href="style.css">
  <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
  <script src="http://code.angularjs.org/1.1.5/angular.js"></script>
</head>
<body ng-init="names=['Igor Minar', 'Brad Green', 'Dave Geddes', 'Naomi Black', 'Greg Weber', 'Dean Sofer', 'Wes Alvaro', 'John Scott', 'Daniel Nadasi'];">
  <div class="well" style="margin-top: 30px; width: 200px; overflow: hidden;">

        <ul class="nav nav-pills nav-stacked">
          <li ng-animate="'animate'" ng-repeat="name in names">
            <a href="#"> {{name}} </a>
          </li> 
      </ul>
    </form>
  </div>
</body>
</html>