Animating a counter with Angular — Using requestAnimationFrame()

Working on my last project, I had to come up with a series of four animated counters and I just couldn’t resolve to import another *.js to my project for such a simple feature.

This article will detail how I built the component and explain the challenges I overcame.

GitHub Project

StackBlitz

Using requestAnimationFrame()

Since I had created something similar in the past, I already knew setTimeout() and setInterval() syntax can be messy and hard to maintain. The result is also flickering and missing some frames. Find out why.

Knowing a little about the requestAnimationFrame() method frame, I set out to discover more about it. I have to say I am highly satisfied with the result, but also with the code cleanliness.

“The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation before the next repaint. […]

You should call this method whenever you’re ready to update your animation onscreen. This will request that your animation function be called before the browser performs the next repaint. The number of callbacks is usually 60 times per second, but will generally match the display refresh rate in most web browsers as per W3C recommendation.”

-MDN

Creating a reusable function

My task is to create four animated counters, so my first concern is to create a reusable function. I need four parameters: the HTML element, a starting value, an ending value, and a duration.

Using requestAnimationFrame, I have to call the method every time I need a new frame: this is why I declare a “step” as a function to do so. Inside it, I set a timestamp, compare it to the progress made so far, and set the value in the HTML. Finally, if the progress has not reached 100%, I use the callback parameter to run the animation again.

Once this function is created, the hard work is done. It is pretty easy to call it and use it as needed. All I have to do is call each counter using @ViewChild. So here is the full app.component.ts file:

export class AppComponent implements AfterViewInit {  title = 'Counter animation with angular';  cAnimated: boolean = false;  dAnimated: boolean = false;  // Accessing DOM elements with ViewChild  @ViewChild('a') a: any;  @ViewChild('b') b: any;  @ViewChild('c') c: any;  @ViewChild('d') d: any;   constructor(private render: Renderer2) {}   // Counter animation function   animateValue(obj, start, end, duration) {      let startTimestamp = null;      const step = timestamp => {         //  Set the actual time         if (!startTimestamp) startTimestamp = timestamp;         // Calculate progress (the time versus the set duration)         const progress = Math.min((timestamp - startTimestamp) / duration, 1);         // Calculate the value compared to the progress and set the value in the HTML         obj.nativeElement.innerHTML = Math.floor(progress * (end - start) + start);         // If progress is not 100%, an call a new animation of step         if (progress < 1) window.requestAnimationFrame(step)      };      // Call a last animation of step     window.requestAnimationFrame(step);   }}

Creating a scrolling event

Since the animations are not showing at the top of the page, I had to create a scrolling event that would allow the animation to start, only once, when the element is fully in the viewport, no matter which size it is.

To check if the element is fully in the viewport I used the getBoundingClientRect() method and compared it to the innerHeight property of the window object. To know more.

To make sure the animation runs only once, I declared an elementAnimated Boolean property and set it to true, once the animation is started so it would not start every time the event is triggered.

// ngAfterViewInit is called after the view is initially rendered.   @ViewChild() depends on it. You can't access view members before they are rendered. See the paragraph below.ngAfterViewInit() {  // Calling the first two animations  this.animateValue(this.a, 0, 2021, 1500);  this.animateValue(this.b, 0, 16, 1500);  // Create a scrolling event using Renderer2  this.render.listen('window', 'scroll', () => {      // Get element c position      let cPosition = this.c.nativeElement.getBoundingClientRect();   // Compare it with the height of the window   if (cPosition.top >= 0 && cPosition.bottom <= window.innerHeight)    {      // if it has not been animated  yet, animate c      if (this.cAnimated == false) {         this.animateValue(this.c, 0, 2300, 1500);         // prevent animation from running again         this.cAnimated = true;      }    }    // Get element d position    let dPosition = this.d.nativeElement.getBoundingClientRect();    // Compare it with the height of the window    if (dPosition.top >= 0 && dPosition.bottom <= window.innerHeight) {       // if it has not been animated  yet, animate d       if (this.dAnimated == false) {       this.animateValue(this.d, 0, 3, 1500);      // prevent animation from running again      this.dAnimated = true;   }}

Hopefully, this has helped you in your path to create a smooth lighting fast animated counter. Cheers!

--

--

Mic B. || Angular Lead Programmer

Web programmer for two years now. I learned on my own and started out by freelancing before finding my first programming position at APRIL Canada.