Core functionality for Better Tables - A comprehensive React table library
npm install @better-tables/corebash
npm install @better-tables/core
or
yarn add @better-tables/core
or
pnpm add @better-tables/core
or
bun add @better-tables/core
`
Quick Start
$3
`typescript
import { createColumnBuilder } from '@better-tables/core';
interface User {
id: string;
name: string;
email: string;
age: number;
role: 'admin' | 'editor' | 'viewer';
status: 'active' | 'inactive';
createdAt: Date;
}
// Create a column builder for your data type
const cb = createColumnBuilder();
// Define columns with a fluent API
const columns = [
cb
.text()
.id('name')
.displayName('Name')
.accessor((user) => user.name)
.filterable()
.sortable()
.build(),
cb
.text()
.id('email')
.displayName('Email')
.accessor((user) => user.email)
.filterable()
.sortable()
.build(),
cb
.number()
.id('age')
.displayName('Age')
.accessor((user) => user.age)
.range(18, 100)
.filterable()
.sortable()
.build(),
cb
.option()
.id('role')
.displayName('Role')
.accessor((user) => user.role)
.options([
{ value: 'admin', label: 'Admin' },
{ value: 'editor', label: 'Editor' },
{ value: 'viewer', label: 'Viewer' },
])
.filterable()
.sortable()
.build(),
cb
.date()
.id('createdAt')
.displayName('Joined')
.accessor((user) => user.createdAt)
.filterable()
.sortable()
.build(),
];
`
$3
`typescript
import {
FilterManager,
SortingManager,
PaginationManager,
TableStateManager,
type FilterState,
type SortingState,
type PaginationState,
} from '@better-tables/core';
// Initialize managers
const filterManager = new FilterManager(columns);
const sortingManager = new SortingManager(columns, { multiSort: true });
const paginationManager = new PaginationManager({ defaultPageSize: 20 });
// Or use the unified TableStateManager
const tableStateManager = new TableStateManager(columns, {
filters: [],
pagination: { page: 1, limit: 20 },
sorting: [],
selectedRows: new Set(),
});
// Subscribe to state changes
const unsubscribe = tableStateManager.subscribe((event) => {
console.log('State changed:', event.type, event.payload);
});
// Update state
filterManager.addFilter({
columnId: 'name',
type: 'text',
operator: 'contains',
values: ['John'],
});
sortingManager.toggleSort('name');
paginationManager.setPage(2);
`
$3
The core package works seamlessly with adapters that support relationship filtering:
`typescript
interface UserWithRelations extends User {
profile?: {
bio: string;
location: string;
website?: string;
};
posts?: Array<{
id: string;
title: string;
views: number;
}>;
}
const cb = createColumnBuilder();
const columns = [
// Direct columns
cb.text().id('name').accessor((u) => u.name).build(),
// One-to-one relationship
cb
.text()
.id('profile.bio')
.displayName('Bio')
.nullableAccessor((user) => user.profile?.bio)
.filterable()
.build(),
cb
.text()
.id('profile.location')
.displayName('Location')
.nullableAccessor((user) => user.profile?.location)
.filterable()
.build(),
// One-to-many relationship (access first item)
cb
.text()
.id('posts.title')
.displayName('Latest Post')
.nullableAccessor((user) => user.posts?.[0]?.title)
.build(),
];
`
Core Concepts
$3
Column builders provide a fluent, type-safe API for defining table columns. Each builder type supports specific features:
- TextColumnBuilder - Text data with search, truncation, and validation
- NumberColumnBuilder - Numeric data with ranges and formatting
- DateColumnBuilder - Date/time data with formatting and ranges
- BooleanColumnBuilder - Boolean values with custom labels
- OptionColumnBuilder - Single-select options with custom rendering
- MultiOptionColumnBuilder - Multi-select options
`typescript
// Text column with advanced features
cb.text()
.id('name')
.displayName('Full Name')
.accessor((user) => ${user.firstName} ${user.lastName})
.searchable()
.filterable()
.sortable()
.truncate({ maxLength: 50, suffix: '...' })
.build();
// Number column with range validation
cb.number()
.id('age')
.displayName('Age')
.accessor((user) => user.age)
.range(0, 120)
.format('number')
.filterable()
.sortable()
.build();
// Option column with custom rendering
cb.option()
.id('status')
.displayName('Status')
.accessor((user) => user.status)
.options([
{ value: 'active', label: 'Active', color: 'green' },
{ value: 'inactive', label: 'Inactive', color: 'red' },
])
.filterable()
.sortable()
.build();
`
$3
State managers handle different aspects of table state:
#### FilterManager
Manages filter state with support for multiple filter types and operators.
`typescript
const filterManager = new FilterManager(columns);
// Add a filter
filterManager.addFilter({
columnId: 'name',
type: 'text',
operator: 'contains',
values: ['John'],
});
// Get all filters
const filters = filterManager.getFilters();
// Remove a filter
filterManager.removeFilter('name');
// Clear all filters
filterManager.clearFilters();
`
#### SortingManager
Handles single and multi-column sorting.
`typescript
const sortingManager = new SortingManager(columns, {
multiSort: true,
maxSortColumns: 3,
});
// Toggle sort on a column
sortingManager.toggleSort('name');
// Set explicit sort
sortingManager.setSorting([{ columnId: 'name', direction: 'asc' }]);
// Get current sorting
const sorting = sortingManager.getSorting();
`
#### PaginationManager
Manages pagination state and calculations.
`typescript
const paginationManager = new PaginationManager({
defaultPageSize: 20,
pageSizeOptions: [10, 20, 50, 100],
});
// Navigate pages
paginationManager.setPage(2);
paginationManager.nextPage();
paginationManager.previousPage();
// Change page size
paginationManager.setPageSize(50);
// Get pagination state
const pagination = paginationManager.getPagination();
`
#### TableStateManager
Unified manager that coordinates all table state.
`typescript
const tableStateManager = new TableStateManager(columns, {
filters: [],
pagination: { page: 1, limit: 20 },
sorting: [],
selectedRows: new Set(),
});
// Subscribe to all state changes
tableStateManager.subscribe((event) => {
switch (event.type) {
case 'filter':
console.log('Filter changed:', event.payload);
break;
case 'sort':
console.log('Sort changed:', event.payload);
break;
case 'pagination':
console.log('Pagination changed:', event.payload);
break;
}
});
// Update multiple states at once
tableStateManager.updateState({
filters: [{ columnId: 'name', type: 'text', operator: 'contains', values: ['John'] }],
pagination: { page: 1, limit: 20 },
});
`
$3
Create actions for bulk operations on selected rows.
`typescript
import { createActionBuilder } from '@better-tables/core';
import { Trash2 } from 'lucide-react';
const deleteAction = createActionBuilder()
.id('delete')
.label('Delete Selected')
.icon(Trash2)
.variant('destructive')
.confirmationDialog({
title: 'Delete Users',
description: 'Are you sure you want to delete {count} user(s)?',
confirmLabel: 'Delete',
cancelLabel: 'Cancel',
destructive: true,
})
.handler(async (selectedIds: string[]) => {
// Perform deletion
await fetch('/api/users', {
method: 'DELETE',
body: JSON.stringify({ ids: selectedIds }),
});
})
.build();
`
$3
The core package includes utility functions for common operations:
`typescript
import {
serializeFiltersToURL,
deserializeFiltersFromURL,
deepEqual,
shallowEqualArrays,
} from '@better-tables/core';
// Serialize filters for URL storage (compressed with lz-string)
const urlParams = serializeFiltersToURL(filters);
// Result: "c:..." (compressed URL-safe string, prefixed with "c:")
// Deserialize from URL (expects compressed format)
const filters = deserializeFiltersFromURL(urlParams);
// Equality checks
const isEqual = deepEqual(obj1, obj2);
const arraysEqual = shallowEqualArrays(arr1, arr2);
`
API Overview
$3
#### Column Builders
`typescript
import {
TextColumnBuilder,
NumberColumnBuilder,
DateColumnBuilder,
BooleanColumnBuilder,
OptionColumnBuilder,
MultiOptionColumnBuilder,
ColumnBuilder,
} from '@better-tables/core';
`
#### Factory Functions
`typescript
import {
createColumnBuilder,
createColumnBuilders,
createTypedColumnBuilder,
column,
typed,
createActionBuilder,
createActionBuilders,
} from '@better-tables/core';
`
#### State Managers
`typescript
import {
FilterManager,
SortingManager,
PaginationManager,
SelectionManager,
TableStateManager,
VirtualizationManager,
} from '@better-tables/core';
`
#### Types
`typescript
import type {
ColumnDefinition,
FilterState,
FilterOperator,
PaginationState,
SortingState,
SelectionState,
VirtualizationConfig,
TableConfig,
TableAdapter,
} from '@better-tables/core';
`
#### Utilities
`typescript
import {
serializeFiltersToURL,
deserializeFiltersFromURL,
deepEqual,
shallowEqualArrays,
} from '@better-tables/core';
`
Usage Examples
$3
`typescript
import { createColumnBuilder, type FilterState, type SortingState } from '@better-tables/core';
import { DrizzleAdapter } from '@better-tables/adapters-drizzle';
// Define columns
const cb = createColumnBuilder();
const columns = [
cb.text().id('name').accessor((u) => u.name).filterable().sortable().build(),
cb.text().id('email').accessor((u) => u.email).filterable().build(),
];
// Create adapter
const adapter = new DrizzleAdapter({
db,
schema,
mainTable: 'users',
driver: 'sqlite',
});
// Fetch data with filters and sorting
const filters: FilterState[] = [
{
columnId: 'name',
type: 'text',
operator: 'contains',
values: ['John'],
},
];
const sorting: SortingState = [{ columnId: 'name', direction: 'asc' }];
const result = await adapter.fetchData({
columns: ['name', 'email'],
filters,
sorting,
pagination: { page: 1, limit: 20 },
});
console.log(result.data); // Array of User records
console.log(result.total); // Total count
`
$3
`typescript
// app/page.tsx
import type { FilterState, SortingState } from '@better-tables/core';
import { deserializeFiltersFromURL } from '@better-tables/core';
import { decompressAndDecode } from '@better-tables/core';
import { getAdapter } from '@/lib/adapter';
export default async function Page({ searchParams }: { searchParams: Promise> }) {
const params = await searchParams;
// Parse URL params
const page = Number.parseInt(params.page || '1', 10);
const limit = Number.parseInt(params.limit || '10', 10);
// Deserialize filters (compressed format, prefixed with "c:")
let filters: FilterState[] = [];
if (params.filters) {
try {
filters = deserializeFiltersFromURL(params.filters);
} catch {
// Invalid or corrupted filter data, use empty array
filters = [];
}
}
// Deserialize sorting (compressed format, prefixed with "c:")
let sorting: SortingState = [];
if (params.sorting) {
try {
sorting = decompressAndDecode(params.sorting);
} catch {
// Invalid or corrupted sorting data, use empty array
sorting = [];
}
}
// Fetch data
const adapter = await getAdapter();
const result = await adapter.fetchData({
columns: defaultVisibleColumns,
pagination: { page, limit },
filters,
sorting,
});
return
;
}
`
Advanced Usage
$3
`typescript
cb.text()
.id('avatar')
.displayName('User')
.accessor((user) => user.name)
.cellRenderer(({ value, row }) => (
{value}
))
.build();
`
$3
`typescript
cb.text()
.id('profile.bio')
.displayName('Bio')
.nullableAccessor((user) => user.profile?.bio, '-') // Default value for null
.build();
`
$3
`typescript
cb.text()
.id('description')
.displayName('Description')
.accessor((item) => item.description)
.truncate({
maxLength: 100,
suffix: '...',
showTooltip: true, // Show full text on hover
})
.build();
`
$3
`typescript
import { serializeFiltersToURL, deserializeFiltersFromURL } from '@better-tables/core';
// Save filters to URL
const filters: FilterState[] = [
{ columnId: 'name', type: 'text', operator: 'contains', values: ['John'] },
{ columnId: 'age', type: 'number', operator: 'greaterThan', values: [18] },
];
const urlParams = serializeFiltersToURL(filters);
// Use in URL: ?filters=c:... (compressed format, prefixed with "c:")
// Restore from URL (must be compressed format)
const restoredFilters = deserializeFiltersFromURL(urlParams);
`
Documentation
For detailed documentation, see:
- User Guide - Complete feature reference and usage patterns
- Column Builders Guide - Deep dive into column configuration
- Managers API Reference - State management API documentation
- Types API Reference - Complete type definitions
- Utilities API Reference - Utility function documentation
- Architecture - Design decisions and system overview
Examples
See the demo app for a complete working example:
- Column Definitions: apps/demo/lib/columns/user-columns.tsx
- Action Builders: apps/demo/lib/actions/user-actions.tsx
- Integration: apps/demo/components/users-table-client.tsx
TypeScript Support
The core package is built with TypeScript and provides full type safety:
`typescript
// Type inference from accessors
const cb = createColumnBuilder();
// Accessor types are inferred
cb.text()
.id('name')
.accessor((user) => user.name) // TypeScript knows user is User
.build();
// Column definitions are fully typed
const columns: ColumnDefinition[] = [
// TypeScript will error if accessor doesn't match User type
];
`
Contributing
Contributions are welcome! This is an open-source project, and we appreciate any help you can provide.
$3
1. Fork the repository
2. Create a feature branch (git checkout -b feature/amazing-feature)
3. Make your changes
4. Run tests (bun test)
5. Commit your changes (git commit -m 'Add some amazing feature')
6. Push to the branch (git push origin feature/amazing-feature)
7. Open a Pull Request
$3
- Documentation: Improving guides and examples
- Tests: Adding more test coverage
- Performance: Optimizing state management
- Type Safety: Enhancing TypeScript types
- Utilities: Adding helpful utility functions
See CONTRIBUTING.md for detailed guidelines.
License
MIT License - see LICENSE for details.
Related Packages
- @better-tables/ui - React components built on top of core
- @better-tables/adapters-drizzle - Drizzle ORM adapter
- Demo App - Complete working example
Support
- GitHub Issues - Report bugs or request features
- GitHub Discussions - Ask questions and share ideas
- Documentation - Comprehensive guides in the docs/` directory