Паттерн наблюдателя позволяет подписаться на изменения относительного времени и реагировать на них. В контексте timeago.js это означает уведомление внешнего кода каждый раз, когда обновляется текст элемента.
Стандартный render timeago.js просто обновляет
textContent элемента. Иногда нужно дополнительно:
type TimeUpdateEvent = {
element: Element;
prevText: string;
nextText: string;
date: string;
locale: string;
};
type Listener = (event: TimeUpdateEvent) => void;
class TimeagoObservable {
private listeners = new Set<Listener>();
subscribe(fn: Listener): () => void {
this.listeners.add(fn);
return () => this.listeners.delete(fn);
}
emit(event: TimeUpdateEvent): void {
this.listeners.forEach(fn => fn(event));
}
}
export const timeagoEvents = new TimeagoObservable();
import { format, render as baseRender, cancel } from 'timeago.js';
function observedRender(
elements: Element | Element[],
locale = 'ru',
observable: TimeagoObservable
): void {
const arr = Array.isArray(elements) ? elements : [elements];
// Кастомный цикл обновления вместо встроенного render
function upd ate() {
arr.forEach(el => {
const datetime = el.getAttribute('datetime');
if (!datetime) return;
const nextText = format(datetime, locale);
const prevText = el.textContent ?? '';
if (nextText !== prevText) {
el.textContent = nextText;
observable.emit({ element: el, prevText, nextText, date: datetime, locale });
}
});
}
upd ate();
const id = setInterval(update, 30_000);
// Вернуть cleanup
return () => clearInterval(id) as unknown as void;
}
function watchTimeElements(
container: Element,
onUpdate: (el: HTMLTimeElement) => void
): () => void {
const mutationObs = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (
mutation.type === 'characterData' &&
mutation.target.parentElement?.tagName === 'TIME'
) {
onUpdate(mutation.target.parentElement as HTMLTimeElement);
}
});
});
mutationObs.observe(container, {
subtree: true,
characterData: true,
});
return () => mutationObs.disconnect();
}
// Использование
const stopWatching = watchTimeElements(document.body, (el) => {
console.log(`Time element upd ated: ${el.textContent}`);
});
class TimeSubject<T> {
private value: T;
private listeners: Se t<(val: T) => void> = new Se t();
constructor(initial: T) {
this.value = initial;
}
next(value: T): void {
this.value = value;
this.listeners.forEach(fn => fn(value));
}
getValue(): T {
return this.value;
}
subscribe(fn: (val: T) => void): () => void {
this.listeners.add(fn);
fn(this.value); // Сразу получить текущее значение
return () => this.listeners.delete(fn);
}
}
function createTimeSubject(date: Date | string, locale = 'ru'): TimeSubject<string> {
const subject = new TimeSubject(format(new Date(date as any), locale));
const id = setInterval(() => {
subject.next(format(new Date(date as any), locale));
}, 30_000);
return subject;
}
import { useState, useEffect } from 'react';
import { format } from 'timeago.js';
interface TimeChangeEvent {
prevValue: string;
nextValue: string;
date: Date | string;
}
function useObservableTimeAgo(
date: Date | string,
locale = 'ru',
onChange?: (event: TimeChangeEvent) => void
): string {
const [value, setValue] = useState(() => format(new Date(date as any), locale));
useEffect(() => {
const id = setInterval(() => {
const nextValue = format(new Date(date as any), locale);
setValue(prev => {
if (prev !== nextValue) {
onChange?.({ prevValue: prev, nextValue, date });
return nextValue;
}
return prev;
});
}, 30_000);
return () => clearInterval(id);
}, [date, locale]);
return value;
}
// Использование
function PostTime({ post }) {
const time = useObservableTimeAgo(post.createdAt, 'ru', (evt) => {
analytics.track('time_updated', {
from: evt.prevValue,
to: evt.nextValue,
});
});
return <time>{time}</time>;
}
class PausableObserver {
private paused = false;
private timers = new Map<string, ReturnType<typeof setInterval>>();
observe(id: string, fn: () => void, interval = 30_000): void {
this.timers.se t(id, setInterval(() => {
if (!this.paused) fn();
}, interval));
}
unobserve(id: string): void {
clearInterval(this.timers.get(id));
this.timers.delete(id);
}
pause(): void { this.paused = true; }
resume(): void { this.paused = false; }
destroy(): void {
this.timers.forEach(t => clearInterval(t));
this.timers.clear();
}
}
| Применение | Описание |
|---|---|
| Логирование обновлений | Счётчик для метрик production |
| Анимация при смене текста | CSS-переход при изменении значения |
| Уведомление state-менеджера | Синхронизация Redux/Pinia при изменении |
| Аналитика | Трекинг момента, когда пользователь видит изменение |
| A11y | Объявление изменения через aria-live |