Question

I'm trying to use ng-animate with an ng-repeat (and ng-show) to fade out old content and replace it with new.

The problem I'm having is that during the remove and add animations, both the element(s) being added and the element(s) being removed have display:block.

I thought I could avoid this by using an animation-delay in the CSS, but that just delays the fade, and not the addition of the class that sets display on the element.

The result is a jerky transition.

This is my animation CSS (cut down):

.keyframe-fade.ng-enter,
.keyframe-fade.ng-move {
  animation: 0.5s fade-in;
  animation-delay: 1s;
  animation-fill-mode: backwards;
}
.keyframe-fade.ng-leave {
  animation: 0.5s fade-out;
}

But it's easier to demonstrate with this plunkr.

Any ideas?

NOTE: To be clear, the desired behaviour on the plunkr linked is that the coloured squares always take up the same space, i.e. they sit on the same line and the button doesn't move. If possible, I'd like to fix this without absolute positioning 'bodges' as the actual page I'm using this on is much more complex than the demo given.

Was it helpful?

Solution

The solution that I found for this is to augment the pure CSS animation with a very small amount of JavaScript.

To summarise the problem:

  • The entering element is added to the DOM with the ng-enter class at the same time that the leaving element is given the ng-leave class.

  • Though there is an animation delay, the entering element still takes up space

So this piece of javascript takes the element and adds ng-hide for the duration of the leave-animation, removing it afterwards.

.animation('.keyframe-fade', ['$timeout', function ($timeout){
  return {
    enter: function (element, done){

      // Add ng-hide for the duration of the leave animation.
      element.addClass('ng-hide');

      $timeout(function(){
        element.removeClass('ng-hide');
      }, 500)

      done();

    }
  }
}])

The duration is hard-coded here but I don't see any reason that you couldn't grab it from the element instead.

Improvements/suggestions welcomed.

Here's the original plunkr with the change.

OTHER TIPS

This is awful, and for Angular 2+ but just for the record here's one idea.

I have two button elements, one for when the user has items in their shopping cart, and one for when they don't.

The easiest way by far is to put position: relative on the parent DIV and position: absolute on both the buttons. The main disadvantage is the parent DIV has to be sized manually, and things like centering becoming trickier.


If the intent is to delay adding to the DOM based on an Observable value, then I thought 'Why not just delay the observable value?' which will have the same end effect. This needs to be done only when the transition is from false > true though because you only want to hide it when it is coming into view. So I used a pipe to handle this.

    <!-- This button for when item is IN the cart -->
    <button [@cartIconAnimation] *ngIf="showCartIcon | delayTrue | async">View Cart</button>

    <!-- This button for when item is NOT IN the cart -->
    <button [@cartIconAnimation] *ngIf="showCartIcon | complement | delayTrue | async">Add to Cart</button>

This assumes showCartIcon is Observable<boolean>.

Then the pipes are as follows, and no delay is required on your animation criteria.

@Pipe({
    name: 'delayTrue'
})
export class DelayTruePipe implements PipeTransform {

    constructor() {}

    transform(value: Observable<any> | any, delay: number): Observable<any> {
        if (isObservable(value)) {
            return value.pipe(distinctUntilChanged(), debounce((show) => show ? timer(delay || 500) : empty()));
        } else {
            throw Error('Needs to be an observable');
        }
    }
}

@Pipe({
    name: 'complement'
})
export class ComplementPipe implements PipeTransform {

    constructor() {}

    transform(value: Observable<boolean> | any): Observable<any> {
        if (isObservable(value)) {
            return value.pipe(map(i => !i));
        } else {
            throw Error('Needs to be an observable');
        }
    }
}

Note: The delay used by the pipe must be greater than the time it takes for the previous item to disappear, or you'll have the same problem.

The complement pipe just inverts the boolean value.

This solution works, but it's hacky and the timing might be harder to get wrong and there may be race conditions as two different browser timers fire off at the same time. I'd only do something like this if you really can't use position: absolute.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top