Перейти к основному содержимому

Работа с таблицами

Описание

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

Компонент таблицы в 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. На данный момент поддерживаются следующие типы данных:

typevalueВнешний вид
photoСсылка на изображениеИзображение фиксированной высоты и ширины
doubleКортеж ["Значение", "Описание"]Два параграфа. Верхний - значение, нижний - описание.
arrayМассив строк ['1', '2', '3', '4']• Маркированный список
dateСтрока в формате ISO-8601dd.mm.yyyy
datetimeСтрока в формате ISO-8601dd.mm.yyyy HH:MM
priceЦена в копейках, 150050Отформатированная цена в рублях, 1 500,50 Р
stringСтрокаСтрока
emailАдрес электронной почтыКликабельный адрес элетронный почты
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 }];
});
},
},
});