How can I animate the movement of remaining ng-repeat items when one is removed?
Asked Answered
A

2

13

I have a dynamic list of items using ng-repeat. When something happens an item may disappear. I have handled smoothly animating the removal of these items using ng-animate, but after they are gone, the remaining items simply snap to their new position. How can I animate this movement smoothly?

I've tried applying an "all" transition to the repeated class and using ng-move with no success.

Alverta answered 10/4, 2014 at 20:18 Comment(4)
Have you seen this: nganimate.org/angularjs/ng-repeat/move ?Luciusluck
@aet, this is for angular 1.1.5. The solution should probaly work with 1.2.x, I assume. Which is possible, but needs to be tested.Uyekawa
unfortunately, ng-move does not apply to this scenario. it only applies when items in the ng-repeat list are sorted or filtered. in this case, the item is being removed completely. Having said that - if I apply a filter first THEN remove the item, maybe I can "cheat" and get the benefits of ng-move. will try and update.Alverta
that did not work. applying the filter still resulted in the instant movement, not an smooth transition.Alverta
E
19

You can achieve this by animating the max-height property. Check out this sample:

http://jsfiddle.net/k4sR3/8/

You will need to pick a sufficiently high value for max-height (in my sample, I used 90px). When an item is initially being added, you want it to start off with 0 height (I'm also animating left to have the item slide in from the left, as well as opacity, but you can remove these if they don't jibe with what you're doing):

.repeated-item.ng-enter {
    -webkit-transition:0.5s linear all;
    -moz-transition:0.5s linear all;
    -o-transition:0.5s linear all;
    transition:0.5s linear all;
    max-height: 0;
    opacity: 0;
    left: -50px;
}

Then, you set the final values for these properties in the ng-enter-active rule:

.repeated-item.ng-enter.ng-enter-active {
    max-height: 90px;
    opacity: 1;
    left: 0;
}

Item removal is a bit trickier, as you will need to use keyframe-based animations. Again, you want to animate max-height, but this time you want to start off at 90px and decrease it down to 0. As the animation runs, the item will shrink, and all the following items will slide up smoothly.

First, define the animation that you will be using:

@keyframes my_animation {
  from {
    max-height: 90px;
    opacity: 1;
    left: 0;
  }
  to {
    max-height: 0;
    opacity: 0;
    left: -50px;
  }
}

(For brevity, I'm omitting the vendor-specific definitions here, @-webkit-keyframes, @-moz-keyframes, etc - check out the jsfiddle above for the full sample.)

Then, declare that you will be using this animation for ng-leave as follows:

.repeated-item.ng-leave {
  -webkit-animation:0.5s my_animation;
  -moz-animation:0.5s my_animation;
  -o-animation:0.5s my_animation;
  animation:0.5s my_animation;
}

Basics

In case anyone is struggling with figuring out how to get AngularJS animations to work at all, here's an abbreviated guide.

First, to enable animation support, you will need to include an additional file, angular-animate.js, after you load up angular.js. E.g.:

<script type="text/javascript" src="angular-1.2/angular.js"></script>
<script type="text/javascript" src="angular-1.2/angular-animate.js"></script>

Next, you will need to load ngAnimate by adding it to the list of your module's dependencies (in the 2nd parameter):

var myApp = angular.module('myApp', ['ngAnimate']);

Then, assign a class to your ng-repeat item. You will be using this class name to assign the animations. In my sample, I used repeated-item as the name:

<li ng-repeat="item in items" class="repeated-item">

Then, you define your animations in the CSS using the repeated-item class, as well as the special classes ng-enter, ng-leave, and ng-move that Angular adds to the item when it is being added, removed, or moved around.

The official documentation for AngularJS animations is here:

http://docs.angularjs.org/guide/animations

Enter answered 11/4, 2014 at 0:56 Comment(2)
This is great thank you! My problem was I was animating the leave/enter using transform:scaleY, which wasn't actually changing the element height, just its appearance of height. manipulating the height itself in the animation does the trick.Alverta
This is a great answer! I actually recorder a video - youtube.com/watch?v=mRItxM1dHFA - to ask a better question and it was already solved by you! Regarding "how to setup an animation" --> #21592360 (very simple example)Sieve
S
2

TLDR: Jank is bad, do animations with transform. Check out this fiddle for css and demo.


Explanation

Note that animating height, max-height, top, ... is really bad performance wise because they cause reflows and thus jank (more information on html5rocks|high-performance-animations).

There is however a method getting this type of animation using only transforms by utilizing the sibling selector.

When elements are added there is one reflow because of the new item, all items below are transformed up so they stay at the same position and then the transformation is removed for a smooth slide-in.

In reverse when elements are removed they are transformed to the new position for a smooth slide-out and when the element is finally removed there is again one reflow and the transform is removed instantly so they stay at their position (this is also why it is important to only have transition set on ng-animate).

Alternatively to the example you could also do a transform: scaleY(0) on the deleted item and only transform: translateY() the siblings.

Caveat

Note that this snippet has trouble when multiple elements are removed in quick succession (before the previous animation has completed).

This can be fixed by having an animation time faster than the time a user takes to delete another item or by doing some more work on the animation (out of scope of this answer).

Finally some code

Note: apparently SO breaks the demo with multiple deletes - check out the fiddle to see it in work.

angular.module('app', ['ngAnimate'])
  .controller('testCtrl', ['$scope', function($scope) {
    var self = this;
    
    self.items = [];
    
    var i = 65;
    for(; i < 72; i++)
    {
    	self.items.push({ value: String.fromCharCode(i) });
    }
    
    self.addItem = function()
    {
    	self.items.push({ value: String.fromCharCode(i) });
      i++;
    }
    
    self.removeItemAt = function(index)
    {
    	self.items.splice(index, 1);
    }
  }])
li
{
  height: 48px;
  width: 300px;
  border: 1px solid lightgrey;
  background-color: white;
  position: relative;
  list-style: none;
}
li.ng-enter,
li.ng-enter ~ li {
  transform: translateY(-100%);
}
li.ng-enter.ng-enter-active,
li.ng-enter.ng-enter-active ~ li {
  transform: translateY(0);
}
li.ng-animate {
  z-index: -1;
}
li.ng-animate,
li.ng-animate ~ li {
  transition: transform 0.6s;
}
li.ng-leave,
li.ng-leave ~ li {
  transform: translateY(0);
}
li.ng-leave.ng-leave-active,
li.ng-leave.ng-leave-active ~ li {
  transform: translateY(-100%);
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.23/angular-animate.js"></script>

<div ng-app="app" ng-controller="testCtrl as ctrl">
  <ul>
    <li ng-repeat="item in ctrl.items" ng-bind="item.value">
      
    </li>
  </ul>
  <button ng-click="ctrl.addItem()">
  Add
  </button>
  <button ng-click="ctrl.removeItemAt(5)">
  Remove at 5
  </button>
</div>
Salley answered 18/11, 2016 at 14:41 Comment(3)
Not bad. Unfortunately it doesn't handle varying item size. jsfiddle.net/Ly9p9kzu/5Cammack
Right, that would probably require some more JS work.Salley
checked the fiddle, the bottom buttons are still jumping up after element removedCorydalis

© 2022 - 2024 — McMap. All rights reserved.