Animating elements moving between lists

Anchored CSS transitions to move elements between ng-repeat lists

Animating elements between parts of an application can be tricky. The beauty of ng-repeat means we can declare that a list, such as a ul, should represent a data model, such as an Array of data, and the list keeps up to date with whatever changes we make to the underlying model. The tricky bit comes when we want to view not just current state of the model, but transitions between states, such as

  • removal of an item from a list,
  • addition of an item to a list, or
  • moving an item from one list to another.

Pre-Angular 1.4, it was fairly straightforward to animate addition or removal of items from an ng-repeat powered list using .ng-enter and .ng-leave transitions. With 1.4, we can now animate moving a peice of data from one list to another.

The key to this is the ng-animate-ref attribute. If the ref-value on an outgoing element matches an element on an incoming element, then Angular clones the outgoing element, inserts the clone positioned absolutely on the page, and using CSS transitions moves it from the position of the outgoing to the position of the incoming.

What outgoing and incoming mean is that their addition to and removal from the DOM are subject to .ng-enter and .ng-leave transitions, either on themselves, or on a parent element. For example, we can animate the heights of elements to or from 0 when they are added or removed from a list:

<ul class="list">
  <li class="item" ng-repeat="item in listA" ng-click="toB(item)">
    Item: {{ item.id }}
  </li>
</ul>
<ul class="list">
  <li class="item" ng-repeat="item in listB" ng-click="toA(item)">
    Item: {{ item.id }}
  </li>
</ul>

with CSS transitions defined as:

/* New element set to 0 height after addition to DOM... */
.item.ng-enter {
  transition: 0.1s linear all;
  height: 0;
}

/* ...then transitioned to 30px */
.item.ng-enter.ng-enter-active {
  height: 30px;
}

/* Existing element set to 30px height just before removal... */
.item.ng-leave {
  transition: 0.1s linear all;
  height: 30px;
}

/* ... then transitioned to 0 */
.item.ng-leave.ng-leave-active {
  height: 0;
}

The above rules work because at the appropriate points in the addition and removal of elements from ng-repeat lists, Angular adds the classes on the elements and allows the browser to perform the CSS transitions.

Once the list elements are subject to .ng-enter and .ng-leave transitions, we add a wrapper span to each element, with an ng-animate-ref attribute containing the ID of the item, so Angular can match each element leaving the DOM with one entering.

<ul class="list" title="List A">
  <li class="item" ng-repeat="item in listA" ng-click="toB(item)">
    <span class="item-contents" ng-animate-ref="{{ item.id }}">Item: {{ item.id }}</span>
  </li>
</ul>
<ul class="list" title="List B">
  <li class="item" ng-repeat="item in listB" ng-click="toA(item)">
    <span class="item-contents" ng-animate-ref="{{ item.id }}">Item: {{ item.id }}</span>
  </li>
</ul>

Note that depending on the transitions we want, we might not need the extra wrapping span, but it keeps the code as flexible as possible, and avoids any ambiguity about what transitions are happening on what elements: the enter and leave transitions take place on the .item elements, and the animations between the lists happen on the .item-contents elements.

We can control the CSS transition with a single style rule on the .ng-anchor-in class that Angular adds to the cloned element.

.item-contents.ng-anchor-in {
  transition: 0.2s linear all;
}

You can have a more complex transition if you want by also having a transition on .ng-anchor-out, but this is not necessary for this case. You can see the docs for ngAnimate for more information.

You can see this in-action, together with the the rest of the boilerplate Javascript and CSS, in the below Plunker. Each item cam be moved by clicking on it.

That's it! There isn't really much to it.