Использование в Angular

Angular предоставляет несколько подходов для интеграции timeago.js: пайпы, директивы, сервисы. Пайп — наиболее идиоматичный вариант для форматирования в шаблонах.


Установка

npm install timeago.js

Регистрация локали в AppModule или в standalone-провайдере:

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { register } from 'timeago.js';
import ru from 'timeago.js/esm/lang/ru';

register('ru', ru);

@NgModule({
  declarations: [AppComponent],
  imports:      [BrowserModule],
  bootstrap:    [AppComponent],
})
export class AppModule {}

Пайп TimeAgoPipe

// pipes/time-ago.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { format } from 'timeago.js';

@Pipe({
  name:  'timeAgo',
  pure:  false, // нечистый пайп — обновляется при каждом CD
})
export class TimeAgoPipe implements PipeTransform {
  transform(value: Date | string | number, locale = 'ru'): string {
    if (!value) return '';
    return format(value, locale);
  }
}

Регистрация:

@NgModule({
  declarations: [AppComponent, TimeAgoPipe],
  // ...
})
export class AppModule {}

Использование в шаблоне:

<time>{{ post.createdAt | timeAgo:'ru' }}</time>

Пайп с кэшированием

pure: false вызывает пайп при каждом цикле change detection — это дорого при большом количестве элементов. Оптимизация:

@Pipe({ name: 'timeAgo', pure: false })
export class TimeAgoPipe implements PipeTransform {
  private lastInput: unknown;
  private lastResult = '';
  private lastUpdate = 0;

  transform(value: Date | string | number, locale = 'ru'): string {
    const now = Date.now();

    if (value === this.lastInput && now - this.lastUpdate < 10000) {
      return this.lastResult;
    }

    this.lastInput  = value;
    this.lastResult = format(value, locale);
    this.lastUpdate = now;
    return this.lastResult;
  }
}

Директива TimeagoDirective

// directives/timeago.directive.ts
import { Directive, ElementRef, Input, OnInit, OnDestroy, OnChanges } from '@angular/core';
import { render, cancel } from 'timeago.js';

@Directive({ selector: '[appTimeago]' })
export class TimeagoDirective implements OnInit, OnDestroy, OnChanges {
  @Input() appTimeago: string = 'ru';

  constructor(private el: ElementRef) {}

  ngOnInit() {
    render(this.el.nativeElement, this.appTimeago);
  }

  ngOnChanges() {
    cancel(this.el.nativeElement);
    render(this.el.nativeElement, this.appTimeago);
  }

  ngOnDestroy() {
    cancel(this.el.nativeElement);
  }
}

Использование:

<time [appTimeago]="'ru'" datetime="2025-05-26T10:00:00Z"></time>

Сервис TimeagoService

// services/timeago.service.ts
import { Injectable } from '@angular/core';
import { format, register } from 'timeago.js';

@Injectable({ providedIn: 'root' })
export class TimeagoService {
  private locale = 'ru';

  setLocale(locale: string) {
    this.locale = locale;
  }

  format(date: Date | string | number): string {
    return format(date, this.locale);
  }
}

Использование в компоненте:

@Component({
  template: '<time>{{ timeago.format(post.createdAt) }}</time>',
})
export class PostComponent {
  constructor(public timeago: TimeagoService) {}
}

Standalone компонент (Angular 17+)

import { Component, Input } from '@angular/core';
import { format } from 'timeago.js';
import { CommonModule } from '@angular/common';

@Component({
  selector:  'app-time-ago',
  standalone: true,
  imports:   [CommonModule],
  template:  '<time [attr.datetime]="date">{{ label }}</time>',
})
export class TimeAgoComponent {
  @Input() set date(value: string) {
    this._date = value;
    this.label = format(value, 'ru');
  }

  _date = '';
  label = '';
}

Обновление через ChangeDetectorRef

import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { format } from 'timeago.js';

@Component({
  selector:  'app-live-time',
  template:  '<time>{{ label }}</time>',
})
export class LiveTimeComponent implements OnInit, OnDestroy {
  label = '';
  private timer: ReturnType<typeof setInterval>;

  constructor(private cdr: ChangeDetectorRef) {}

  ngOnInit() {
    const date = '2025-05-26T08:00:00Z';

    this.label = format(date, 'ru');

    this.timer = setInterval(() => {
      this.label = format(date, 'ru');
      this.cdr.detectChanges(); // принудительно обновить шаблон
    }, 60000);
  }

  ngOnDestroy() {
    clearInterval(this.timer);
  }
}