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 {}
// 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;
}
}
// 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>
// 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) {}
}
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 = '';
}
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);
}
}