Zum Hauptinhalt springen

Table

Die Table-Komponente bietet eine leistungsstarke und flexible Möglichkeit, tabellarische Daten anzuzeigen. Sie unterstützt Sortierung, Filterung, Paginierung und benutzerdefinierte Zellendarstellung.

Import

import { Table } from '@smolitux/core';

Verwendung

Einfache Tabelle

const data = [
{ id: 1, name: 'Max Mustermann', email: 'max@example.com', role: 'Admin' },
{ id: 2, name: 'Erika Musterfrau', email: 'erika@example.com', role: 'User' },
{ id: 3, name: 'John Doe', email: 'john@example.com', role: 'Editor' },
];

const columns = [
{
id: 'name',
header: 'Name',
accessor: (row) => row.name,
},
{
id: 'email',
header: 'E-Mail',
accessor: (row) => row.email,
},
{
id: 'role',
header: 'Rolle',
accessor: (row) => row.role,
},
];

<Table
data={data}
columns={columns}
rowKey={(row) => row.id.toString()}
/>

Tabelle mit benutzerdefinierten Zellen

const columns = [
{
id: 'name',
header: 'Name',
accessor: (row) => row.name,
},
{
id: 'email',
header: 'E-Mail',
accessor: (row) => row.email,
cell: (value) => <a href={`mailto:${value}`}>{value}</a>,
},
{
id: 'role',
header: 'Rolle',
accessor: (row) => row.role,
cell: (value) => {
const badgeColor = {
'Admin': 'danger',
'User': 'primary',
'Editor': 'success',
}[value] || 'default';

return <Badge variant={badgeColor}>{value}</Badge>;
},
},
{
id: 'actions',
header: 'Aktionen',
accessor: (row) => row.id,
cell: (value, row) => (
<div className="flex space-x-2">
<Button size="sm" variant="outline" onClick={() => handleEdit(row)}>
Bearbeiten
</Button>
<Button size="sm" variant="danger" onClick={() => handleDelete(row)}>
Löschen
</Button>
</div>
),
},
];

Sortierbare Tabelle

const [sortConfig, setSortConfig] = useState({ key: 'name', direction: 'asc' });

const handleSort = (columnId) => {
setSortConfig((prevSortConfig) => {
if (prevSortConfig.key === columnId) {
return {
key: columnId,
direction: prevSortConfig.direction === 'asc' ? 'desc' : 'asc',
};
}
return { key: columnId, direction: 'asc' };
});
};

const columns = [
{
id: 'name',
header: 'Name',
accessor: (row) => row.name,
sortable: true,
},
{
id: 'email',
header: 'E-Mail',
accessor: (row) => row.email,
sortable: true,
},
{
id: 'role',
header: 'Rolle',
accessor: (row) => row.role,
sortable: true,
},
];

// Sortiere die Daten basierend auf der Konfiguration
const sortedData = useMemo(() => {
if (!sortConfig.key) return data;

return [...data].sort((a, b) => {
const aValue = a[sortConfig.key];
const bValue = b[sortConfig.key];

if (aValue < bValue) {
return sortConfig.direction === 'asc' ? -1 : 1;
}
if (aValue > bValue) {
return sortConfig.direction === 'asc' ? 1 : -1;
}
return 0;
});
}, [data, sortConfig]);

<Table
data={sortedData}
columns={columns}
rowKey={(row) => row.id.toString()}
sortConfig={sortConfig}
onSort={handleSort}
/>

Tabelle mit Paginierung

const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10;

// Berechne die anzuzeigenden Daten basierend auf der aktuellen Seite
const paginatedData = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
return data.slice(startIndex, startIndex + pageSize);
}, [data, currentPage, pageSize]);

// Berechne die Gesamtzahl der Seiten
const totalPages = Math.ceil(data.length / pageSize);

<>
<Table
data={paginatedData}
columns={columns}
rowKey={(row) => row.id.toString()}
/>

<div className="flex justify-between items-center mt-4">
<div>
Zeige {(currentPage - 1) * pageSize + 1} bis {Math.min(currentPage * pageSize, data.length)} von {data.length} Einträgen
</div>

<div className="flex space-x-2">
<Button
variant="outline"
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
>
Zurück
</Button>

{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
<Button
key={page}
variant={currentPage === page ? 'primary' : 'outline'}
onClick={() => setCurrentPage(page)}
>
{page}
</Button>
))}

<Button
variant="outline"
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
>
Weiter
</Button>
</div>
</div>
</>

Tabelle mit Filterung

const [filters, setFilters] = useState({});

const handleFilterChange = (columnId, value) => {
setFilters(prev => ({
...prev,
[columnId]: value,
}));
};

// Filtere die Daten basierend auf den aktiven Filtern
const filteredData = useMemo(() => {
return data.filter(row => {
return Object.entries(filters).every(([columnId, filterValue]) => {
if (!filterValue) return true;

const column = columns.find(col => col.id === columnId);
if (!column) return true;

const cellValue = column.accessor(row);
return String(cellValue).toLowerCase().includes(String(filterValue).toLowerCase());
});
});
}, [data, filters, columns]);

const columns = [
{
id: 'name',
header: 'Name',
accessor: (row) => row.name,
filterable: true,
},
{
id: 'email',
header: 'E-Mail',
accessor: (row) => row.email,
filterable: true,
},
{
id: 'role',
header: 'Rolle',
accessor: (row) => row.role,
filterable: true,
},
];

<Table
data={filteredData}
columns={columns}
rowKey={(row) => row.id.toString()}
filters={filters}
onFilterChange={handleFilterChange}
/>

Props

PropTypStandardBeschreibung
dataT[]-Tabellendaten als Array von Objekten
columnsTableColumn<T>[]-Spaltenkonfiguration
rowKey(row: T) => string(_, index) => index.toString()Funktion zur Generierung eines eindeutigen Schlüssels für jede Zeile
onRowClick(row: T) => void-Callback für Klick auf eine Zeile
loadingbooleanfalseZeigt einen Ladeindikator an
emptyMessageReactNode'Keine Daten vorhanden'Nachricht, wenn keine Daten vorhanden sind
classNamestring-Zusätzliche CSS-Klassen für die Tabelle
headerClassNamestring-CSS-Klassen für den Tabellenkopf
bodyClassNamestring-CSS-Klassen für den Tabellenkörper
rowClassNamestring | (row: T, index: number) => string-CSS-Klassen für Tabellenzeilen
cellClassNamestring | (value: any, row: T, index: number) => string-CSS-Klassen für Tabellenzellen
stripedbooleanfalseZeigt abwechselnd gefärbte Zeilen an
hoverablebooleanfalseHebt Zeilen beim Darüberfahren hervor
borderedbooleanfalseZeigt Rahmen um die Tabelle und Zellen an
compactbooleanfalseReduziert den Abstand in der Tabelle
sortablebooleanfalseAktiviert die Sortierung für alle Spalten
sortConfig{ key: string; direction: 'asc' | 'desc' }-Aktuelle Sortierkonfiguration
onSort(columnId: string) => void-Callback für Sortieränderungen
filterablebooleanfalseAktiviert die Filterung für alle Spalten
filtersRecord<string, any>-Aktuelle Filterkonfiguration
onFilterChange(columnId: string, value: any) => void-Callback für Filteränderungen
paginationbooleanfalseAktiviert die Paginierung
pageSizenumber10Anzahl der Zeilen pro Seite
currentPagenumber1Aktuelle Seite
totalPagesnumber-Gesamtzahl der Seiten
onPageChange(page: number) => void-Callback für Seitenänderungen
showPageSizeSelectorbooleanfalseZeigt einen Selektor für die Seitengröße an
pageSizeOptionsnumber[][10, 25, 50, 100]Optionen für die Seitengröße
onPageSizeChange(pageSize: number) => void-Callback für Änderungen der Seitengröße
showPaginationInfobooleantrueZeigt Informationen zur Paginierung an
showSearchbooleanfalseZeigt ein Suchfeld an
searchValuestring-Aktueller Suchwert
onSearchChange(value: string) => void-Callback für Suchänderungen
searchPlaceholderstring'Suchen...'Platzhaltertext für das Suchfeld
showToolbarbooleanfalseZeigt eine Toolbar über der Tabelle an
toolbarContentReactNode-Benutzerdefinierter Inhalt für die Toolbar
showColumnSelectorbooleanfalseZeigt einen Spaltenauswahl-Dialog an
onColumnVisibilityChange(columnId: string, visible: boolean) => void-Callback für Änderungen der Spaltensichtbarkeit
stickyHeaderbooleanfalseFixiert den Tabellenkopf beim Scrollen
stickyFirstColumnbooleanfalseFixiert die erste Spalte beim Scrollen
heightstring | number-Höhe der Tabelle
maxHeightstring | number-Maximale Höhe der Tabelle
resizablebooleanfalseErmöglicht das Ändern der Spaltenbreite
onColumnResize(columnId: string, width: number) => void-Callback für Änderungen der Spaltenbreite
selectablebooleanfalseErmöglicht die Auswahl von Zeilen
selectedRowsstring[]-Array von ausgewählten Zeilen-IDs
onSelectionChange(selectedRows: string[]) => void-Callback für Änderungen der Zeilenauswahl
selectAllRowsbooleanfalseErmöglicht die Auswahl aller Zeilen

TableColumn Interface

EigenschaftTypStandardBeschreibung
idstring-Eindeutige ID der Spalte
headerReactNode-Angezeigter Header-Text
accessor(row: T) => any-Funktion zur Extraktion des Zellwerts aus einer Zeile
cell(value: any, row: T, index: number) => ReactNode-Benutzerdefinierte Render-Funktion für die Zelle
classNamestring-Zusätzliche CSS-Klassen für die Spalte
headerClassNamestring-CSS-Klassen für den Header der Spalte
sortablebooleanfalseFalls true, ist die Spalte sortierbar
filterablebooleanfalseFalls true, ist die Spalte filterbar
widthstring-Breite der Spalte
minWidthstring-Minimale Breite der Spalte
maxWidthstring-Maximale Breite der Spalte
hidebooleanfalseFalls true, wird die Spalte ausgeblendet

Barrierefreiheit

Die Table-Komponente ist für Barrierefreiheit optimiert:

  • Verwendet semantische HTML-Tabellen-Tags (<table>, <thead>, <tbody>, <tr>, <th>, <td>)
  • Korrekte ARIA-Attribute für sortierbare und filterbare Spalten
  • Tastaturnavigation für Paginierung und Sortierung
  • Screenreader-Unterstützung für Tabellendaten

Beispiele

Tabelle mit Zeilenauswahl

const [selectedRows, setSelectedRows] = useState([]);

const handleSelectionChange = (selected) => {
setSelectedRows(selected);
};

const handleBulkAction = () => {
console.log('Ausgewählte Zeilen:', selectedRows);
// Führe Aktionen mit den ausgewählten Zeilen durch
};

<>
{selectedRows.length > 0 && (
<div className="mb-4 p-2 bg-primary-50 rounded flex justify-between items-center">
<span>{selectedRows.length} Einträge ausgewählt</span>
<Button onClick={handleBulkAction}>Massenbearbeitung</Button>
</div>
)}

<Table
data={data}
columns={columns}
rowKey={(row) => row.id.toString()}
selectable
selectedRows={selectedRows}
onSelectionChange={handleSelectionChange}
selectAllRows
/>
</>

Tabelle mit erweiterbaren Zeilen

const [expandedRows, setExpandedRows] = useState({});

const toggleRowExpanded = (rowId) => {
setExpandedRows(prev => ({
...prev,
[rowId]: !prev[rowId],
}));
};

const renderExpandedContent = (row) => (
<div className="p-4 bg-gray-50">
<h4 className="font-medium">Details für {row.name}</h4>
<div className="grid grid-cols-2 gap-4 mt-2">
<div>
<p><strong>Adresse:</strong> {row.address}</p>
<p><strong>Telefon:</strong> {row.phone}</p>
</div>
<div>
<p><strong>Registriert am:</strong> {new Date(row.registeredAt).toLocaleDateString()}</p>
<p><strong>Letzte Aktivität:</strong> {new Date(row.lastActivity).toLocaleDateString()}</p>
</div>
</div>
</div>
);

const columns = [
{
id: 'expander',
header: '',
accessor: (row) => row.id,
cell: (value) => (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
toggleRowExpanded(value);
}}
>
{expandedRows[value] ? '▼' : '►'}
</Button>
),
width: '40px',
},
// ... andere Spalten
];

<Table
data={data}
columns={columns}
rowKey={(row) => row.id.toString()}
expandedRows={expandedRows}
renderExpandedContent={renderExpandedContent}
/>

Tabelle mit Drag-and-Drop für Zeilen

import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';

const DraggableRow = ({ row, index, moveRow, ...props }) => {
const [, ref] = useDrag({
type: 'ROW',
item: { index },
});

const [, drop] = useDrop({
accept: 'ROW',
hover: (draggedItem) => {
if (draggedItem.index !== index) {
moveRow(draggedItem.index, index);
draggedItem.index = index;
}
},
});

return <tr ref={(node) => ref(drop(node))} {...props} />;
};

function DraggableTable() {
const [rows, setRows] = useState(data);

const moveRow = (fromIndex, toIndex) => {
const updatedRows = [...rows];
const [movedRow] = updatedRows.splice(fromIndex, 1);
updatedRows.splice(toIndex, 0, movedRow);
setRows(updatedRows);
};

return (
<DndProvider backend={HTML5Backend}>
<Table
data={rows}
columns={columns}
rowKey={(row) => row.id.toString()}
renderRow={(row, index, props) => (
<DraggableRow
key={row.id}
row={row}
index={index}
moveRow={moveRow}
{...props}
/>
)}
/>
</DndProvider>
);
}