Работа с таблицами
Описание
Таблица - один из главных компонентов административной панели.
Компонент таблицы в 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 }];
});
},
},
});