Динамическое добавление элементов

В современных приложениях DOM постоянно меняется: новые посты загружаются через infinite scroll, уведомления приходят через WebSocket, комментарии добавляются без перезагрузки. Для каждого нового элемента с датой нужно подключать автообновление.


Проблема динамического контента

render обрабатывает только элементы, существующие на момент вызова. Если позже добавляется новый элемент с datetime, он не попадает в систему обновлений автоматически.


Ручное подключение нового элемента

import { render } from 'timeago.js';

function addComment(comment) {
  const li = document.createElement('li');
  li.innerHTML = `
    <p>${comment.text}</p>
    <time class="comment-time" datetime="${comment.createdAt}"></time>
  `;

  document.getElementById('comments').appendChild(li);

  // Явно подключить новый элемент
  render(li.querySelector('.comment-time'), 'ru');
}

Загрузка данных через fetch

import { render } from 'timeago.js';

async function loadMorePosts() {
  const response = await fetch('/api/posts?page=2');
  const posts    = await response.json();

  const list = document.getElementById('feed');

  posts.forEach(post => {
    const li = document.createElement('li');
    li.innerHTML = `
      <h3>${post.title}</h3>
      <time datetime="${post.createdAt}"></time>
    `;
    list.appendChild(li);
  });

  // Инициализировать все новые элементы
  render(list.querySelectorAll('time'), 'ru');
}

WebSocket с автообновлением

import { render } from 'timeago.js';

const socket = new WebSocket('wss://example.com/feed');

socket.onmess age = (event) => {
  const post = JSON.parse(event.data);

  const el = document.createElement('article');
  el.innerHTML = `
    <h2>${post.title}</h2>
    <time datetime="${post.createdAt}"></time>
  `;

  document.getElementById('stream').prepend(el);

  render(el.querySelector('time'), 'ru');
};

Автоматическое подключение через MutationObserver

Для полностью автоматической инициализации — без ручных вызовов:

import { render, cancel } from 'timeago.js';

function initAutoTimeago(locale = 'ru') {
  // Инициализировать существующие элементы
  render(document.querySelectorAll('time[datetime]'), locale);

  const observer = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
      mutation.addedNodes.forEach(node => {
        if (node.nodeType !== Node.ELEMENT_NODE) return;

        // Проверить сам элемент
        if (node.matches('time[datetime]')) {
          render(node, locale);
        }

        // Проверить дочерние элементы
        const timeNodes = node.querySelectorAll('time[datetime]');
        if (timeNodes.length) {
          render(timeNodes, locale);
        }
      });

      mutation.removedNodes.forEach(node => {
        if (node.nodeType !== Node.ELEMENT_NODE) return;

        if (node.matches('time[datetime]')) {
          cancel(node);
        }

        node.querySelectorAll?.('time[datetime]').forEach(cancel);
      });
    });
  });

  observer.observe(document.body, {
    childList: true,
    subtree: true,
  });

  return {
    destroy() {
      observer.disconnect();
      cancel();
    }
  };
}

const watcher = initAutoTimeago('ru');

React: useEffect для динамических элементов

import { useEffect, useRef } from 'react';
import { render, cancel } from 'timeago.js';

function FeedItem({ post }) {
  const timeRef = useRef(null);

  useEffect(() => {
    if (!timeRef.current) return;
    render(timeRef.current, 'ru');
    return () => cancel(timeRef.current);
  }, [post.createdAt]);

  return (
    <article>
      <h3>{post.title}</h3>
      <time ref={timeRef} dateTime={post.createdAt} />
    </article>
  );
}

Infinite scroll с timeago

import { render } from 'timeago.js';

let page = 1;

async function loadNextPage() {
  const data = await fetch(`/api/posts?page=${page++}`).then(r => r.json());

  const fragment = document.createDocumentFragment();

  data.posts.forEach(post => {
    const li = document.createElement('li');
    li.innerHTML = `<time datetime="${post.createdAt}"></time>`;
    fragment.appendChild(li);
  });

  const container = document.getElementById('feed');
  container.appendChild(fragment);

  // Инициализировать только новые элементы
  const newTimes = container.querySelectorAll('li:not([data-initialized]) time[datetime]');
  render(newTimes, 'ru');
  newTimes.forEach(t => t.closest('li')?.setAttribute('data-initialized', '1'));
}

Паттерн: централизованный менеджер

import { render, cancel } from 'timeago.js';

class TimeagoManager {
  constructor(locale = 'ru') {
    this.locale   = locale;
    this.elements = new Set();
  }

  add(element) {
    if (!this.elements.has(element)) {
      this.elements.add(element);
      render(element, this.locale);
    }
  }

  remove(element) {
    cancel(element);
    this.elements.delete(element);
  }

  destroy() {
    this.elements.forEach(el => cancel(el));
    this.elements.clear();
  }
}

const manager = new TimeagoManager('ru');

// При добавлении нового поста
manager.add(newTimeElement);

// При удалении поста
manager.remove(oldTimeElement);