Add spring animations to your angular app with popmotion

8 min read


You might have heard of spring animations recently with popular libraries such as react-spring and Framer Motion in the React world. I was on a hunt recently for a similar package in Angular, but found it missing. Angular's own animation API is quite powerful, but it relies on CSS based animations which are difficult to map to spring based animations.

Video tutorial

Spring animations

In case you're wondering, spring animations are a bit different than the traditional CSS animations. They model physical motion much better and result in more natural looking results, so are worth adding to your apps!

In this article, I'll show you a way to add spring animations to your Angular app with a Javascript library called Popmotion.

Popmotion is a low level animation library which is behind the popular Framer Motion animation package in React

It supports a wide range of animation options, but we're only interested in spring animations here.

Our final result will be the following simple fly in animations added to a contacts list app.

Nice, isn't it? Let's see step by step, how we can build this!

If you'd like to see how to create animations in Angular's own API, have a look at my blog post about a card flip animation here.

If videos are not your thing, continue below with the text version :)

Setting up the project

I already have a sample contacts app built in Angular to which I want to add some spring animations. To get this sample app yourself, you can start with this github repository. Once you have the app ready, continue on the following.

Let's now install the popmotion library with the following command.

npm install popmotion --save

Adding a basic animation to our header

To add an animation, we need to get a reference to the element we want to animate. For that we'll add a template variable to our element which in this case is our app heading and its icon. We'll call it title. In the header.component.html, we add this.

...
<div #title class="flex flex-row items-center">
  <span class="material-icons mr-3">contact_phone</span>
  <span class="text-2xl">Contacts App</span>
</div>
...

Then we add a ViewChild decorator in our component class.

export class HeaderComponent implements OnInit {
  @ViewChild('title') title: ElementRef;
...
}

Popmotion's animate function

Next we add our Popmotion animate function.

The animate function provides a lot of options. We're simply interested in the from, to and the type property here. The type will be "spring" since we need spring animations. We want to create a fly in from the left animation, so in the from we'll add 'translateX(-200px)' and in the to we'll specify 'translateX(0px)'.

We'll call the function in the ngAfterViewInit life cycle method. This is so because otherwise the element might not be initialized when we refer to it.

import { animate } from 'popmotion';
...
ngAfterViewInit() {
    animate({
      from: 'translateX(-200px)',
      to: 'translateX(0px)',
      type: 'spring'
    });
}

Great! The last thing we need to do is to specify our update function, which will actually change the style of our element to animate it as the animation is running. We could directly change the element's style, but the more proper Angular way is to use Renderer2 service. Let's do that.

constructor(private renderer: Renderer2) {}

ngAfterViewInit() {
    animate({
      from: 'translateX(-200px)',
      to: 'translateX(0px)',
      type: 'spring',
      onUpdate: (value) => {
        this.renderer.setStyle(this.title.nativeElement, 'transform', value);
      }
    });
  }

If we test it now, we get a nice looking springy animation of our title.

Making a reusable animation directive

This is all well and good. However, you'll notice that it's a bit too much work. What if we have to add the same animation to different elements? We'll have to add a reference to all the elements and then call them from the code.

But it's Angular, there must be a better way! And there is…by using Angular directives.

Just to revise, directives are basically like components, but without any UI associated with them.

Directives can be attached to UI elements to change them in some way. This is exactly what we want!

So let's create a directive for our animation called fly-left using the following command on the Angular CLI.

ng generate directive animations/fly-left

Setting up our directive

Ok, now the first thing we need is to get the element the directive is attached to. We can do this by simply referencing to the ElementRef in our constructor. Angular will pass the element through here, so we can use it.

Next we're going to just copy the code we already have for our animation after deleting it from the header component. And also add the renderer as before.

@Directive({
  selector: "[appFlyLeft]",
})
export class FlyLeftDirective {
  constructor(private el: ElementRef, private renderer: Renderer2) {}

  ngOnInit() {
    animate({
      from: "translateX(-200px)",
      to: "translateX(0px)",
      type: "spring",
      onUpdate: (value) => {
        this.renderer.setStyle(this.el.nativeElement, "transform", value);
      },
    });
  }
}

Great, so we have our directive all setup now. Let's add it to our template and verify if it animates as before.

<div appFlyLeft class="flex flex-row items-center">
  <span class="material-icons mr-3">contact_phone</span>
  <span class="text-2xl">Contacts App</span>
</div>

Nice, so we have our animation all wrapped up in our directive and the great thing is we can reuse this on whatever element we want to. Let's try adding to our list items now.

<div *ngFor="let contact of data">
  <div appFlyLeft>
    <p class="text-xl">{{ contact.name }}</p>
    <p class="text-gray-500 text-lg">{{ contact.phone }}</p>
  </div>

  ...
</div>

Making a staggered list animation

This animates fine as expected. But it'd be nicer if the list animates in a staggered manner i.e. one after the other. Let's add an input to our directive which can specify a delay value to start the animation.

We'll add an input decorator to our directive with the same name as our directive. Then we'll just shift our code to ngOnChanges instead, so that we can animate whenever we get an input.

export class FlyLeftDirective {
  @Input() appFlyLeft = 0;

  constructor(private el: ElementRef, private renderer: Renderer2) {}

  ngOnChanges(simpleChanges: SimpleChanges) {
    const delay = simpleChanges?.appFlyLeft.currentValue || 0;
    animate({
      from: "translateX(-200px)",
      to: "translateX(0px)",
      type: "spring",
      elapsed: -1 * delay,
      onUpdate: (value) => {
        this.renderer.setStyle(this.el.nativeElement, "transform", value);
      },
    });
  }
}

Also, we get the delay value and use it to plug in the animate function. We have an elapsed property which we can use - any negative number here means there will be a delay, so we'll multiply our delay with -1. Our input will be in the form of milliseconds.

Great, now that we have our delay setup in our directive, let's shift to the contacts list again.

Here we'll specify an expression for each of the item. First, we'll add index parameter which will tell us which item we're referring to. Then, we'll use that to calculate an expression to give us a delay for each item.

<div *ngFor="let contact of data; let idx=index" class="...">
  <div [appFlyLeft]="200 * (idx + 1)">
    <p class="text-xl">{{ contact.name }}</p>
    <p class="text-gray-500 text-lg">{{ contact.phone }}</p>
  </div>
  ...
</div>

This means the higher the index, the greater the delay, giving us a nice staggered list animation!

Testing this out now gives us the following result.

Awesome!

Performance considerations

Now this works great as it is now, but this is a simple app with not much functionality. A problem that we have currently has to do with Angular's change detection system. As it stands now, the change detection is called on every animation frame - which can become a huge drain for our app.

To check this, you can print out something in the console in ngDoCheck() lifecycle method for the component. You'll notice a series of repeated change detection cycles being triggered as our animation progresses.

How to resolve this? Well, so the first step is to recognize that we don't need change detection when animating. Change detection is used so that Angular can keep our UI in sync with the state of our component (it's data and variables). Since we're just animating the styles of the our elements, we can skip the change detection here completely.

To do this, we need to specify the animation code to be run outside of the Angular zone. Here is how the directive changes.

constructor(
    private el: ElementRef,
    private renderer: Renderer2,
    private zone: NgZone
  ) {}

ngOnChanges(simpleChanges: SimpleChanges) {
    const delay = simpleChanges?.appFlyLeft?.currentValue || 0;
    this.zone.runOutsideAngular(() => {
    ...our animate function

    });
}

If you check in ngDoCheck now, you will not see repeated calls to change detection while animating - making our spring animations in Angular buttery smooth!

Conclusion

As you can see, we've managed to create a clean and reusable spring animation directive in Angular with custom delay input. We can use it wherever we want in our apps. You can build upon this and create more spring animations in your own Angular apps!

You can find the complete code for this tutorial here.

Thanks for reading! Bye :)

Check out my Angular and Firebase Authentication crash course

thumbnail
Angular Firebase Authentication: Create Full Sign Up App

Use Angular 16, Angular Material and Firebase Authentication, Firestore and Storage to create a complete Sign Up App!

You may also like...