Home > Software design >  Implementing a countdown timer in a pipe with additional service to update component when timer fini
Implementing a countdown timer in a pipe with additional service to update component when timer fini

Time:10-07

I'm showing a date/time countdown on the screen with my Angular app. It's a countdown from a specific date and time.

Running it every second is killing my CPU with Google Chrome. It might be the moment() lib that has some effect, but not sure yet. I might try it without moment, to see if that helps. enter image description here

Is there a better (more efficient) way to do this?

Here is my code

calculateTimeRemaining(endDate: Date, timeZone: string, eventId: number) {
  setInterval(() => {
    const element = document.getElementById("event-"   eventId);
    if (element) {
      const currentDateUtc = moment().utc();
      const endDateUtc = moment(endDate).utc(true);
      endDateUtc.tz(timeZone);
      const dif = moment.duration(endDateUtc.diff(currentDateUtc));
      const hours = dif.hours();
      const minutes = dif.minutes();
      const seconds = dif.seconds();
      if (hours < 1) {
        element.innerHTML = minutes   ' m '   seconds   ' s';
      } else {
        element.innerHTML = hours   ' h '   minutes   ' m '   seconds   ' s';
      }
    }
  }, 1000);
}
<span id="event-{{event.id}}" class="float-right yb-color">{{calculateTimeRemaining(event.datePendingUtc, event.yogabandTimeZoneId, event.id)}}</span>

EDIT (solution) - I've implemented a countdown timer in a pipe (as recommended) to solve this issue. I'll include the full code for the pipe below and how I call it. I also have a service that is being used to send notifications to my component when the timer is up so I can perform other actions.

import { Pipe, PipeTransform } from '@angular/core';
import { Observable, of, timer } from 'rxjs';
import { map, takeWhile } from 'rxjs/operators';
import { EventService } from 'src/app/core/services/event.service';

@Pipe({
  name: 'timeRemaining'
})
export class TimeRemainingPipe implements PipeTransform {
  eventId: number;
  expired = false;

  constructor(private eventService: EventService) {}

  /**
   * @param futureDate    should be in a valid Date Time format
   *                      e.g. YYYY-MM-DDTHH:mm:ss.msz
   *                      e.g. 2021-10-06T17:27:10.740z
   */
   public transform(futureDateUtc: string, eventId: number): Observable<string> {
    
    this.eventId = eventId;
    /**
     * Initial check to see if time remaining is in the future
     * If not, don't bother creating an observable
     */

    if (!futureDateUtc || this.getMsDiff(futureDateUtc) < 0) {
      console.info('Pipe - Time Expired Event: '   eventId);
      return of('EXPIRED');
    }
    
    return timer(0, 1000).pipe(
        takeWhile(() => !this.expired),
        map(() => {
            return this.msToTime(this.getMsDiff(futureDateUtc));
        })
    );
  }

  /**
   * Gets the millisecond difference between a future date and now
   * @private
   * @param   futureDateUtc: string
   * @returns number  milliseconds remaining
   */
   // Z converts to local time
   private getMsDiff = (futureDate: string): number => ( (new Date(futureDate   'Z')) - Date.now());


   /**
    * Converts milliseconds to the
    *
    * @private
    * @param msRemaining
    * @returns null    when no time is remaining
    *          string  in the format `HH:mm:ss`
    */
   private msToTime(msRemaining: number): string | null {
       if (msRemaining < 0) {
           console.info('Pipe - No Time Remaining:', msRemaining);
           this.expired = true;
           this.eventService.expired(this.eventId);
           return 'EXPIRED';
       }

       let seconds: string | number = Math.floor((msRemaining / 1000) % 60),
           minutes: string | number = Math.floor((msRemaining / (1000 * 60)) % 60),
           hours: string | number = Math.floor((msRemaining / (1000 * 60 * 60)) % 24);

       /**
        * Add the relevant `0` prefix if any of the numbers are less than 10
        * i.e. 5 -> 05
        */
       seconds = (seconds < 10) ? '0'   seconds : seconds;
       minutes = (minutes < 10) ? '0'   minutes : minutes;
       hours = (hours < 10) ? '0'   hours : hours;

       return `${hours}:${minutes}:${seconds}`;
   }

}
<span class="float-right yb-color">{{(event.dateUtc | timeRemaining: event.id | async}}</span>

Here is the service

export class EventService {
  private subject = new Subject <any> ();

  expired(eventId: number) {
    this.subject.next(eventId);
  }

  expiredEvents(): Observable <any> {
    return this.subject.asObservable();
  }
}

// how it's called in the component

this.eventService.expiredEvents().subscribe(eventId => {
  console.info('Service - Expired Event:', eventId);
  // do something now w/ expired event
});

CodePudding user response:

For performance, don't call any function inside Angular template because it will run in every change detection.

This article describes the issue very well https://medium.com/showpad-engineering/why-you-should-never-use-function-calls-in-angular-template-expressions-e1a50f9c0496

We could change your implementation using pure pipe

time-remaining.pipe.ts

@Pipe({
  name: 'timeRemaining',
})
export class TimeRemainingPipe {
  transform(event: any): any {
    // put implementation here
    // return time remaining
  }
}

I believe in the pipe, you could remove const element = document.getElementById("event-" eventId); part.

component.html

<span id="event-{{event.id}}" class="float-right yb-color">{{ event | timeRemaining }}</span>

Reference: https://angular.io/guide/pipes

  • Related