Работа с таблицами
Описание
Таблица - один из главных компонентов административной панели.
Компонент таблицы в Ensi основан на @tanstack/table. Для лучшего понимания принципов работы ознакомьтесь с официальной документации.
Столбцы
Таблица в целом определяется двумя основными параметрами - columns и data.
В самом простом виде columns представляет из себя массив объектов вида:
{
header: 'Название',
cell: props => props.getValue(),
accessorKey: 'title',
}
Здесь:
header: определяет то, как будет выглядить ячейка колонки в шапке (<thead>) таблицы
cell: определяет то, как будет выглядить ячейка колонки в теле (<tbody>) таблицы
accessorKey: определяет ключ, по которым из элемента массива data будет взято значение
Для большей гибкости в header и cell можно указывать полноценный реакт-компонент, который в качестве пропсов получает почти все данные таблицы. Например:
cell: ({ getValue, row, table }) =>
getValue().map(item => (
<p
key={item}
css={{ color: 'lightgrey' }}
onClick={() => {
console.log('this cell belongs to row', row, 'and table', table);
}}
>
{item}
</p>
)),
Встречающиеся почти во всех таблицах столбцы с чекбоксами и настройками вынесены в отдельные функции getSettingsColumn и getSelectColumn расположенные в папке columns.
Ячейка
Зачастую в таблицах требуется отображать разнородную информацию в специальном, отличном от простой ячейки виде: дата и время, изображения, кнопки действий, кликабельный номер телефона, логическое состояние да/нет.
Для упрощения представления создан компонент Cell.
Он имеет пропы type, value, row и metaField. На данный момент поддерживаются следующие типы данных:
| type | value | Внешний вид |
|---|---|---|
| photo | Ссылка на изображение | Изображение фиксированной высоты и ширины |
| double | Кортеж ["Значение", "Описание"] | Два параграфа. Верхний - значение, нижний - описание. |
| array | Массив строк ['1', '2', '3', '4'] | • Маркированный список |
| date | Строка в формате ISO-8601 | dd.mm.yyyy |
| datetime | Строка в формате ISO-8601 | dd.mm.yyyy HH:MM |
| price | Цена в копейках, 150050 | Отформатированная цена в рублях, 1 500,50 Р |
| string | Строка | Строка |
| Адрес электронной почты | Кликабельный адрес элетронный почты | |
| phone | Номер телефона | Кликабельный номер телефона |
| url | Ссылка | Кликабельная ссылка |
| int | Целое число | Целое число |
| float | Число с плавающей точкой | Число с плавающей точкой |
| enum | Любой объект | Если значение пустое, то прочерк. Если строковое, то строка. В остальных случаях отформатированный JSON |
| object | Любой объект | Если значение пустое, то прочерк. Если строковое, то строка. В остальных случаях отформатированный JSON |
| bool | Булево значение | Да / Нет |
| plural_numeric | Число в разных единицах измерения для разных строк. Для определения какую единцу измерения выводить, принимает дополнительный параметр metaField и ищет в нем value_types | <Значение> <единица измерения>, 10 руб, 10% |
Вспомогательные компоненты
TableHeader
Представляет из себя тэг <header> с предназначенными стилями. Используется для отображения обобщающей информации и действий над строками, располагается над самой таблицей.
TableEmpty
Представляет из себя блок, с разным текстом в случае наличия и отсутствия примененных фильтров (отвечают за это пропы filtersActive, titleWithFilters, titleWithoutFilters).
В странице где используется с условным рендером при отсутствии выдачи:
{!total && <TableEmpty filtersActive={filtersActive} />}
В случае примененных фильтров, имеет в себе текст предлагающий сбросить фильтры и кнопку с обработчиком сброса.
TableFooter
Компонент подвала таблицы. Стилизован. Содержит пагинацию и селект для выбора количества отображаемых элементов. Допустимое количество отображаемых элементов захардкожено в самом компоненте. Стейт, содержащий количество, и функция для его изменения прокидываются извне.
<TableFooter
pages={7}
itemsPerPageCount={10}
setItemsPerPageCount={...}
/>
Использование
Для максимальной гибкости компонента, рендер разделен с логикой через хук useTable, который является надстройкой над хуком useReactTable
const table = useTable({
// плагины, настройки, состояние, наследуются из useReactTable
});
const selectedRows = table.getSelectedRowModel().flatRows;
// можно получать доступ к состоянию таблицы(выделенные строки, редактирование и т.д.)
return <div>
<TableHeader>
<p>Выделено строк: {selectedRows.length}</p>
</TableHeader>
<Table
instance={table}
// визуальное отображение
/>
</div>;
Сортировка
Простейший пример использования сортировки:
// Задаем сортировку по-умолчанию - id по возрастанию
const initialSort: ColumnSort = {
id: 'id',
desc: false,
};
interface MyEntity {
id: number;
name: string;
}
const columnHelper = createColumnHelper<MyEntity>();
const columns = [
columnHelper.accessor('id', {
header: 'ID',
cell: props => props.getValue(),
}),
columnHelper.accessor('name', {
header: 'Название',
// Опционально можно выключить сортировку конкретных колонок
enableSorting: false,
cell: props => props.getValue(),
})
];
const EntitiesListPage = () => {
const [{ backendSorting }, sortingPlugin] = useSorting<MyEntity>(initialSort);
const { data } = useEntities({
...
sort: backendSorting,
});
const table = useTable(
{
data: products,
columns,
meta: {
tableKey: `entitiesList_entities`,
// уникальный ключ таблицы
// необходим для префиксов в атрибутах name элементов управления
// решает проблему когда выделение одних строк приводит к выделению других в одном разделе
},
},
[sortingPlugin]
);
};
Редактируемые данные в таблице
По-умолчанию не сделана возможность редактировать строки, но при необходимости можно реализовать:
interface MyEntity {
id: number;
name: string;
}
type TableValue = MyEntity[keyof MyEntity];
declare module '@tanstack/react-table' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface TableMeta<TData extends RowData> {
onChangeRow?: (row: Row<TData>, value: TableValue) => void;
}
}
const columns = [
{
accessorKey: 'name',
header: 'Название (редактируемое)',
cell: ({ getValue, row, table }) => {
const [value, setValue] = useState(() => getValue());
return (
<input
name="someinput"
value={value}
onChange={e => setValue(e.currentTarget.value)}
onBlur={() => {
if (table.options?.meta?.onChangeRow) {
table.options.meta.onChangeRow(row, value);
}
}}
/>
);
},
},
];
// Допустим можно хранить значения в виде массива редактированных строк
const [state, setState] = useState<{ row: Row<MyEntity>; value: TableValue }[]>([]);
const table = useTable({
meta: {
onChangeRow: (row, value) => {
setState(old => {
if (old.find(e => e.row === row)) {
return old.map(e => {
if (e.row === row) return { row, value };
return e;
});
}
return [...old, { row, value }];
});
},
},
});