I Updated My Component Host Animation… Here’s the Angular 20 Way

June 26, 2025 | 13 Minute Read

If you’ve been building Angular apps for a while, like I have, you know the framework evolves fast. In this tutorial I’m going to show you how to modernize an older app step-by-step using the latest Angular features… modern host bindings and events, control flow, and signal inputs. By the end, your code will be smaller, a little faster, and overall more modern. You’ll see exactly how to quickly modernize several aspects of an existing application.

Animating List Items in Angular

A couple years back, I built this little Angular demo showing how to use the @HostBinding decorator to bind an animation to a component host:

An example of an older Angular application using the @HostBinding decorator to bind a animation to a component host

Back then, it worked great.

But Angular has come a long way and now we have some much cleaner, more powerful ways to write the logic behind this functionality.

Let’s look at the code that’s making this happen.

The app component is where this list of players lives currently:

<app-player 
    [player]="player" 
    *ngFor="let player of players; trackBy: trackByFn">
</app-player>

Modernizing Control Flow: @for and @if Blocks

First off, this list is using the old *ngFor structural directive.

This isn’t the way we want to do this anymore.

Instead, we want to use a @for block:

@for (player of players; track player.name) {
    <app-player [player]="player"></app-player>
}

This is part of Angular’s modern built-in control flow syntax.

It’s a little cleaner, no more asterisk, no structural directive needed.

And instead of the old TrackByFunction, we now use “track” with a unique identifier, in this case, the player name.

Same idea, just nicer to read.

And because of that change, we no longer need the trackBy method in the component TypeScript:

trackByFn(index: number, player: Player): string {
    return player.name;
}

We can just delete it.

The @for block takes care of tracking for us.

Now, let’s switch back to the template, there were a couple more things that we can improve.

We’ve got two buttons that use *ngIf, one for removing a player and one for adding:

<button
    *ngIf="players.length > 0"
    (click)="removePlayer()"
    title="Remove Player"
    class="remove">
    <span class="cdk-visually-hidden">
        Remove Player
    </span>
</button>
<button
    *ngIf="players.length < totalCount"
    (click)="addPlayer()"
    title="Add Player"
    class="add">
    <span class="cdk-visually-hidden">
        Add Player
    </span>
</button>

We can modernize these too, using @if instead of the structural directive:

@if (players.length > 0) {
    <button
        (click)="removePlayer()"
        title="Remove Player"
        class="remove">
        <span class="cdk-visually-hidden">
            Remove Player
        </span>
    </button>
}
@if (players.length < totalCount) {
    <button
        (click)="addPlayer()"
        title="Add Player"
        class="add">
        <span class="cdk-visually-hidden">
            Add Player
        </span>
    </button>
}

Again, no more asterisk, no structural directive.

This is the new syntax Angular recommends moving forward, and once you start using it, you won’t want to go back.

Ok, this is cool but what does it have to do with host-binding animations?

Nothing, right?

Cleaner Animations with Host Metadata

There aren’t even any animations in this template.

Well, this is because we’re using the @HostBinding decorator in the player component for this animation.

So, let’s take a look at this component because this is where the animations live.

Here, we’ve got a @HostBinding decorator to bind the animation on the component host:

@HostBinding('@enterLeaveAnimation') animate = true;

And then two @HostListener decorators.

One for when the animation starts, and one for when it’s done:

@HostListener('@enterLeaveAnimation.start') start() {
    document.body.style.backgroundColor = 'yellow';
}

@HostListener('@enterLeaveAnimation.done') done() {
    document.body.style.backgroundColor = 'white';
}

But here’s the thing, while these decorators are still supported, they’re not the recommended way to bind things on the component host anymore.

We want to do this with the host property instead now.

We’ll switch this over in a minute, but first let’s look at the animation itself.

Here we can see that we’re using an external animation:

import { enterLeaveAnimation } from '../animation';

@Component({
    selector: 'app-player',
    ...,
    animations: [
        enterLeaveAnimation
    ]
})

Let’s look at this file real quick to better understand how it works:

import { style, trigger, transition, animate } from '@angular/animations';

export const enterLeaveAnimation = trigger('enterLeaveAnimation', [
    transition(':enter', [
        style({opacity: 0, transform: 'scale(0.8)'}),
        animate('500ms ease-in', 
            style({opacity: 1, transform: 'scale(1)'}))
    ]),
    transition(':leave', [
        style({opacity: 1, transform: 'scale(1)'}),
        animate('500ms ease-in', 
            style({opacity: 0, transform: 'scale(0.8)'}))
    ]),
]);

It’s just a simple “enter” and “leave” animation.

The :enter alias allows us to animate an element when it’s added to the DOM, and the :leave alias does the opposite, it animates items as they leave the DOM.

Okay, now that we know how this works, let’s go back to the player component and switch over to the host property.

First, we add a new host property inside the component decorator and then add a binding for the animation like this:

@Component({
    selector: 'app-player',
    ...,
    host: {
        '[@enterLeaveAnimation]': ''
    }
})

That’s it, now the animation is bound to the component host without using the @HostBinding decorator.

And, since we’re doing it this way now, we don’t need the old @HostBinding decorator anymore, so it can be removed.

Next, we can move the “start” and “done” events to the host property too.

We use parentheses for this just as we would for events in the template.

Then we just need to call our “start” and “done” methods for these:

@Component({
    selector: 'app-player',
    ...,
    host: {
        '[@enterLeaveAnimation]': '',
        '(@enterLeaveAnimation.start)': 'start()',
        '(@enterLeaveAnimation.done)': 'done()'
    }
})

Now we don’t need the @HostListener decorators anymore, so they can be removed as well.

Now before we finish, there’s one more modern pattern we’ll definitely want to use here: signal inputs.

Upgrading to Signal Inputs

Right now, this uses the classic @Input decorator:

@Input({required: true}) player!: Player;

But modern Angular gives us a newer way, using signal inputs, which makes the component more reactive.

So, we just need to switch this over to the new input function:

import { ..., input } from '@angular/core';

readonly player = input.required<Player>();

Then, we can remove the old decorator import too.

Now, we’re not done yet… over in the template we need to update it to use this new signal.

What I’m going to do is create a template variable here with the @let syntax called “playerVar”.

Then I’ll set this to the signal input.

@let playerVar = player();

Then I just need to update all instances of the old “player” property:

Before:

<img [ngSrc]="'/assets/' + player.imageName + '.avif'" width="1040" height="760" />
<h2></h2>
<dl>
    <div>
        <dt>
            GP
        </dt>
        <dd>
            {{ player.games | number }}
        </dd>
    </div>
    <div>
        <dt>
            PTS
        </dt>
        <dd>
            {{ player.points | number }}
        </dd>
    </div>
    <div>
        <dt>
            FG%
        </dt>
        <dd>
            {{ player.fieldGoalPercentage | percent:'.1' }}
        </dd>
    </div>
    <div>
        <dt>
            3P%
        </dt>
        <dd>
            {{ player.threePointPercentage | percent:'.1' }}
        </dd>
    </div>
</dl>

After:

@let playerVar = player();
<img [ngSrc]="'/assets/' + playerVar.imageName + '.avif'" width="1040" height="760" />
<h2></h2>
<dl>
    <div>
        <dt>
            GP
        </dt>
        <dd>
            {{ playerVar.games | number }}
        </dd>
    </div>
    <div>
        <dt>
            PTS
        </dt>
        <dd>
            {{ playerVar.points | number }}
        </dd>
    </div>
    <div>
        <dt>
            FG%
        </dt>
        <dd>
            {{ playerVar.fieldGoalPercentage | percent:'.1' }}
        </dd>
    </div>
    <div>
        <dt>
            3P%
        </dt>
        <dd>
            {{ playerVar.threePointPercentage | percent:'.1' }}
        </dd>
    </div>
</dl>

Using a template variable like this means we can reference “playerVar” directly, without having to write player() with parentheses every time.

But, since it’s now a local variable, I do have to change the name. So, pick your poison!

Alright, I think we’ve got everything, let’s save it and take a look in the browser:

An example of an older Angular application using the @HostBinding decorator to bind a animation to a component host

There we go, add a player, nice entrance animation, remove a player, smooth exit animation.

And everything works exactly as it should.

But now the code is more modern, and ready for anything Angular throws at us in future updates.

Final Result & Key Takeaways

So that’s it… a quick refactor of an older app using Angular modern syntax: new control flow, signal inputs, and cleaner host bindings.

If you’re building apps in Angular today, these patterns will help you write clearer, more reactive code, and they’ll keep your apps easy to maintain long-term.

If you found this helpful, don’t forget to subscribe and check out my other Angular tutorials for more tips and tricks!

Additional Resources

Want to See It in Action?

Want to experiment with the final version? Explore the full StackBlitz demo below. If you have any questions or thoughts, don’t hesitate to leave a comment.