'How to animate :enter & :leave transitions conditionally in Angular?

I have a list, where the items have an animation like this:

<li @animation>

And this is my animation trigger:

trigger('animation', [
  transition(':enter', [
    style({ height: '0px', 'padding-top': '0', 'padding-bottom': '0'}),  // initial
    animate('0.5s',
      style({ height: '*', 'padding-top': '*', 'padding-bottom': '*'}))  // final
  ]),
  transition(':leave', [
    style({ height: '*', 'padding-top': '*', 'padding-bottom': '*', opacity: 1}),  // initial
    animate('0.5s',
      style({ height: '0px', 'padding-top': '0', 'padding-bottom': '0', opacity: 0}))  // final
  ])
])

How can i conditionally turn on/off this animation for a specific item? Actually i look for sth. like this:

<li [@animation]=item.isAnimated>

which does not work at all.

And unfortunately Angular documentation has just a sentence about this:

For elements entering or leaving a page (inserted or removed from the DOM), you can make the animations conditional. For example, use *ngIf with the animation trigger in the HTML template.

But when i combine the animation annotation with a *ngIf, the not-animated items will obviously not be shown at all:

<li *ngIf="item.isAnimated" @animation>

I want to show all the items further on regardless of isAnimated flag. I just want to turn on/off the animation for a specific item.



Solution 1:[1]

According to Angular IO:

When true, the special animation control binding @.disabled binding prevents all animations from rendering. Place the @.disabled binding on an element to disable animations on the element itself, as well as any inner animation triggers within the element.

The following example shows how to use this feature:

@Component({
    selector: 'my-component',
    template: `
    <div [@.disabled]="isDisabled">
        <div [@childAnimation]="exp"></div>
    </div>
    `,
    animations: [
        trigger("childAnimation", [
            // ...
        ])
    ]
})
class MyComponent {
      isDisabled = true;
  exp = '...';
}

When @.disabled is true, it prevents the @childAnimation trigger from animating, along with any inner animations.

Solution 2:[2]

To clarify: Angulars :enter and :leave keywords are to animate components if the are entering or leaving the dom. Sounds simple but that's exactly the issue with your approach and the aim you try to achieve. Instead of just animating, if there is a new element in the dom you want it more customized, so therefore you need own states, wich can be controlled by yourself in the ngOnInit and ngOnDestroy of a list-entry.

A approach could be the following:

@Component({
  animations: [
    trigger('animation', [
      state('invisible', style({ height: '0px', 'padding-top': '0', 'padding-bottom': '0'}),
      state('visible', style({ height: '*', 'padding-top': '*', 'padding-bottom': '*'})
      transition('invisible => visible', animate('0.5s'))
      transition('visible => invisible', animate('0.5s'))
    ])
  ],
})

private readonly isAnimated: boolean = false/true //Where ever you get this value.
public animationState: string //Or Enum with visible/invisible.

public ngOnInit(): void {
  if (this.isAnimated) {
    animationState = 'visible'
  }
}

public ngOnDestroy(): void {
  if (this.isAnimated && this.animationState === 'visible') {
    animationState = 'invisible'
  }
}
<li [@animation]="animationState"/>

If there are any more questions or issues with this approach - let me know and we can adjust and discuss.

Solution 3:[3]

Create a no-op animation trigger and insert it before :enter

I think the [@.disabled] approach is fine, but it feels odd to create something and disable it via a different mechanism. Fortunately there's an even simpler way.


First, it's important to remember that :enter is exactly equivalent to void => *. In fact here it is in the Angular source code.

function parseAnimationAlias(alias: string, errors: string[]): string|TransitionMatcherFn {
  switch (alias) {
    case ':enter':
      return 'void => *';
    case ':leave':
      return '* => void';
    case ':increment':
      return (fromState: any, toState: any): boolean => parseFloat(toState) > parseFloat(fromState);
    case ':decrement':
      return (fromState: any, toState: any): boolean => parseFloat(toState) < parseFloat(fromState);
    default:
      errors.push(`The transition alias value "${alias}" is not supported`);
      return '* => *';
  }
}

Simplest solution

It's also important to know that Angular's animation engine only matches the first trigger and runs it.

Therefore all you need to do is create a new trigger and insert it before :enter. This animation is a no-op.

transition('void => false', []),
transition(':enter', [...])

And for :leave

transition('false => void', []),
transition(':leave', [...])

So your animation in the template becomes:

[@animation]="enableAnimations"

And when it's false it'll match and do nothing. ANY other value would match :enter which is of course really void => *.

The beauty of this is it won't break anything that already uses [@animation] because that still matches void => * as before.

Note: Any (done) handlers will still get executed even after the no-op animation - so if you're using one make sure it does the right thing when there was no animation.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 j4rey
Solution 2
Solution 3