Перейти к содержимому
Поиск
Разработка

Tabulator: таблицы для веб-сервисов — мастерская и 6 примеров

Мини-мастерская Tabulator и шесть рабочих интерфейсов: каталог, редактор расчёта, задачи по группам, массовые действия, список заказов и карточка клиента. Живые таблицы, пояснения и исходники.

Александр Некрасов
Tabulator — таблицы для сервисов: фильтры, редактируемые строки и выбор записей

Таблица в веб-сервисе часто становится рабочим местом: найти заявку, изменить количество, выбрать несколько записей, проверить итог и скачать результат. Обычная HTML-таблица хорошо показывает строки, но поиск, сортировку, редактирование и постраничную навигацию вокруг неё приходится собирать самостоятельно.

Tabulator превращает массив объектов в управляемую таблицу. В этой статье можно настроить собственный вариант в мини-мастерской и попробовать шесть полноценных интерфейсов: каталог, расчёт стоимости, задачи по группам, массовую обработку заявок, журнал заказов и карточку организации. Все примеры используют Tabulator 6.7.1; доступны исходники и самостоятельный HTML.

Попробуйте за пять минут: найдите услуги по слову «форм» → измените количество в расчёте → выберите две заявки и поменяйте статус → скачайте отфильтрованные строки. Сразу видно, какую часть работы берёт библиотека, а что нужно написать в приложении.

Обычная HTML-таблица или Tabulator: где проходит граница

Для прайс-листа из десяти строк, сравнительной таблицы в статье или печатного документа обычно достаточно <table>, заголовков <th> и CSS. Такой вариант прост, хорошо вписывается в текст и не требует JavaScript для чтения.

Представьте другой случай: оператор видит сто заявок, отбирает новые, сортирует по сумме, назначает исполнителя и выгружает выбранные. Здесь появляется состояние интерфейса: фильтры, выбор, текущая страница, изменённые значения. Tabulator даёт механизмы для этой работы, а вы связываете их с задачей пользователя.

Задача HTML-таблица Tabulator
Показать небольшой набор Достаточно разметки и CSS Можно, но дополнительные возможности могут не понадобиться
Сортировать и искать Нужны свои обработчики и обновление строк Настройки колонок, фильтры и методы API
Редактировать значения Нужны поля, сохранение и проверки Редакторы ячеек и валидаторы; сохранение на сервер пишется отдельно
Работать с выбором и страницами Состояние и навигация реализуются вручную Есть выбор строк и локальная или серверная пагинация

Данные и колонки описываются отдельно. field указывает на свойство объекта, а title задаёт подпись пользователю. Стабильный id помогает обновлять нужную запись после сортировки: номер видимой строки для этого не подходит.

const rows = [
  { id: 'S-01', name: 'Аудит интерфейса', price: 18000 },
  { id: 'S-02', name: 'Форма заявки', price: 12000 }
];
const columns = [
  { title: 'Услуга', field: 'name' },
  { title: 'Цена, ₽', field: 'price', sorter: 'number' }
];

Библиотека работает с обычным JavaScript, без обязательного React или jQuery. У неё есть тема Bootstrap 5. Авторизация, проверка прав, запись в базу и бизнес-правила остаются на стороне вашего приложения.

Документация: темы, включая Bootstrap 5.

Мини-мастерская: настройте свою рабочую таблицу

Мастерская позволяет менять данные и настройки, видеть результат и сразу получать код. Начните с небольшого набора: понятные названия, числа без символа валюты внутри значения, отдельная колонка категории. Затем настройте поиск, отображение колонок и количество строк на странице.

Упражнение: оставьте в таблице только самые важные поля, измените порядок сортировки и сократите размер страницы. Проверьте, можно ли найти нужную запись и понять итог без горизонтального прокручивания всей страницы. Названия колонок должны объяснять данные даже после переноса в мобильные подробности.

Сохраняйте HTML, когда нужен редактируемый самостоятельный пример; JSON — когда нужны исходные строки. Конфигурация мастерской показывается как JavaScript, поскольку содержит функции форматирования. JSON такие функции не переносит. Полный HTML включает необходимые ресурсы и запоминает данные, настройки, поиск и сортировку.

В исходном наборе шесть позиций на 14 000 ₽. Поменяйте цену «Диагностики» с 1 500 на 2 000 ₽ — общая сумма станет 14 500 ₽. В поле «Поиск по названию и категории» над таблицей введите «Сервис» — останутся три позиции. Это поле доступно и на телефоне. Сводка мастерской относится ко всем строкам; число найденных показано отдельно. Кнопка CSV также выгружает все строки, как указано в её названии.

Изменения в демонстрациях выполняются локально. Они не меняют рабочую CRM и не отправляют введённые записи серверу. Сброс на странице возвращает исходный пример; сброс в экспортированном HTML возвращает состояние на момент сохранения файла.

Шесть интерфейсов: от поиска до массовых действий

Каждая вкладка ниже решает отдельную задачу. Здесь важны не только строки, но и пояснение, управляющие элементы, итоги и реакция на пустой результат. Демонстрационные данные фиксированы: действия можно повторить и сверить с ожидаемыми числами.

Пример Где пригодится Что проверить
Каталог услуг Прайс-лист, каталог работ Поиск и совместную работу фильтров
Расчёт стоимости Калькулятор заказа, предварительный расчёт Редактирование, сумму и отмену изменения
Задачи по группам Планирование команды Группировку по статусу и исполнителю
Массовые действия Очередь заявок в CRM Выбор строк, смену статуса и отмену
Журнал заказов Большой список с навигацией Страницы, фильтр и размер выдачи
Организации и проекты Справочник с подробной карточкой Открытие записи и связанные проекты

Скачать мастерскую и все исходники ZIP. В архиве есть инструкция запуска. Общие страницы index.html и lab.html открывайте через локальный сервер, поскольку сборка самостоятельного HTML читает соседние файлы. Готовые файлы examples/ и скачанный HTML можно открывать отдельно без CDN.

1. Каталог услуг: поиск, фильтры и понятный итог

Каталог помогает подобрать услуги без ручного просмотра всех позиций. В примере восемь услуг общей стоимостью 198 000 ₽. Поиск сокращает набор, сортировка меняет порядок, а итог объясняет, что осталось после отбора. Это подходит для прайс-листов, справочников оборудования и каталогов внутренних сервисов.

Попробуйте: введите «форм» — останутся две услуги на 46 000 ₽. Затем в фильтре названия колонки укажите «слож» — останется одна строка на 28 000 ₽. Очистите оба поля и проверьте восстановление списка. Несколько активных фильтров легко забыть, поэтому возможность сброса должна находиться рядом.

Tabulator выполняет фильтрацию и сортировку. Приложение задаёт смысл поиска и пересчитывает сводку по тому же набору. В этом фрагменте поиск идёт по названию; table — уже созданная и готовая таблица.

function searchByName(table, query) {
  const text = query.trim();
  if (text) table.setFilter('name', 'like', text);
  else table.clearFilter();
}

table.on('dataFiltered', (filters, rows) => {
  const total = rows.reduce((sum, row) => sum + row.getData().price, 0);
  document.getElementById('result').textContent =
    `Найдено: ${rows.length}. Сумма: ${total.toLocaleString('ru-RU')} ₽`;
});

Фильтры заголовков и программные фильтры имеют отдельное состояние. Для кнопки полного сброса предусмотрите оба вида, например clearFilter(true). Подпись «Найдено» относится ко всему результату фильтра, а не только к строкам текущей страницы.

Документация: фильтры и поиск.

Открыть каталог услуг ↑

2. Расчёт стоимости: редактирование с проверкой

В расчёте можно менять количество и цену позиции, добавлять и удалять строки, отменять изменения. Таблица удобна для быстрого ввода, а отдельная форма под ней помогает на телефоне: не нужно попадать в маленькую ячейку и разбираться, какие поля доступны для изменения.

Попробуйте: у «Макета интерфейса» поменяйте количество с 4 на 5 при цене 3 000 ₽. Общая стоимость трёх позиций вырастет с 31 000 до 34 000 ₽. Затем выполните отмену. Пустое или отрицательное количество не должно превращаться в правдоподобный новый итог.

Редактор и валидатор предоставляет библиотека. Формула суммы и поведение кнопок принадлежат приложению. Встроенные числовые проверки пропускают пустое значение без отдельного required, поэтому набор правил важно задавать явно.

const quantityColumn = {
  title: 'Количество', field: 'quantity', editor: 'number',
  editorParams: { min: 0, max: 1000, step: 1 },
  validator: ['required', 'integer', 'min:0', 'max:1000']
};

table.on('cellEdited', cell => {
  if (!['quantity', 'price'].includes(cell.getField())) return;
  const row = cell.getRow();
  const data = row.getData();
  row.update({ total: data.quantity * data.price });
});

Здесь показан принцип, а не полный редактор. Для денежных расчётов заранее определите округление; при сохранении используйте целые копейки или подходящий десятичный тип. Клиентская проверка делает ввод удобнее, но сервер обязан проверить изменения повторно. Локальная отмена не откатывает уже сохранённую запись в базе автоматически.

Документация: режимы проверки значений.

Открыть расчёт стоимости ↑

3. Задачи по группам: увидеть состав работы

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

Попробуйте: сравните группы статусов: «Новая» — три задачи и 15 часов, «В работе» — три и 24 часа, «Готово» — три и 9 часов. Переключите группировку на ответственного, сверните все группы и откройте только Анну кнопкой над таблицей: в её группе три задачи на 18 часов. Общий набор остаётся прежним — девять задач на 48 часов.

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

const options = {
  groupBy: 'status',
  groupStartOpen: true,
  columns: [
    { title: 'Задача', field: 'title' },
    { title: 'Исполнитель', field: 'assignee' },
    { title: 'Часы', field: 'hours', sorter: 'number', bottomCalc: 'sum' }
  ]
};
// Для уже готовой таблицы:
table.setGroupBy('assignee');

Группа — представление строк с одинаковым значением, а не отдельная копия данных. Смена группировки не должна менять сами задачи. Подписывайте, входят ли в итог только отфильтрованные записи. Для длительности не смешивайте часы и минуты внутри одного числового столбца.

Документация: группировка строк.

Документация: расчёты по колонкам.

Открыть задачи по группам ↑

4. Массовая обработка заявок: выбор должен быть явным

Когда одно действие применяется сразу к нескольким заявкам, интерфейс обязан объяснить его область. Нужны заметные флажки, количество выбранных, понятное название действия и возможность снять выбор. Цвет строки служит дополнительной подсказкой.

Попробуйте: выберите R-101 и R-102, назначьте статус «Готово». Число готовых заявок изменится с двух до четырёх из восьми. Отмените действие — снова останется две. Демонстрация меняет локальные данные и не отправляет уведомления настоящим людям.

Библиотека хранит выбор и возвращает выбранные объекты. Приложение проверяет, разрешён ли переход статуса, фиксирует предыдущее состояние и запускает сохранение. Пример ниже меняет существующие записи по их идентификаторам:

async function markSelectedDone(table) {
  const selected = table.getSelectedData();
  if (selected.length === 0) return 0;
  await table.updateData(selected.map(row => ({ id: row.id, status: 'Готово' })));
  table.deselectRow();
  return selected.length;
}

table.on('rowSelectionChanged', data => {
  document.getElementById('selected-count').textContent =
    `Выбрано: ${data.length}`;
});

В Tabulator выбор может сохраняться после фильтрации. Поэтому заранее решите, участвуют ли скрытые выбранные строки в массовой операции. Не называйте действие «Изменить видимые», если код берёт весь выбор. В реальном сервисе результат сервера может содержать частичный отказ; показывайте его отдельно от успешно изменённых записей.

Документация: выбор строк и его сохранение.

Открыть массовую обработку ↑

5. Журнал заказов: страницы и размер выдачи

Журнал содержит 120 демонстрационных заказов на 615 000 ₽. Размер страницы переключается между 5, 10 и 20 строками. Это уменьшает плотность экрана и помогает сохранять контекст, особенно если у каждой строки несколько действий.

Попробуйте: выберите статус «Готово» — останется 40 заказов. Установите 20 строк на страницу: результат займёт две страницы. При смене фильтра текущая страница возвращается к первой, чтобы пользователь не оказался на пустом хвосте прежней выдачи.

Этот пример использует локальную пагинацию: все 120 записей уже находятся в браузере. Нажатие «Следующая» не загружает новую порцию с сервера. Указывайте размер выдачи и общий найденный объём рядом с управлением страницами.

const options = {
  pagination: true,
  paginationMode: 'local',
  paginationSize: 10,
  paginationSizeSelector: [5, 10, 20],
  paginationButtonCount: 3,
  paginationCounter: 'rows'
};

Локальные страницы не уменьшают первоначальный объём загружаемых данных. Если записей много, переходите к серверному API, где поиск и сортировка выполняются по всему доступному пользователю набору. Пример такого подключения разобран ниже; для демонстрационного журнала отдельный сервер не нужен.

Документация: локальная и серверная пагинация.

Открыть журнал заказов ↑

6. Организации и проекты: подробности выбранной строки

В справочнике важно сохранить компактный обзор и при этом дать доступ к деталям. Шестой интерфейс показывает шесть вымышленных организаций. Кнопка «Открыть» выводит карточку под таблицей: менеджера, этап, заметку и связанные проекты. Контактов реальных людей в данных нет.

Попробуйте: откройте студию «Ритм». У неё два проекта: на 36 000 и 24 000 ₽, вместе 60 000 ₽. Перейдите к другой организации и убедитесь, что заголовок, заметка и список проектов обновились одновременно.

Tabulator создаёт ячейку действия; карточку строит наше приложение. Возвращайте настоящий DOM-элемент кнопки: он доступен с клавиатуры и позволяет безопасно передать текст. showCustomer() в этом фрагменте — ваша функция обновления карточки.

function openButtonFormatter(cell) {
  const button = document.createElement('button');
  button.type = 'button';
  button.className = 'btn btn-outline-primary btn-sm';
  button.textContent = 'Открыть';
  button.setAttribute('aria-label', 'Открыть: ' + cell.getRow().getData().name);
  button.addEventListener('click', () => showCustomer(cell.getRow().getData()));
  return button;
}

const actionColumn = {
  title: 'Карточка', headerSort: false, formatter: openButtonFormatter
};

После открытия объявите выбор текстом и дайте понятный переход к карточке. На телефоне карточка может находиться ниже первого экрана. Не делайте двойной щелчок по строке единственным способом её открыть: такое действие трудно обнаружить и неудобно повторять на сенсорном экране.

Документация: форматтеры и колонки действий.

Открыть организации и проекты ↑

Подключение: полный HTML, npm и модули

Для обычной страницы нужны CSS темы и JavaScript библиотеки. Ниже — минимальный полный пример с Bootstrap 5. Сохраните файл как HTML и откройте в браузере. Он использует CDN, поэтому нужен интернет; в архиве статьи библиотеки лежат локально.

<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Каталог услуг</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tabulator-tables@6.7.1/dist/css/tabulator_bootstrap5.min.css">
  <style>body{max-width:900px;margin:auto;padding:24px 16px}#services{min-width:0}</style>
</head>
<body>
  <h1>Каталог услуг</h1>
  <label for="search" class="form-label">Найти услугу</label>
  <input id="search" class="form-control mb-3" disabled>
  <div id="services" aria-label="Услуги и цены"></div>
  <noscript>Для интерактивной таблицы включите JavaScript.</noscript>
  <script src="https://cdn.jsdelivr.net/npm/tabulator-tables@6.7.1/dist/js/tabulator.min.js"></script>
  <script>
    const table = new Tabulator('#services', {
      index: 'id', layout: 'fitColumns', height: '280px',
      placeholder: 'Услуги не найдены',
      data: [
        { id: 1, name: 'Аудит', price: 18000 },
        { id: 2, name: 'Форма заявки', price: 12000 }
      ],
      columns: [
        { title: 'Услуга', field: 'name', minWidth: 140 },
        { title: 'Цена, ₽', field: 'price', sorter: 'number',
          hozAlign: 'right', minWidth: 100 }
      ]
    });
    const search = document.getElementById('search');
    table.on('tableBuilt', () => { search.disabled = false; });
    search.addEventListener('input', () => {
      const query = search.value.trim();
      if (query) table.setFilter('name', 'like', query);
      else table.clearFilter();
    });
  </script>
</body>
</html>

Это UMD-сборка: после загрузки доступен глобальный Tabulator. Скрипт библиотеки должен выполниться раньше вашего кода. В проекте со сборкой используйте npm и ESM:

npm install tabulator-tables@6.7.1
import { TabulatorFull as Tabulator } from 'tabulator-tables';
import 'tabulator-tables/dist/css/tabulator_bootstrap5.min.css';

// После появления контейнера в DOM:
const table = new Tabulator('#services', { data: rows, columns });

TabulatorFull уже включает модули. При выборочной сборке нужно зарегистрировать используемые возможности. Не копируйте короткий список модулей в сложный пример вслепую: редакторы, пагинация, группировка и экспорт требуют своих модулей.

import { Tabulator, FormatModule, SortModule, FilterModule } from 'tabulator-tables';
Tabulator.registerModule([FormatModule, SortModule, FilterModule]);

Тема Bootstrap заменяет стандартный CSS Tabulator. Одновременное подключение двух полных тем усложнит предсказуемость оформления. Установленная версия должна совпадать у JavaScript, CSS и примеров.

Документация: установка и асинхронная инициализация.

Данные и жизненный цикл: что обновлять и когда

Инициализация Tabulator асинхронная. Обработчики можно назначить сразу после конструктора, но команды, которым нужна готовая таблица, вызывайте после tableBuilt. Это событие не означает завершение будущих сетевых запросов; результат загрузки отслеживается отдельно.

setData() загружает новый набор, updateData() меняет переданные поля существующих строк по id, replaceData() заменяет данные с сохранением состояния сортировки и фильтрации. Для сохранения прокрутки при замене документация требует высоту таблицы и виртуальный DOM.

async function refreshRows(table, nextRows) {
  await table.replaceData(nextRows);
  return table.getData();
}

// Выполняйте после готовности таблицы, например из кнопки:
async function changePrice(table) {
  await table.updateData([{ id: 'S-01', price: 19500 }]);
}

// Полная смена набора: await table.setData(nextRows).
// При удалении компонента со страницы: table.destroy().

Неизвестный идентификатор в updateData() не создаёт новую строку. Для добавления нужны addData() или addRow(). Перед повторным созданием таблицы в том же месте уничтожайте прежний экземпляр и снимайте собственные обработчики внешних кнопок.

Проверяйте схему данных до отображения: уникальность идентификаторов, типы, допустимые диапазоны. Цена должна храниться числом, а не строкой «12 000 ₽». Пустота и настоящий ноль имеют разный смысл; преобразование Number('') само по себе этого не учитывает.

function nonNegativeNumber(raw) {
  if (raw === null || raw === undefined || String(raw).trim() === '') {
    throw new Error('Введите число');
  }
  const value = Number(raw);
  if (!Number.isFinite(value) || value < 0) throw new Error('Число должно быть от нуля');
  return value;
}

Форматирование не должно менять исходное число. Для текста из внешнего источника используйте обычный текстовый форматтер или DOM-узел с textContent. Строка HTML из произвольной заметки клиента не должна становиться разметкой страницы.

function safeTextFormatter(cell) {
  const span = document.createElement('span');
  span.textContent = String(cell.getValue() ?? '');
  return span;
}

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

const remoteOptions = {
  ajaxURL: '/api/orders.php',
  pagination: true,
  paginationMode: 'remote',
  paginationSize: 20,
  filterMode: 'remote',
  sortMode: 'remote'
};

Обычный ответ серверной пагинации имеет следующую форму. last_page — число страниц, last_row — общее количество найденных записей, data — строки запрошенной страницы. Сервер проверяет параметры запроса, ограничения размера и права доступа.

{
  "last_page": 3,
  "last_row": 45,
  "data": [
    { "id": "O-041", "name": "Заказ 41", "amount": 5000 }
  ]
}

Форма выше показывает контракт, а не содержимое всех 45 строк. При ошибке API сообщайте о неудачной загрузке и давайте повтор; при пустом ответе показывайте «Ничего не найдено». Старый результат с новой подписью фильтра может ввести пользователя в заблуждение.

Документация: замена и обновление данных.

Документация: загрузка, серверные фильтры и сортировка.

Мобильная таблица: сохранить смысл, а не просто уменьшить шрифт

fitColumns распределяет доступную ширину между колонками. Когда полей много, этого недостаточно. В режиме responsiveLayout: 'collapse' второстепенные значения можно перенести в список под строкой. Главное поле оставьте видимым, а колонки снабдите минимальной шириной.

const responsiveOptions = {
  layout: 'fitColumns',
  responsiveLayout: 'collapse',
  responsiveLayoutCollapseStartOpen: false,
  columns: [
    { formatter: 'responsiveCollapse', width: 44, minWidth: 44,
      headerSort: false, resizable: false, responsive: 0 },
    { title: 'Название', field: 'name', minWidth: 150, responsive: 0 },
    { title: 'Исполнитель', field: 'owner', minWidth: 140, responsive: 2 },
    { title: 'Сумма, ₽', field: 'amount', minWidth: 110, responsive: 1 }
  ]
};

Скрытые колонки должны оставаться доступными через раскрытие или карточку. Для редактирования добавляйте отдельную форму выбранной строки, если редакторы в таблице неудобны на узком экране. На странице не должно появляться случайной горизонтальной прокрутки из-за контейнера grid или flex.

.table-host { min-width: 0; max-width: 100%; }
.table-controls { display: flex; gap: 10px; flex-wrap: wrap; }
.table-controls button, .table-controls input { min-height: 44px; }
@media (max-width: 600px) {
  .table-controls input, .table-controls select { font-size: 16px; }
}

Проверьте путь с клавиатуры: поиск, сортировку, открытие подробностей, изменение поля, отмену и возврат фокуса. Кнопки должны иметь названия, ошибки — текстовые пояснения. Наличие ARIA-атрибутов у библиотеки не гарантирует удобство конкретной сборки: пользовательские форматтеры и скрытые колонки требуют своей проверки.

Если таблица создана внутри скрытой вкладки, после её открытия может понадобиться redraw(true). Для больших наборов фиксированная высота позволяет использовать виртуализацию строк. Она уменьшает количество элементов DOM, но не заменяет серверную загрузку и не уменьшает размер уже полученного массива.

Документация: адаптивность, высота и виртуализация.

Документация: доступность интерфейса.

CSV, JSON и самостоятельный HTML: выбираем результат

Экспорт должен отвечать на вопрос «какие именно записи я получу?». В шести интерфейсах CSV содержит весь отфильтрованный набор, а не только текущую страницу; у мини-мастерской отдельная кнопка «Скачать все строки CSV». JSON удобен для передачи данных между программами. Самостоятельный HTML сохраняет интерфейс и текущий вариант для дальнейших экспериментов.

// active: строки после фильтрации, включая другие страницы.
table.download('csv', 'services.csv', { bom: true, delimiter: ';' }, 'active');
table.download('json', 'services.json', {}, 'active');

// selected: выбранные строки, в том числе скрытые фильтром.
table.download('csv', 'selected.csv', { bom: true }, 'selected');

visible означает текущую видимую область таблицы, active — результат фильтра, selected — выбор. Не смешивайте эти режимы в подписи одной кнопки. Для группированных и древовидных данных действуют дополнительные ограничения; область экспорта проверяется на конкретном сценарии.

Кавычки CSV защищают структуру полей, но не мешают табличному редактору распознать формулу. Если текст пришёл от пользователя, значения с начальными =, +, - или @ требуют отдельной обработки. Один из вариантов — префикс для текстовых полей при экспорте:

function spreadsheetText(value) {
  const text = String(value ?? '');
  const risky = /^[\s\u0000-\u001f]*[=+\-@=+-@]/.test(text)
    || /^[\t\r\n]/.test(text);
  return risky ? "'" + text : text;
}
const nameColumn = {
  title: 'Название', field: 'name',
  accessorDownload: spreadsheetText
};

Это мера для текстового CSV, не универсальная гарантия для любых редакторов: при сохранении и повторном открытии Excel может менять экранирование. Для точного обмена исходными значениями сохраняйте JSON и проверяйте ожидаемый способ открытия CSV. Разбор CSV Injection у OWASP.

XLSX потребует SheetJS, PDF — jsPDF и AutoTable. Встроенная выгрузка HTML создаёт таблицу; полноценная рабочая страница с кнопками и редакторами в нашей мастерской собирается отдельным кодом. Она включает библиотеку и стили, поэтому после сохранения не зависит от CDN.

Документация: форматы и область экспорта.

PHP: передать подготовленные данные и сохранить изменения

Для PHP-страницы сервер может сразу подготовить разрешённые пользователю записи. Передавайте их через json_encode(), а не склеивайте JavaScript из строк базы. Вариант ниже предназначен для PHP-шаблона: контейнер #services, CSS и Tabulator подключаются обычным способом.

<?php
$rows = [
    ['id' => 'S-01', 'name' => 'Аудит интерфейса', 'price' => 18000],
    ['id' => 'S-02', 'name' => 'Форма заявки', 'price' => 12000],
];
$json = json_encode($rows,
    JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP |
    JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR
);
?>
<script id="table-data" type="application/json"><?= $json ?></script>
<script>
  const rows = JSON.parse(document.getElementById('table-data').textContent);
  new Tabulator('#services', {
    data: rows, index: 'id', layout: 'fitColumns',
    columns: [
      { title: 'Услуга', field: 'name' },
      { title: 'Цена, ₽', field: 'price', sorter: 'number' }
    ]
  });
</script>

Флаги JSON_HEX_* защищают границу HTML, включая последовательность закрытия script внутри строки. Не применяйте к этому JSON htmlspecialchars(): в script-блоке HTML-сущности не декодируются как обычный текст страницы. Ошибки кодирования обрабатывайте на сервере.

Изменение ячейки в браузере ещё не означает сохранение. Для рабочего сервиса понадобится отдельный запрос: идентификатор записи, разрешённые изменённые поля и, при необходимости, версия записи для защиты от конфликтов. Сервер проверяет сессию, права, допустимые поля, типы и диапазоны, сохраняет данные и возвращает подтверждённый результат.

Пока запрос выполняется, покажите состояние «Сохраняем». После отказа не оставляйте изменённое число без пояснения: восстановите подтверждённое значение или явно обозначьте несохранённую правку. Демонстрации статьи показывают локальную механику и не притворяются готовым серверным приложением.

Документация PHP: json_encode().

Частые ошибки: проверка перед переносом в проект

Начните с маленького набора с известным результатом. Затем проверьте пустой поиск, одинаковые значения, неверный ввод и быстрые переключения. Красиво оформленная таблица может работать с неправильной областью данных, поэтому сопоставляйте строки, счётчики, итог и скачанный файл.

Симптом Что проверить Как исправить
Tabulator is not defined Загрузку библиотеки и порядок скриптов Подключить UMD раньше собственного кода или использовать ESM-импорт
Метод вызывается слишком рано Готовность экземпляра Дождаться tableBuilt; загрузку данных проверять отдельно
Обновилась не та строка Уникальность и тип id Обновлять по стабильному ключу, не по видимому номеру
Новая запись не появилась Использование updateData для отсутствующего id Использовать addData/addRow или заменить весь набор
Пустое количество принято Наличие required вместе с numeric/integer Различать пустоту, ноль и допустимое число
Итог не совпал с фильтром По каким строкам считается сумма Использовать одну область данных для таблицы и сводки
Колонки стали слишком узкими minWidth и правила responsive Оставить ключевое поле, второстепенные вынести в подробности
Выгрузились неожиданные строки active, visible, selected и скрытый выбор Подписать область выгрузки и проверить файл
Правка пропала после перезагрузки Есть ли серверное сохранение или экспорт Явно сообщать о локальном состоянии и способе его сохранить

Не пересоздавайте таблицу при каждом символе поиска: используйте API фильтрации. Не смешивайте разные полные темы CSS. Не переносите демонстрационные формулы в финансовую систему без согласованных правил округления и проверки сервером.

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

Исходники и документация: куда двигаться дальше

Tabulator особенно полезен там, где данные нужно не только показать, но и обработать: отобрать, изменить, сгруппировать, передать в другое представление. Начните с одного понятного сценария и добавляйте возможности по мере необходимости. Набор настроек должен помогать работе, а не превращать каждую колонку в отдельную панель управления.

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

Вместе с таблицами удобно использовать Chart.js для графиков, GridStack.js для размещения карточек и Alpine.js для состояния интерфейса. Общий массив данных может питать и таблицу, и показатели, и диаграмму; согласованные фильтры делают такую страницу понятнее.