Table Builder
The Table Builder is a powerful, framework-agnostic library for building, configuring, and managing complex data tables with support for sorting, filtering, pagination, row selection, custom rendering, and reactive updates.
Key Features
- ✅ Zero-Dependency Core - Pure TypeScript, works everywhere
- ✅ Type-Safe - Full TypeScript support with type inference
- ✅ Sorting - Single and multi-column sorting
- ✅ Filtering - Integrated filter service support
- ✅ Pagination - Built-in pagination with customizable page sizes
- ✅ Row Selection - Single and multi-select modes
- ✅ Column Configuration - Flexible column definitions with custom rendering
- ✅ Reactive Updates - RxJS Observable-based state management
- ✅ React Hooks -
useDynamicTable,useTableColumns,useTablePaginationfor React
Installation
# Install the table builder package
npm install @codella-software/utils
# Or from JSR
npx jsr add @codella-software/utils
Quick Start
Basic Table (Core)
import { TableBuilder } from '@codella-software/utils';
// Define your data
const data = [
{ id: 1, name: 'Alice', email: 'alice@example.com', role: 'Admin' },
{ id: 2, name: 'Bob', email: 'bob@example.com', role: 'User' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com', role: 'User' },
];
// Create table
const table = new TableBuilder({
data,
columns: [
{ id: 'id', accessorKey: 'id', header: 'ID', width: 80 },
{ id: 'name', accessorKey: 'name', header: 'Name', width: 150 },
{ id: 'email', accessorKey: 'email', header: 'Email', width: 200 },
{ id: 'role', accessorKey: 'role', header: 'Role', width: 100 }
],
pageSize: 10
});
// Subscribe to table data
table.displayedData$.subscribe(rows => {
console.log('Displaying rows:', rows);
});
// Get current state
const state = table.state$.value;
console.log('Total rows:', state.totalRows);
console.log('Current page:', state.currentPage);
Basic Table (React)
import { useDynamicTable } from '@codella-software/react';
export function UserTable() {
const data = [
{ id: 1, name: 'Alice', email: 'alice@example.com', role: 'Admin' },
{ id: 2, name: 'Bob', email: 'bob@example.com', role: 'User' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com', role: 'User' },
];
const {
columns,
displayedData,
sorting,
pagination,
selectRow,
selectedRows
} = useDynamicTable({
data,
columns: [
{ id: 'id', accessorKey: 'id', header: 'ID' },
{ id: 'name', accessorKey: 'name', header: 'Name' },
{ id: 'email', accessorKey: 'email', header: 'Email' },
{ id: 'role', accessorKey: 'role', header: 'Role' }
]
});
return (
<table>
<thead>
<tr>
{columns.map(col => (
<th key={col.id}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{displayedData.map(row => (
<tr key={row.id}>
<td>{row.id}</td>
<td>{row.name}</td>
<td>{row.email}</td>
<td>{row.role}</td>
</tr>
))}
</tbody>
</table>
);
}
Table with Sorting
Enable column sorting with multiple sort modes:
import { useDynamicTable } from '@codella-software/react';
export function SortableUserTable() {
const data = [
{ id: 1, name: 'Alice', email: 'alice@example.com', joinDate: '2023-01-15' },
{ id: 2, name: 'Bob', email: 'bob@example.com', joinDate: '2023-03-20' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com', joinDate: '2023-02-10' },
];
const { displayedData, sorting, setSorting } = useDynamicTable({
data,
columns: [
{ id: 'id', accessorKey: 'id', header: 'ID', sortable: true },
{ id: 'name', accessorKey: 'name', header: 'Name', sortable: true },
{ id: 'email', accessorKey: 'email', header: 'Email', sortable: true },
{ id: 'joinDate', accessorKey: 'joinDate', header: 'Join Date', sortable: true }
]
});
const handleSort = (columnKey: string) => {
const currentSort = sorting.find(s => s.id === columnKey);
const newDirection = currentSort?.direction === 'asc' ? 'desc' : 'asc';
setSorting([{ id: columnKey, direction: newDirection }]);
};
return (
<table>
<thead>
<tr>
<th onClick={() => handleSort('id')}>
ID {sorting.some(s => s.id === 'id') && '↓'}
</th>
<th onClick={() => handleSort('name')}>
Name {sorting.some(s => s.id === 'name') && '↓'}
</th>
<th onClick={() => handleSort('email')}>
Email {sorting.some(s => s.key === 'email') && '↓'}
</th>
<th onClick={() => handleSort('joinDate')}>
Join Date {sorting.some(s => s.key === 'joinDate') && '↓'}
</th>
</tr>
</thead>
<tbody>
{displayedData.map(row => (
<tr key={row.id}>
<td>{row.id}</td>
<td>{row.name}</td>
<td>{row.email}</td>
<td>{row.joinDate}</td>
</tr>
))}
</tbody>
</table>
);
}
Table with Filtering
Integrate with FiltersAndSort service for advanced filtering:
import { useDynamicTable } from '@codella-software/react';
import { FiltersAndSortService } from '@codella-software/utils';
export function FilteredUserTable() {
const data = [
{ id: 1, name: 'Alice', email: 'alice@example.com', role: 'Admin', active: true },
{ id: 2, name: 'Bob', email: 'bob@example.com', role: 'User', active: true },
{ id: 3, name: 'Charlie', email: 'charlie@example.com', role: 'User', active: false },
];
// Initialize filter service
const filterService = new FiltersAndSortService<{ role?: string }>({
storageKey: 'users-table',
defaultFilters: { role: '' }
});
filterService.setFilters({
role: 'User'
});
const { displayedData, columns } = useDynamicTable({
data,
columns: [
{ id: 'id', accessorKey: 'id', header: 'ID' },
{ id: 'name', accessorKey: 'name', header: 'Name' },
{ id: 'email', accessorKey: 'email', header: 'Email' },
{ id: 'role', accessorKey: 'role', header: 'Role' },
{ id: 'active', accessorKey: 'active', header: 'Active' }
],
filters$: filterService.state$
});
return (
<div>
<div className="filters">
<button onClick={() => filterService.setFilters({ role: 'Admin' })}>
Admin Only
</button>
<button onClick={() => filterService.clearFilters()}>
Clear Filters
</button>
</div>
<table>
<thead>
<tr>
{columns.map(col => (
<th key={col.key}>{col.label}</th>
))}
</tr>
</thead>
<tbody>
{displayedData.map(row => (
<tr key={row.id}>
<td>{row.id}</td>
<td>{row.name}</td>
<td>{row.email}</td>
<td>{row.role}</td>
<td>{row.active ? '✓' : '✗'}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Table with Pagination
Control data display with customizable pagination:
import { useDynamicTable, useTablePagination } from '@codella-software/react';
export function PaginatedTable() {
const data = Array.from({ length: 150 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`
}));
const { displayedData, pagination } = useDynamicTable({
data,
columns: [
{ id: 'id', accessorKey: 'id', header: 'ID' },
{ id: 'name', accessorKey: 'name', header: 'Name' },
{ id: 'email', accessorKey: 'email', header: 'Email' }
],
pageSize: 10
});
const { currentPage, totalPages, goToPage, nextPage, prevPage } = pagination;
return (
<div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{displayedData.map(row => (
<tr key={row.id}>
<td>{row.id}</td>
<td>{row.name}</td>
<td>{row.email}</td>
</tr>
))}
</tbody>
</table>
<div className="pagination">
<button onClick={prevPage} disabled={currentPage === 1}>
Previous
</button>
<span>
Page {currentPage} of {totalPages}
</span>
<button onClick={nextPage} disabled={currentPage === totalPages}>
Next
</button>
<select onChange={(e) => goToPage(parseInt(e.target.value))}>
{Array.from({ length: totalPages }, (_, i) => (
<option key={i + 1} value={i + 1}>
Page {i + 1}
</option>
))}
</select>
</div>
</div>
);
}
Table with Row Selection
Enable single or multi-select row selection:
import { useDynamicTable } from '@codella-software/react';
export function SelectableTable() {
const data = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' },
];
const { displayedData, selectedRows, selectRow, selectAll, selectionMode } = useDynamicTable({
data,
columns: [
{ id: 'id', accessorKey: 'id', header: 'ID' },
{ id: 'name', accessorKey: 'name', header: 'Name' },
{ id: 'email', accessorKey: 'email', header: 'Email' }
],
selectionMode: 'multi' // or 'single'
});
const handleSelectAll = (checked: boolean) => {
if (checked) {
selectAll();
} else {
selectedRows.forEach(id => selectRow(id));
}
};
return (
<div>
<div className="toolbar">
<button disabled={selectedRows.length === 0}>
Delete ({selectedRows.length})
</button>
<button disabled={selectedRows.length === 0}>
Export ({selectedRows.length})
</button>
</div>
<table>
<thead>
<tr>
<th>
<input
type="checkbox"
onChange={(e) => handleSelectAll(e.target.checked)}
checked={selectedRows.length === displayedData.length}
/>
</th>
<th>ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{displayedData.map(row => (
<tr key={row.id} className={selectedRows.includes(row.id) ? 'selected' : ''}>
<td>
<input
type="checkbox"
checked={selectedRows.includes(row.id)}
onChange={() => selectRow(row.id)}
/>
</td>
<td>{row.id}</td>
<td>{row.name}</td>
<td>{row.email}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Column Configuration
Customize column rendering and behavior with advanced options:
const columns = [
{
id: 'id',
accessorKey: 'id',
header: 'ID',
width: 80,
sortable: true,
hideable: false
},
{
id: 'name',
accessorKey: 'name',
header: 'Name',
width: 150,
sortable: true,
hideable: true,
cell: ({ value, row }) => (
<strong>{value}</strong>
)
},
{
id: 'email',
accessorKey: 'email',
header: 'Email',
width: 200,
sortable: true,
hideable: true,
cell: ({ value }) => (
<a href={`mailto:${value}`}>{value}</a>
)
},
{
id: 'status',
accessorKey: 'status',
header: 'Status',
width: 120,
cell: ({ value, row }) => (
<span className={`badge badge-${value.toLowerCase()}`}>
{value}
</span>
)
},
{
id: 'actions',
header: 'Actions',
width: 150,
sortable: false,
cell: ({ row }) => (
<div>
<button onClick={() => editRow(row)}>Edit</button>
<button onClick={() => deleteRow(row)}>Delete</button>
</div>
)
}
];
Column Properties
- id (required): Unique column identifier
- accessorKey: Data property to display (string or object key)
- accessorFn: Function to derive displayed value from row data
- header: Column header text or custom renderer
- cell: Custom cell renderer function
- footer: Column footer text or custom renderer
- width: Fixed column width (number or string)
- minWidth: Minimum column width
- maxWidth: Maximum column width
- sortable: Enable sorting for this column (default: true)
- sortField: Alternative field to sort by (if different from accessorKey)
- hideable: Allow user to toggle column visibility
- hidden: Initially hide column
- sticky: Stick column to left or right ('left' | 'right')
- align: Text alignment ('left' | 'center' | 'right')
- meta: Store arbitrary metadata on column
Advanced Features
Remote Data Loading
Load data from an API with pagination and filtering:
import { useDynamicTable } from '@codella-software/react';
import { useState, useEffect } from 'react';
export function RemoteDataTable() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const {
displayedData,
pagination,
sorting,
filters
} = useDynamicTable({
data,
columns: [
{ id: 'id', accessorKey: 'id', header: 'ID' },
{ id: 'name', accessorKey: 'name', header: 'Name' },
{ id: 'email', accessorKey: 'email', header: 'Email' }
]
});
useEffect(() => {
const loadData = async () => {
setLoading(true);
try {
const params = new URLSearchParams({
page: pagination.currentPage.toString(),
pageSize: '10',
sort: sorting.map(s => `${s.key}:${s.direction}`).join(',')
});
const response = await fetch(`/api/users?${params}`);
const result = await response.json();
setData(result.data);
} finally {
setLoading(false);
}
};
loadData();
}, [pagination.currentPage, sorting]);
if (loading) return <div>Loading...</div>;
return (
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{displayedData.map(row => (
<tr key={row.id}>
<td>{row.id}</td>
<td>{row.name}</td>
<td>{row.email}</td>
</tr>
))}
</tbody>
</table>
);
}
Expandable Rows
Show additional details for each row:
import { useDynamicTable } from '@codella-software/react';
import { useState } from 'react';
export function ExpandableTable() {
const [expandedRows, setExpandedRows] = useState<number[]>([]);
const data = [
{
id: 1,
name: 'Alice',
email: 'alice@example.com',
details: 'Senior Developer, 5 years experience'
},
{
id: 2,
name: 'Bob',
email: 'bob@example.com',
details: 'Product Manager, 3 years experience'
}
];
const { displayedData } = useDynamicTable({
data,
columns: [
{ id: 'id', accessorKey: 'id', header: 'ID' },
{ id: 'name', accessorKey: 'name', header: 'Name' },
{ id: 'email', accessorKey: 'email', header: 'Email' }
]
});
const toggleExpand = (id: number) => {
setExpandedRows(prev =>
prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]
);
};
return (
<table>
<thead>
<tr>
<th />
<th>ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{displayedData.map(row => (
<React.Fragment key={row.id}>
<tr>
<td>
<button onClick={() => toggleExpand(row.id)}>
{expandedRows.includes(row.id) ? '▼' : '▶'}
</button>
</td>
<td>{row.id}</td>
<td>{row.name}</td>
<td>{row.email}</td>
</tr>
{expandedRows.includes(row.id) && (
<tr>
<td colSpan={4}>
<div className="details">
<strong>Details:</strong> {row.details}
</div>
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
);
}
Editable Cells
Enable inline editing of table cells:
import { useDynamicTable } from '@codella-software/react';
import { useState } from 'react';
export function EditableTable() {
const [editingCell, setEditingCell] = useState<[number, string] | null>(null);
const [data, setData] = useState([
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
]);
const { displayedData } = useDynamicTable({
data,
columns: [
{ id: 'id', accessorKey: 'id', header: 'ID' },
{ id: 'name', accessorKey: 'name', header: 'Name' },
{ id: 'email', accessorKey: 'email', header: 'Email' }
]
});
const handleSave = (rowId: number, key: string, value: string) => {
setData(prev =>
prev.map(row =>
row.id === rowId ? { ...row, [key]: value } : row
)
);
setEditingCell(null);
};
return (
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{displayedData.map(row => (
<tr key={row.id}>
<td>{row.id}</td>
<td>
{editingCell?.[0] === row.id && editingCell?.[1] === 'name' ? (
<input
autoFocus
defaultValue={row.name}
onBlur={(e) => handleSave(row.id, 'name', e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave(row.id, 'name', e.currentTarget.value);
}}
/>
) : (
<span onClick={() => setEditingCell([row.id, 'name'])}>
{row.name}
</span>
)}
</td>
<td>
{editingCell?.[0] === row.id && editingCell?.[1] === 'email' ? (
<input
autoFocus
defaultValue={row.email}
onBlur={(e) => handleSave(row.id, 'email', e.target.value)}
/>
) : (
<span onClick={() => setEditingCell([row.id, 'email'])}>
{row.email}
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
);
}
Table with Form Integration
Combine Table Builder with Form Builder:
import { useDynamicTable } from '@codella-software/react';
import { useForm } from '@codella-software/react';
export function TableWithForm() {
const [tableData, setTableData] = useState([
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
]);
const { values, register, submit, reset } = useForm({
initialValues: { name: '', email: '' },
onSubmit: (values) => {
setTableData([
...tableData,
{ id: Date.now(), ...values }
]);
reset();
}
});
const { displayedData, columns } = useDynamicTable({
data: tableData,
columns: [
{ id: 'id', accessorKey: 'id', header: 'ID' },
{ id: 'name', accessorKey: 'name', header: 'Name' },
{ id: 'email', accessorKey: 'email', header: 'Email' }
]
});
return (
<div>
<form onSubmit={submit}>
<input {...register('name')} placeholder="Name" required />
<input {...register('email')} placeholder="Email" required />
<button type="submit">Add User</button>
</form>
<table>
<thead>
<tr>
{columns.map(col => (
<th key={col.id}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{displayedData.map(row => (
<tr key={row.id}>
<td>{row.id}</td>
<td>{row.name}</td>
<td>{row.email}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Next Steps
- Explore Form Builder for data input
- Check out Filters and Sort for advanced filtering
- See Core Services for reactive state management
- View more examples in the getting started guide