Форматирование чисел и дат

Формирование отображаемых значений в интерфейсе выбора в Tom Select напрямую влияет на восприятие данных пользователем, особенно когда элементы содержат числовые значения, валюты, даты, временные метки или смешанные типы данных. Библиотека оперирует строковыми представлениями, однако источником данных часто выступают структурированные объекты, требующие предварительного или динамического форматирования на уровне рендера.

Внутренний механизм Tom Select строится вокруг объектов вида:

{
  value: 1,
  text: "2024-01-01"
}

или более сложных структур:

{
  id: 42,
  name: "Order",
  amount: 15000,
  createdAt: "2025-05-10T14:30:00Z"
}

По умолчанию библиотека использует строковое поле text, однако при работе с числовыми и временными данными этого недостаточно. Основная задача заключается в том, чтобы преобразование происходило либо на этапе подготовки данных, либо на этапе рендера элементов.

Форматирование чисел перед передачей в Tom Select

Первый подход — нормализация данных до их передачи в компонент. Это снижает вычислительную нагрузку при рендеринге.

const data = orders.map(order => ({
  value: order.id,
  text: new Intl.NumberFormat('ru-RU').format(order.amount)
}));

Использование Intl.NumberFormat позволяет учитывать локаль, разделители тысяч и формат отображения валютных значений.

Расширенный вариант с валютой:

const formatter = new Intl.NumberFormat('ru-RU', {
  style: 'currency',
  currency: 'KZT'
});

const data = orders.map(order => ({
  value: order.id,
  text: formatter.format(order.amount)
}));

Такой подход обеспечивает единообразное отображение независимо от источника данных.

Динамическое форматирование чисел через render

При необходимости сохранить исходные значения и выполнять форматирование только при отображении применяется переопределение render:

new TomSelect("#select", {
  options: orders,
  valueField: "id",
  labelField: "name",
  searchField: ["name"],
  render: {
    option: function(data, escape) {
      return `
        <div>
          <span>${escape(data.name)}</span>
          <small>${new Intl.NumberFormat('ru-RU').format(data.amount)}</small>
        </div>
      `;
    },
    item: function(data, escape) {
      return `<div>${escape(data.name)} — ${new Intl.NumberFormat('ru-RU').format(data.amount)}</div>`;
    }
  }
});

Здесь форматирование отделено от модели данных, что позволяет изменять отображение без трансформации исходных объектов.

Форматирование дат в списках выбора

Работа с датами требует отдельного подхода, так как строковое представление ISO 8601 не подходит для интерфейсов пользователя.

Базовое преобразование:

const data = events.map(event => ({
  value: event.id,
  text: new Date(event.date).toLocaleDateString('ru-RU')
}));

Более контролируемый вариант через Intl.DateTimeFormat:

const dateFormatter = new Intl.DateTimeFormat('ru-RU', {
  year: 'numeric',
  month: 'long',
  day: '2-digit'
});

const data = events.map(event => ({
  value: event.id,
  text: dateFormatter.format(new Date(event.date))
}));

Такой способ обеспечивает стабильность форматирования при изменении локали и предотвращает неоднозначность представления даты.

Комбинированные структуры: числа и даты в одном элементе

При работе с бизнес-данными часто требуется отображение нескольких типов значений одновременно:

new TomSelect("#select", {
  options: invoices,
  valueField: "id",
  labelField: "title",
  searchField: ["title"],
  render: {
    option: function(data, escape) {
      const amount = new Intl.NumberFormat('ru-RU', {
        style: 'currency',
        currency: 'KZT'
      }).format(data.amount);

      const date = new Intl.DateTimeFormat('ru-RU', {
        year: 'numeric',
        month: 'short',
        day: '2-digit'
      }).format(new Date(data.createdAt));

      return `
        <div>
          <div>${escape(data.title)}</div>
          <div style="opacity:0.7; font-size:12px">
            ${amount} • ${date}
          </div>
        </div>
      `;
    }
  }
});

Такой подход формирует многоуровневое представление данных без изменения их структуры.

Предварительная нормализация данных как стратегия производительности

При большом объёме элементов (тысячи записей) форматирование на этапе рендера может стать узким местом. В таких случаях применяется предрасчёт:

const prepared = transactions.map(t => {
  const amount = new Intl.NumberFormat('ru-RU', {
    style: 'currency',
    currency: 'KZT'
  }).format(t.amount);

  const date = new Date(t.date).toLocaleDateString('ru-RU');

  return {
    id: t.id,
    title: t.title,
    amountFormatted: amount,
    dateFormatted: date
  };
});

И затем используется уже готовое поле:

render: {
  option: function(data, escape) {
    return `
      <div>
        <div>${escape(data.title)}</div>
        <small>${data.amountFormatted} • ${data.dateFormatted}</small>
      </div>
    `;
  }
}

Форматирование при поиске и фильтрации

Tom Select использует searchField, который работает по строковым значениям. При числовых данных требуется явное приведение:

new TomSelect("#select", {
  options: products,
  valueField: "id",
  labelField: "title",
  searchField: ["title", "amountString"],
  render: {
    option: function(data, escape) {
      return `
        <div>
          ${escape(data.title)}
          <small>${data.amountString}</small>
        </div>
      `;
    }
  }
});

Подготовка данных:

const products = rawProducts.map(p => ({
  ...p,
  amountString: String(p.amount)
}));

Для более точного поиска используется нормализация без форматирования (без пробелов и символов валюты), чтобы избежать искажений поиска.

Локализация и адаптация форматов

При динамическом изменении языка интерфейса форматирование должно быть реактивным. Используется фабрика форматтеров:

function createFormatters(locale) {
  return {
    number: new Intl.NumberFormat(locale),
    currency: new Intl.NumberFormat(locale, {
      style: "currency",
      currency: "KZT"
    }),
    date: new Intl.DateTimeFormat(locale)
  };
}

И применение в рендере:

const fmt = createFormatters('ru-RU');

render: {
  option: function(data, escape) {
    return `
      <div>
        ${escape(data.name)}
        <small>${fmt.currency.format(data.amount)}</small>
      </div>
    `;
  }
}

Работа с сырыми значениями и отображаемыми строками

Частая ошибка заключается в смешивании логики отображения и логики хранения. Tom Select хранит value, но отображает labelField. При форматировании чисел и дат важно сохранять неизменяемость value:

  • value — идентификатор
  • labelField — исходный текст
  • render — отображаемая форма

Такое разделение позволяет избегать потери точности при обратном преобразовании данных.

Форматирование при асинхронной загрузке данных

При использовании remote data источников форматирование выполняется после получения ответа:

new TomSelect("#select", {
  load: function(query, callback) {
    fetch(`/api/orders?q=${query}`)
      .then(res => res.json())
      .then(data => {
        callback(data.map(item => ({
          ...item,
          amountFormatted: new Intl.NumberFormat('ru-RU').format(item.amount),
          dateFormatted: new Date(item.date).toLocaleDateString('ru-RU')
        })));
      });
  }
});

Это позволяет разгрузить клиентский рендеринг и стандартизировать формат уже на уровне загрузчика.

Итоговая модель применения форматирования

При проектировании интерфейсов на Tom Select с числовыми и временными данными формируется три уровня обработки:

  1. Уровень данных — исходные объекты без изменений
  2. Уровень подготовки — нормализация, вычисление строковых представлений
  3. Уровень рендера — визуальное форматирование через render.option и render.item

Разделение этих уровней обеспечивает предсказуемость поведения интерфейса, стабильность отображения и масштабируемость при росте объёма данных.